merging hashing and price history APIs
This commit is contained in:
parent
ed41b6a1f5
commit
a0c59e4030
181
index.html
Normal file
181
index.html
Normal file
@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RanchiMall Utility APIs</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid black;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
background-color: #eee;
|
||||
padding: 0.3rem;
|
||||
border-radius: 0.2rem;
|
||||
font: monospace;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #222;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border-color: #eee;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #333;
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section style="padding:4vw;">
|
||||
<h1>
|
||||
Welcome to the RanchiMall Utility APIs!
|
||||
</h1>
|
||||
<h2>
|
||||
Endpoints:
|
||||
</h2>
|
||||
<ol>
|
||||
<li>
|
||||
<h3>
|
||||
<a href="/price-history">/price-history</a>
|
||||
</h3>
|
||||
<h4>
|
||||
Query parameters:
|
||||
</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>format | values</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>from</td>
|
||||
<td>No</td>
|
||||
<td>None</td>
|
||||
<td>YYYY-MM-DD</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>to</td>
|
||||
<td>No</td>
|
||||
<td>None</td>
|
||||
<td>YYYY-MM-DD</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>on</td>
|
||||
<td>No</td>
|
||||
<td>None</td>
|
||||
<td>YYYY-MM-DD</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>limit</td>
|
||||
<td>No</td>
|
||||
<td>100</td>
|
||||
<td>all | <number></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>asset</td>
|
||||
<td>No</td>
|
||||
<td>btc</td>
|
||||
<td>btc</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>currency</td>
|
||||
<td>No</td>
|
||||
<td>All</td>
|
||||
<td>usd | inr</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>
|
||||
Example:
|
||||
</h4>
|
||||
<code>
|
||||
/price-history?from=2020-01-01&to=2020-01-31
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
<h3>
|
||||
<a href="/hash">/hash</a>
|
||||
</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>POST</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Body</td>
|
||||
<td>JSON</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Body parameter</td>
|
||||
<td>urls [Array]</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>
|
||||
Example:
|
||||
</h4>
|
||||
<code>
|
||||
fetch('https://utility-api.ranchimall.net/hash',{ <br>
|
||||
method: 'POST',<br>
|
||||
headers: {<br>
|
||||
'Content-Type': 'application/json'<br>
|
||||
},<br>
|
||||
body: JSON.stringify({ urls: [url] })<br>
|
||||
}).then(res => res.json()).then(console.log)
|
||||
<br>
|
||||
<br>
|
||||
Output: <br>
|
||||
[{<br>
|
||||
"url": url,<br>
|
||||
"hash": hash<br>
|
||||
}]
|
||||
</code>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
152
index.js
152
index.js
@ -1,155 +1,57 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const mongoose = require('mongoose');
|
||||
const cors = require('cors');
|
||||
const axios = require('axios');
|
||||
const { createHash } = require('crypto');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const { parse: parseUrl, URL } = require('url');
|
||||
const { parse: parseHtml } = require('node-html-parser');
|
||||
const path = require('path');
|
||||
|
||||
// Set up the allowed domains (replace with your specific domains)
|
||||
const allowedDomains = process.env.ALLOWED_DOMAINS.split(',');
|
||||
// const allowedDomains = process.env.ALLOWED_DOMAINS.split(',');
|
||||
const app = express();
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const HOST = process.env.HOST || '127.0.0.1';
|
||||
|
||||
// Middleware to parse JSON requests
|
||||
app.use(express.json());
|
||||
// Middleware to enable CORS
|
||||
// pass the cors options to the cors middleware to enable CORS for the allowed domains
|
||||
// const corsOptions = {
|
||||
// origin: allowedDomains,
|
||||
// optionsSuccessStatus: 200, // Some legacy browsers (IE11, various SmartTVs) choke on 204
|
||||
// }
|
||||
app.use(cors());
|
||||
const port = process.env.PORT || 3000;
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
|
||||
// Middleware to parse JSON requests
|
||||
app.use(express.json());
|
||||
// Middleware to enable CORS
|
||||
|
||||
|
||||
app.use(
|
||||
rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
max: 20, // limit each IP request per windowMs
|
||||
max: 30, // limit each IP request per windowMs
|
||||
})
|
||||
);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Hello There!');
|
||||
})
|
||||
function addProtocolToUrl(url) {
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = 'https://' + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function parseUrlWithoutHashAndQuery(fullUrl) {
|
||||
fullUrl = addProtocolToUrl(fullUrl);
|
||||
const parsedUrl = new URL(fullUrl);
|
||||
|
||||
// Set the hash and search/query to empty strings
|
||||
parsedUrl.hash = '';
|
||||
parsedUrl.search = '';
|
||||
|
||||
// Reconstruct the URL without hash and query
|
||||
const urlWithoutHashAndQuery = parsedUrl.toString();
|
||||
|
||||
return urlWithoutHashAndQuery;
|
||||
}
|
||||
// hashContent function to hash the content of a file
|
||||
async function hashContent(content) {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(content);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
// Recursive function to fetch and hash content, including linked resources
|
||||
async function fetchAndHashContent(url, visitedUrls = new Set()) {
|
||||
if (visitedUrls.has(url)) {
|
||||
return ''; // Avoid fetching the same URL multiple times to prevent infinite loops
|
||||
}
|
||||
|
||||
visitedUrls.add(url);
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||
const content = response.data.toString('utf-8');
|
||||
// Parse HTML content to identify linked resources
|
||||
const root = parseHtml(content);
|
||||
const linkedResources = root.querySelectorAll('link[rel="stylesheet"], script[src]');
|
||||
// Fetch and hash linked resources
|
||||
const linkedResource = await Promise.all(linkedResources.map(async (resource) => {
|
||||
const resourceUrl = parseUrl(resource.getAttribute('href') || resource.getAttribute('src'), true);
|
||||
let absoluteResourceUrl = resourceUrl.href;
|
||||
if (!resourceUrl.hostname) {
|
||||
if (!resourceUrl.path.startsWith('/') && !url.endsWith('/'))
|
||||
url += '/';
|
||||
absoluteResourceUrl = `${url}${resourceUrl.path}`;
|
||||
}
|
||||
const resourceContent = await fetchAndHashContent(absoluteResourceUrl, visitedUrls);
|
||||
return `${resourceUrl.path}_${resourceContent}`;
|
||||
}));
|
||||
|
||||
// Combine the content and hashes of linked resources
|
||||
return `${content}_${linkedResource.join('_')}`;
|
||||
}
|
||||
|
||||
const hashCache = new Map();
|
||||
// API endpoint to start the recursive download and hashing
|
||||
app.post('/hash', async (req, res) => {
|
||||
try {
|
||||
let { urls } = req.body;
|
||||
if (!urls) {
|
||||
return res.status(400).json({ error: 'Missing <urls> in the request parameters' });
|
||||
}
|
||||
if (!Array.isArray(urls))
|
||||
urls = [urls];
|
||||
const promises = urls.map(async (url) => {
|
||||
const urlWithoutHashAndQuery = parseUrlWithoutHashAndQuery(url);
|
||||
let hash;
|
||||
// regex to identify owner and repo name from https://owner.github.io/repo-name
|
||||
const githubRepoRegex = /https?:\/\/([\w-]+)\.github\.io\/([\w-]+)/;
|
||||
if (githubRepoRegex.test(urlWithoutHashAndQuery) && urlWithoutHashAndQuery.match(githubRepoRegex)[1] === 'ranchimall') {
|
||||
if (!hashCache.has(urlWithoutHashAndQuery)) {
|
||||
await fetchAndSaveAppHash(urlWithoutHashAndQuery)
|
||||
}
|
||||
hash = hashCache.get(urlWithoutHashAndQuery).hash;
|
||||
} else {
|
||||
const hashedContent = await fetchAndHashContent(urlWithoutHashAndQuery);
|
||||
hash = await hashContent(Buffer.from(hashedContent, 'utf-8'));
|
||||
}
|
||||
return { url, hash };
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
// connect to MongoDB
|
||||
mongoose.connect(`mongodb://${HOST}/price-history`);
|
||||
const db = mongoose.connection;
|
||||
db.on('error', console.error.bind(console, 'connection error:'));
|
||||
db.once('open', () => {
|
||||
console.log('Connected to MongoDB');
|
||||
});
|
||||
async function fetchAndSaveAppHash(url, lastUpdated = Date.now()) {
|
||||
const hashedContent = await fetchAndHashContent(url);
|
||||
const hash = await hashContent(Buffer.from(hashedContent, 'utf-8'));
|
||||
hashCache.set(url, { hash, lastUpdated });
|
||||
}
|
||||
|
||||
app.post('/gitwh', async (req, res) => {
|
||||
try {
|
||||
// ignore if request is not from github
|
||||
if (!req.headers['user-agent'].startsWith('GitHub-Hookshot/'))
|
||||
return;
|
||||
const { repository: { pushed_at, organization, name, has_pages } } = req.body;
|
||||
if (!has_pages)
|
||||
return;
|
||||
const url = `https://${organization}.github.io/${name}`
|
||||
await fetchAndSaveAppHash(url, pushed_at)
|
||||
res.json({ message: 'success' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, './index.min.html'));
|
||||
})
|
||||
const hash = require('./routes/hash')
|
||||
app.use("/hash", hash);
|
||||
const priceHistory = require('./routes/price-history')
|
||||
app.use("/price-history", priceHistory);
|
||||
|
||||
// Start the server
|
||||
app.listen(port, host, () => {
|
||||
console.log(`Server is running at http://${host}:${port}`);
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`Server is running at http://${HOST}:${PORT}`);
|
||||
});
|
||||
// TODO
|
||||
//https://utility-api.ranchimall.net/hash/gitwh
|
||||
|
||||
|
||||
// Export the Express API
|
||||
module.exports = app;
|
||||
|
||||
1
index.min.html
Normal file
1
index.min.html
Normal file
@ -0,0 +1 @@
|
||||
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>RanchiMall Utility APIs</title> <style>body{font-family:sans-serif}h1{font-size:2rem;margin-bottom:2rem}ol li{margin-bottom:5rem}a{color:inherit}table{border-collapse:collapse}table,td,th{border:1px solid #000;padding:.5rem}code{display:inline-block;background-color:#eee;padding:.3rem;border-radius:.2rem;font:monospace;font-size:inherit}@media (prefers-color-scheme:dark){body{background-color:#222;color:#eee}table,td,th{border-color:#eee}code{background-color:#333;color:#eee}}</style> </head> <body> <section style="padding:4vw"> <h1> Welcome to the RanchiMall Utility APIs! </h1> <h2> Endpoints: </h2> <ol> <li> <h3> <a href="/price-history">/price-history</a> </h3> <h4> Query parameters: </h4> <table> <thead> <tr> <th>Parameter</th> <th>Required</th> <th>Default</th> <th>format | values</th> </tr> </thead> <tbody> <tr> <td>from</td> <td>No</td> <td>None</td> <td>YYYY-MM-DD</td> </tr> <tr> <td>to</td> <td>No</td> <td>None</td> <td>YYYY-MM-DD</td> </tr> <tr> <td>on</td> <td>No</td> <td>None</td> <td>YYYY-MM-DD</td> </tr> <tr> <td>limit</td> <td>No</td> <td>100</td> <td>all | <number></td> </tr> <tr> <td>asset</td> <td>No</td> <td>btc</td> <td>btc</td> </tr> <tr> <td>currency</td> <td>No</td> <td>All</td> <td>usd | inr</td> </tr> </tbody> </table> <h4> Example: </h4> <code> /price-history?from=2020-01-01&to=2020-01-31 </code> </li> <li> <h3> <a href="/hash">/hash</a> </h3> <table> <tbody> <tr> <td>Type</td> <td>POST</td> </tr> <tr> <td>Body</td> <td>JSON</td> </tr> <tr> <td>Body parameter</td> <td>urls [Array]</td> </tr> </tbody> </table> <h4> Example: </h4> <code> fetch('https://utility-api.ranchimall.net/hash',{ <br> method: 'POST',<br> headers: {<br> 'Content-Type': 'application/json'<br> },<br> body: JSON.stringify({ urls: [url] })<br> }).then(res => res.json()).then(console.log) <br> <br> Output: <br> [{<br> "url": url,<br> "hash": hash<br> }] </code> </li> </ol> </section>
|
||||
2
index.min.js
vendored
2
index.min.js
vendored
@ -1 +1 @@
|
||||
require("dotenv").config();const express=require("express"),cors=require("cors"),axios=require("axios"),{createHash:createHash}=require("crypto"),rateLimit=require("express-rate-limit"),{parse:parseUrl,URL:URL}=require("url"),{parse:parseHtml}=require("node-html-parser"),allowedDomains=process.env.ALLOWED_DOMAINS.split(","),app=express();app.use(cors());const port=process.env.PORT||3e3,host=process.env.HOST||"0.0.0.0";function addProtocolToUrl(url){return url.startsWith("http://")||url.startsWith("https://")||(url="https://"+url),url}function parseUrlWithoutHashAndQuery(fullUrl){fullUrl=addProtocolToUrl(fullUrl);const parsedUrl=new URL(fullUrl);parsedUrl.hash="",parsedUrl.search="";return parsedUrl.toString()}async function hashContent(content){const hash=createHash("sha256");return hash.update(content),hash.digest("hex")}async function fetchAndHashContent(url,visitedUrls=new Set){if(visitedUrls.has(url))return"";visitedUrls.add(url);const content=(await axios.get(url,{responseType:"arraybuffer"})).data.toString("utf-8"),linkedResources=parseHtml(content).querySelectorAll('link[rel="stylesheet"], script[src]');return`${content}_${(await Promise.all(linkedResources.map((async resource=>{const resourceUrl=parseUrl(resource.getAttribute("href")||resource.getAttribute("src"),!0);let absoluteResourceUrl=resourceUrl.href;resourceUrl.hostname||(resourceUrl.path.startsWith("/")||url.endsWith("/")||(url+="/"),absoluteResourceUrl=`${url}${resourceUrl.path}`);const resourceContent=await fetchAndHashContent(absoluteResourceUrl,visitedUrls);return`${resourceUrl.path}_${resourceContent}`})))).join("_")}`}app.use(express.json()),app.use(rateLimit({windowMs:6e4,max:20})),app.get("/",((req,res)=>{res.send("Hello There!")}));const hashCache=new Map;async function fetchAndSaveAppHash(url,lastUpdated=Date.now()){const hashedContent=await fetchAndHashContent(url),hash=await hashContent(Buffer.from(hashedContent,"utf-8"));hashCache.set(url,{hash:hash,lastUpdated:lastUpdated})}app.post("/hash",(async(req,res)=>{try{let{urls:urls}=req.body;if(!urls)return res.status(400).json({error:"Missing <urls> in the request parameters"});Array.isArray(urls)||(urls=[urls]);const promises=urls.map((async url=>{const urlWithoutHashAndQuery=parseUrlWithoutHashAndQuery(url);let hash;const githubRepoRegex=/https?:\/\/([\w-]+)\.github\.io\/([\w-]+)/;if(githubRepoRegex.test(urlWithoutHashAndQuery)&&"ranchimall"===urlWithoutHashAndQuery.match(githubRepoRegex)[1])hashCache.has(urlWithoutHashAndQuery)||await fetchAndSaveAppHash(urlWithoutHashAndQuery),hash=hashCache.get(urlWithoutHashAndQuery).hash;else{const hashedContent=await fetchAndHashContent(urlWithoutHashAndQuery);hash=await hashContent(Buffer.from(hashedContent,"utf-8"))}return{url:url,hash:hash}})),results=await Promise.all(promises);res.json(results)}catch(error){res.status(500).json({error:error.message})}})),app.post("/gitwh",(async(req,res)=>{try{if(!req.headers["user-agent"].startsWith("GitHub-Hookshot/"))return;const{repository:{pushed_at:pushed_at,organization:organization,name:name,has_pages:has_pages}}=req.body;if(!has_pages)return;const url=`https://${organization}.github.io/${name}`;await fetchAndSaveAppHash(url,pushed_at),res.json({message:"success"})}catch(err){res.status(500).json({error:err.message})}})),app.listen(port,host,(()=>{console.log(`Server is running at http://${host}:${port}`)})),module.exports=app;
|
||||
require("dotenv").config();const express=require("express"),mongoose=require("mongoose"),cors=require("cors"),rateLimit=require("express-rate-limit"),path=require("path"),app=express(),PORT=process.env.PORT||3e3,HOST=process.env.HOST||"127.0.0.1";app.use(express.json()),app.use(cors()),app.use(rateLimit({windowMs:6e4,max:30})),mongoose.connect(`mongodb://${HOST}/price-history`);const db=mongoose.connection;db.on("error",console.error.bind(console,"connection error:")),db.once("open",(()=>{console.log("Connected to MongoDB")})),app.get("/",((req,res)=>{res.sendFile(path.join(__dirname,"./index.min.html"))}));const hash=require("./routes/hash");app.use("/hash",hash);const priceHistory=require("./routes/price-history");app.use("/price-history",priceHistory),app.listen(PORT,HOST,(()=>{console.log(`Server is running at http://${HOST}:${PORT}`)})),module.exports=app;
|
||||
22
models/price-history.js
Normal file
22
models/price-history.js
Normal file
@ -0,0 +1,22 @@
|
||||
const mongoose = require('mongoose');
|
||||
// scheme to store price history
|
||||
const Schema = new mongoose.Schema({
|
||||
date: {
|
||||
type: Date,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
asset: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
usd: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
inr: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
module.exports = mongoose.model('PriceHistory', Schema);
|
||||
1
models/price-history.min.js
vendored
Normal file
1
models/price-history.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
const mongoose=require("mongoose"),Schema=new mongoose.Schema({date:{type:Date,required:!0,unique:!0},asset:{type:String,required:!0},usd:{type:Number,required:!0},inr:{type:Number,required:!0}});module.exports=mongoose.model("PriceHistory",Schema);
|
||||
249
package-lock.json
generated
249
package-lock.json
generated
@ -14,6 +14,8 @@
|
||||
"crypto": "^1.0.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"mongoose": "^8.0.4",
|
||||
"node-cron": "^3.0.3",
|
||||
"node-html-parser": "^6.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -21,6 +23,36 @@
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.3.tgz",
|
||||
"integrity": "sha512-SyCxhJfmK6MoLNV5SbDpNdUy9SDv5H7y9/9rl3KpnwgTHWuNNMc87zWqbcIZXNWY+aUjxLGLEcvHoLagG4tWCg==",
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
|
||||
"integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
|
||||
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@ -137,6 +169,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.2.0.tgz",
|
||||
"integrity": "sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==",
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@ -763,6 +803,14 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kareem": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
|
||||
"integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@ -783,6 +831,11 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
@ -838,6 +891,126 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.2.0.tgz",
|
||||
"integrity": "sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.0",
|
||||
"bson": "^6.2.0",
|
||||
"mongodb-connection-string-url": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.188.0",
|
||||
"@mongodb-js/zstd": "^1.1.0",
|
||||
"gcp-metadata": "^5.2.0",
|
||||
"kerberos": "^2.0.1",
|
||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||
"snappy": "^7.2.2",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
"optional": true
|
||||
},
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-client-encryption": {
|
||||
"optional": true
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
},
|
||||
"socks": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
|
||||
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^8.2.1",
|
||||
"whatwg-url": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.0.4.tgz",
|
||||
"integrity": "sha512-wN9qvdevX3+922VnLT7CpaZRT3jmVCBOK2QMHMGeScQxDRnFMPpkuI9StEPpZo/3x8t+kbzH7F8RMPsyNwyM4w==",
|
||||
"dependencies": {
|
||||
"bson": "^6.2.0",
|
||||
"kareem": "2.5.1",
|
||||
"mongodb": "6.2.0",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "5.0.0",
|
||||
"ms": "2.1.3",
|
||||
"sift": "16.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/mpath": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
|
||||
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mquery": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
|
||||
"integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
|
||||
"dependencies": {
|
||||
"debug": "4.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mquery/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mquery/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@ -851,6 +1024,17 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
|
||||
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
|
||||
"dependencies": {
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-html-parser": {
|
||||
"version": "6.1.11",
|
||||
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.11.tgz",
|
||||
@ -1021,6 +1205,14 @@
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
@ -1182,6 +1374,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sift": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
|
||||
"integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ=="
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
@ -1194,6 +1391,14 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sparse-bitfield": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||
"dependencies": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@ -1246,6 +1451,17 @@
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@ -1264,6 +1480,11 @@
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@ -1280,6 +1501,14 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@ -1288,6 +1517,26 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||
"dependencies": {
|
||||
"tr46": "^3.0.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
"crypto": "^1.0.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"mongoose": "^8.0.4",
|
||||
"node-cron": "^3.0.3",
|
||||
"node-html-parser": "^6.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
118
routes/hash.js
Normal file
118
routes/hash.js
Normal file
@ -0,0 +1,118 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const axios = require('axios');
|
||||
const { createHash } = require('crypto');
|
||||
const { parse: parseUrl, URL } = require('url');
|
||||
const { parse: parseHtml } = require('node-html-parser');
|
||||
function addProtocolToUrl(url) {
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = 'https://' + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function parseUrlWithoutHashAndQuery(fullUrl) {
|
||||
fullUrl = addProtocolToUrl(fullUrl);
|
||||
const parsedUrl = new URL(fullUrl);
|
||||
|
||||
// Set the hash and search/query to empty strings
|
||||
parsedUrl.hash = '';
|
||||
parsedUrl.search = '';
|
||||
|
||||
// Reconstruct the URL without hash and query
|
||||
const urlWithoutHashAndQuery = parsedUrl.toString();
|
||||
|
||||
return urlWithoutHashAndQuery;
|
||||
}
|
||||
// hashContent function to hash the content of a file
|
||||
async function hashContent(content) {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(content);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
// Recursive function to fetch and hash content, including linked resources
|
||||
async function fetchAndHashContent(url, visitedUrls = new Set()) {
|
||||
if (visitedUrls.has(url)) {
|
||||
return ''; // Avoid fetching the same URL multiple times to prevent infinite loops
|
||||
}
|
||||
|
||||
visitedUrls.add(url);
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||
const content = response.data.toString('utf-8');
|
||||
// Parse HTML content to identify linked resources
|
||||
const root = parseHtml(content);
|
||||
const linkedResources = root.querySelectorAll('link[rel="stylesheet"], script[src]');
|
||||
// Fetch and hash linked resources
|
||||
const linkedResource = await Promise.all(linkedResources.map(async (resource) => {
|
||||
const resourceUrl = parseUrl(resource.getAttribute('href') || resource.getAttribute('src'), true);
|
||||
let absoluteResourceUrl = resourceUrl.href;
|
||||
if (!resourceUrl.hostname) {
|
||||
if (!resourceUrl.path.startsWith('/') && !url.endsWith('/'))
|
||||
url += '/';
|
||||
absoluteResourceUrl = `${url}${resourceUrl.path}`;
|
||||
}
|
||||
const resourceContent = await fetchAndHashContent(absoluteResourceUrl, visitedUrls);
|
||||
return `${resourceUrl.path}_${resourceContent}`;
|
||||
}));
|
||||
|
||||
// Combine the content and hashes of linked resources
|
||||
return `${content}_${linkedResource.join('_')}`;
|
||||
}
|
||||
|
||||
const hashCache = new Map();
|
||||
// API endpoint to start the recursive download and hashing
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
let { urls } = req.body;
|
||||
if (!urls) {
|
||||
return res.status(400).json({ error: 'Missing <urls> in the request parameters' });
|
||||
}
|
||||
if (!Array.isArray(urls))
|
||||
urls = [urls];
|
||||
const promises = urls.map(async (url) => {
|
||||
const urlWithoutHashAndQuery = parseUrlWithoutHashAndQuery(url);
|
||||
let hash;
|
||||
// regex to identify owner and repo name from https://owner.github.io/repo-name
|
||||
const githubRepoRegex = /https?:\/\/([\w-]+)\.github\.io\/([\w-]+)/;
|
||||
if (githubRepoRegex.test(urlWithoutHashAndQuery) && urlWithoutHashAndQuery.match(githubRepoRegex)[1] === 'ranchimall') {
|
||||
if (!hashCache.has(urlWithoutHashAndQuery)) {
|
||||
await fetchAndSaveAppHash(urlWithoutHashAndQuery)
|
||||
}
|
||||
hash = hashCache.get(urlWithoutHashAndQuery).hash;
|
||||
} else {
|
||||
const hashedContent = await fetchAndHashContent(urlWithoutHashAndQuery);
|
||||
hash = await hashContent(Buffer.from(hashedContent, 'utf-8'));
|
||||
}
|
||||
return { url, hash };
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
async function fetchAndSaveAppHash(url, lastUpdated = Date.now()) {
|
||||
const hashedContent = await fetchAndHashContent(url);
|
||||
const hash = await hashContent(Buffer.from(hashedContent, 'utf-8'));
|
||||
hashCache.set(url, { hash, lastUpdated });
|
||||
}
|
||||
|
||||
router.post('/gitwh', async (req, res) => {
|
||||
try {
|
||||
// ignore if request is not from github
|
||||
if (!req.headers['user-agent'].startsWith('GitHub-Hookshot/'))
|
||||
return res.json({ message: 'ignored' });
|
||||
const { repository: { pushed_at, organization, name, has_pages } } = req.body;
|
||||
if (!has_pages)
|
||||
return res.json({ message: 'ignored' });
|
||||
const url = `https://${organization}.github.io/${name}`
|
||||
await fetchAndSaveAppHash(url, pushed_at)
|
||||
res.json({ message: 'success' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router;
|
||||
1
routes/hash.min.js
vendored
Normal file
1
routes/hash.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
const express=require("express"),router=express.Router(),axios=require("axios"),{createHash:createHash}=require("crypto"),{parse:parseUrl,URL:URL}=require("url"),{parse:parseHtml}=require("node-html-parser");function addProtocolToUrl(url){return url.startsWith("http://")||url.startsWith("https://")||(url="https://"+url),url}function parseUrlWithoutHashAndQuery(fullUrl){fullUrl=addProtocolToUrl(fullUrl);const parsedUrl=new URL(fullUrl);parsedUrl.hash="",parsedUrl.search="";return parsedUrl.toString()}async function hashContent(content){const hash=createHash("sha256");return hash.update(content),hash.digest("hex")}async function fetchAndHashContent(url,visitedUrls=new Set){if(visitedUrls.has(url))return"";visitedUrls.add(url);const content=(await axios.get(url,{responseType:"arraybuffer"})).data.toString("utf-8"),linkedResources=parseHtml(content).querySelectorAll('link[rel="stylesheet"], script[src]');return`${content}_${(await Promise.all(linkedResources.map((async resource=>{const resourceUrl=parseUrl(resource.getAttribute("href")||resource.getAttribute("src"),!0);let absoluteResourceUrl=resourceUrl.href;resourceUrl.hostname||(resourceUrl.path.startsWith("/")||url.endsWith("/")||(url+="/"),absoluteResourceUrl=`${url}${resourceUrl.path}`);const resourceContent=await fetchAndHashContent(absoluteResourceUrl,visitedUrls);return`${resourceUrl.path}_${resourceContent}`})))).join("_")}`}const hashCache=new Map;async function fetchAndSaveAppHash(url,lastUpdated=Date.now()){const hashedContent=await fetchAndHashContent(url),hash=await hashContent(Buffer.from(hashedContent,"utf-8"));hashCache.set(url,{hash:hash,lastUpdated:lastUpdated})}router.post("/",(async(req,res)=>{try{let{urls:urls}=req.body;if(!urls)return res.status(400).json({error:"Missing <urls> in the request parameters"});Array.isArray(urls)||(urls=[urls]);const promises=urls.map((async url=>{const urlWithoutHashAndQuery=parseUrlWithoutHashAndQuery(url);let hash;const githubRepoRegex=/https?:\/\/([\w-]+)\.github\.io\/([\w-]+)/;if(githubRepoRegex.test(urlWithoutHashAndQuery)&&"ranchimall"===urlWithoutHashAndQuery.match(githubRepoRegex)[1])hashCache.has(urlWithoutHashAndQuery)||await fetchAndSaveAppHash(urlWithoutHashAndQuery),hash=hashCache.get(urlWithoutHashAndQuery).hash;else{const hashedContent=await fetchAndHashContent(urlWithoutHashAndQuery);hash=await hashContent(Buffer.from(hashedContent,"utf-8"))}return{url:url,hash:hash}})),results=await Promise.all(promises);res.json(results)}catch(error){res.status(500).json({error:error.message})}})),router.post("/gitwh",(async(req,res)=>{try{if(!req.headers["user-agent"].startsWith("GitHub-Hookshot/"))return res.json({message:"ignored"});const{repository:{pushed_at:pushed_at,organization:organization,name:name,has_pages:has_pages}}=req.body;if(!has_pages)return res.json({message:"ignored"});const url=`https://${organization}.github.io/${name}`;await fetchAndSaveAppHash(url,pushed_at),res.json({message:"success"})}catch(err){res.status(500).json({error:err.message})}})),module.exports=router;
|
||||
122
routes/price-history.js
Normal file
122
routes/price-history.js
Normal file
@ -0,0 +1,122 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const cron = require('node-cron');
|
||||
|
||||
const PriceHistory = require('../models/price-history');
|
||||
|
||||
function loadHistoricToDb() {
|
||||
const now = parseInt(Date.now() / 1000);
|
||||
Promise.all([
|
||||
fetch(`https://query1.finance.yahoo.com/v7/finance/download/BTC-USD?period1=1410912000&period2=${now}&interval=1d&events=history&includeAdjustedClose=true`).then((res) => res.text()),
|
||||
fetch(`https://query1.finance.yahoo.com/v7/finance/download/BTC-INR?period1=1410912000&period2=${now}&interval=1d&events=history&includeAdjustedClose=true`).then((res) => res.text()),
|
||||
])
|
||||
.then(async ([usd, inr]) => {
|
||||
const usdData = usd.split("\n").slice(1);
|
||||
const inrData = inr.split("\n").slice(1);
|
||||
const priceHistoryData = [];
|
||||
for (let i = 0; i < usdData.length; i++) {
|
||||
const [date, open, high, low, close, adjClose, volume] = usdData[i].split(",");
|
||||
const [date2, open2, high2, low2, close2, adjClose2, volume2] = inrData[i].split(",");
|
||||
priceHistoryData.push({
|
||||
date: new Date(date).getTime(),
|
||||
asset: "btc",
|
||||
usd: parseFloat(parseFloat(close).toFixed(2)),
|
||||
inr: parseFloat(parseFloat(close2).toFixed(2)),
|
||||
});
|
||||
}
|
||||
// update many
|
||||
await PriceHistory.deleteMany({ asset: 'btc' });
|
||||
await PriceHistory.insertMany(priceHistoryData);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
})
|
||||
}
|
||||
loadHistoricToDb();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
console.log('price-history');
|
||||
try {
|
||||
const { from, to, on, limit = 100, asset = 'btc', currency } = req.query;
|
||||
const searchParams = {
|
||||
asset
|
||||
}
|
||||
if (from) {
|
||||
searchParams.date = { $gte: new Date(from).getTime() };
|
||||
}
|
||||
if (to) {
|
||||
searchParams.date = { ...searchParams.date, $lte: new Date(to).getTime() };
|
||||
}
|
||||
if (on) {
|
||||
searchParams.date = { $eq: new Date(on).getTime() };
|
||||
}
|
||||
if (currency) {
|
||||
searchParams[currency] = { $exists: true };
|
||||
}
|
||||
const dataFormat = { _id: 0, __v: 0, asset: 0 };
|
||||
if (currency === 'inr') {
|
||||
dataFormat.usd = 0;
|
||||
}
|
||||
if (currency === 'usd') {
|
||||
dataFormat.inr = 0;
|
||||
}
|
||||
const priceHistory = await PriceHistory.find(searchParams, dataFormat)
|
||||
.sort({ date: -1 })
|
||||
.limit(limit === 'all' ? 0 : parseInt(limit));
|
||||
res.json(priceHistory);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({ error: err });
|
||||
}
|
||||
})
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
const { dates } = req.body;
|
||||
if (!dates) {
|
||||
return res.status(400).json({ error: 'dates is required' });
|
||||
}
|
||||
if (!Array.isArray(dates)) {
|
||||
return res.status(400).json({ error: 'dates must be an array' });
|
||||
}
|
||||
const priceHistory = await PriceHistory.find({ date: { $in: dates } }, { _id: 0, __v: 0, asset: 0 });
|
||||
res.json(priceHistory);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({ error: err });
|
||||
}
|
||||
})
|
||||
|
||||
cron.schedule('0 */4 * * *', async () => {
|
||||
try {
|
||||
// will return a csv file
|
||||
const [usd, inr] = await Promise.all([
|
||||
fetch("https://query1.finance.yahoo.com/v7/finance/download/BTC-USD").
|
||||
then((res) => res.text()),
|
||||
fetch("https://query1.finance.yahoo.com/v7/finance/download/BTC-INR").
|
||||
then((res) => res.text())
|
||||
]);
|
||||
|
||||
const usdData = usd.split("\n").slice(1);
|
||||
const inrData = inr.split("\n").slice(1);
|
||||
for (let i = 0; i < usdData.length; i++) {
|
||||
const [date, open, high, low, close, adjClose, volume] = usdData[i].split(",");
|
||||
const [date2, open2, high2, low2, close2, adjClose2, volume2] = inrData[i].split(",");
|
||||
const priceHistoryData = {
|
||||
date: new Date(date).getTime(),
|
||||
asset: "btc",
|
||||
usd: parseFloat(parseFloat(close).toFixed(2)),
|
||||
inr: parseFloat(parseFloat(close2).toFixed(2)),
|
||||
};
|
||||
await PriceHistory.findOneAndUpdate(
|
||||
{ date: priceHistoryData.date, asset: priceHistoryData.asset },
|
||||
priceHistoryData,
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router;
|
||||
1
routes/price-history.min.js
vendored
Normal file
1
routes/price-history.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
const express=require("express"),router=express.Router(),cron=require("node-cron"),PriceHistory=require("../models/price-history");function loadHistoricToDb(){const now=parseInt(Date.now()/1e3);Promise.all([fetch(`https://query1.finance.yahoo.com/v7/finance/download/BTC-USD?period1=1410912000&period2=${now}&interval=1d&events=history&includeAdjustedClose=true`).then((res=>res.text())),fetch(`https://query1.finance.yahoo.com/v7/finance/download/BTC-INR?period1=1410912000&period2=${now}&interval=1d&events=history&includeAdjustedClose=true`).then((res=>res.text()))]).then((async([usd,inr])=>{const usdData=usd.split("\n").slice(1),inrData=inr.split("\n").slice(1),priceHistoryData=[];for(let i=0;i<usdData.length;i++){const[date,open,high,low,close,adjClose,volume]=usdData[i].split(","),[date2,open2,high2,low2,close2,adjClose2,volume2]=inrData[i].split(",");priceHistoryData.push({date:new Date(date).getTime(),asset:"btc",usd:parseFloat(parseFloat(close).toFixed(2)),inr:parseFloat(parseFloat(close2).toFixed(2))})}await PriceHistory.deleteMany({asset:"btc"}),await PriceHistory.insertMany(priceHistoryData)})).catch((err=>{console.log(err)}))}loadHistoricToDb(),router.get("/",(async(req,res)=>{console.log("price-history");try{const{from:from,to:to,on:on,limit:limit=100,asset:asset="btc",currency:currency}=req.query,searchParams={asset:asset};from&&(searchParams.date={$gte:new Date(from).getTime()}),to&&(searchParams.date={...searchParams.date,$lte:new Date(to).getTime()}),on&&(searchParams.date={$eq:new Date(on).getTime()}),currency&&(searchParams[currency]={$exists:!0});const dataFormat={_id:0,__v:0,asset:0};"inr"===currency&&(dataFormat.usd=0),"usd"===currency&&(dataFormat.inr=0);const priceHistory=await PriceHistory.find(searchParams,dataFormat).sort({date:-1}).limit("all"===limit?0:parseInt(limit));res.json(priceHistory)}catch(err){console.log(err),res.status(500).json({error:err})}})),router.post("/",(async(req,res)=>{try{const{dates:dates}=req.body;if(!dates)return res.status(400).json({error:"dates is required"});if(!Array.isArray(dates))return res.status(400).json({error:"dates must be an array"});const priceHistory=await PriceHistory.find({date:{$in:dates}},{_id:0,__v:0,asset:0});res.json(priceHistory)}catch(err){console.log(err),res.status(500).json({error:err})}})),cron.schedule("0 */4 * * *",(async()=>{try{const[usd,inr]=await Promise.all([fetch("https://query1.finance.yahoo.com/v7/finance/download/BTC-USD").then((res=>res.text())),fetch("https://query1.finance.yahoo.com/v7/finance/download/BTC-INR").then((res=>res.text()))]),usdData=usd.split("\n").slice(1),inrData=inr.split("\n").slice(1);for(let i=0;i<usdData.length;i++){const[date,open,high,low,close,adjClose,volume]=usdData[i].split(","),[date2,open2,high2,low2,close2,adjClose2,volume2]=inrData[i].split(","),priceHistoryData={date:new Date(date).getTime(),asset:"btc",usd:parseFloat(parseFloat(close).toFixed(2)),inr:parseFloat(parseFloat(close2).toFixed(2))};await PriceHistory.findOneAndUpdate({date:priceHistoryData.date,asset:priceHistoryData.asset},priceHistoryData,{upsert:!0})}}catch(err){console.log(err)}})),module.exports=router;
|
||||
Loading…
Reference in New Issue
Block a user