First commit

This commit is contained in:
2024-02-21 18:47:21 +01:00
commit 0b025587bd
13 changed files with 2291 additions and 0 deletions

50
.eslintrc.json Normal file
View File

@@ -0,0 +1,50 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
"arrow-spacing": ["warn", { "before": true, "after": true }],
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
"comma-dangle": ["error", "always-multiline"],
"comma-spacing": "error",
"comma-style": "error",
"curly": ["error", "multi-line", "consistent"],
"dot-location": ["error", "property"],
"handle-callback-err": "off",
"indent": ["error", "tab"],
"keyword-spacing": "error",
"max-nested-callbacks": ["error", { "max": 4 }],
"max-statements-per-line": ["error", { "max": 2 }],
"no-console": "off",
"no-empty-function": "error",
"no-floating-decimal": "error",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
"no-trailing-spaces": ["error"],
"no-var": "error",
"object-curly-spacing": ["error", "always"],
"prefer-const": "error",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"space-before-blocks": "error",
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"yoda": "error"
}
}

177
.gitignore vendored Normal file
View File

@@ -0,0 +1,177 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
config.json

48
Modules/cronManager.js Normal file
View File

@@ -0,0 +1,48 @@
import cron from 'node-cron';
const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
const hoursOfDay = [...Array(24).keys()].map(hour => hour.toString());
const minutesOfHour = [...Array(60).keys()].map(minute => minute.toString());
function toCronExpression(str) {
switch (str.toLowerCase()) {
case 'everyfiveseconds': {
return '*/5 * * * * *';
}
case 'everythirtyseconds': {
return '*/30 * * * * *';
}
case 'everyminute': {
return '* * * * *';
}
case 'everyfiveminutes': {
return '*/5 * * * *';
}
case 'everyfifteenminutes': {
return '*/15 * * * *';
}
case 'everythirtyminutes': {
return '*/30 * * * *';
}
case 'everyday': {
return '0 0 * * *';
}
case 'weekdays': {
return '0 0 * * 1-5';
}
case 'weekends': {
return '0 0 * * 6,7';
}
default: {
const [day, hour, minute] = str.toLowerCase().split(' ');
const dayIndex = daysOfWeek.indexOf(day);
const hourIndex = hoursOfDay.indexOf(hour);
const minuteIndex = minutesOfHour.indexOf(minute);
return `${minuteIndex} ${hourIndex} * * ${dayIndex}`;
}
}
}
export function createCron(rate, exec) {
cron.schedule(toCronExpression(rate), exec);
}

71
Modules/fetchManager.js Normal file
View File

@@ -0,0 +1,71 @@
import fetch from 'node-fetch';
import { error } from './logManager';
async function get(url, token) {
const options = {
method: 'GET',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
.catch(err => error(err));
}
async function post(url, body, token) {
const options = {
method: 'POST',
mode: 'cors',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
body: JSON.stringify(body),
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
.catch(err => error(err));
}
async function patch(url, body, token) {
const options = {
method: 'PATCH',
mode: 'cors',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
body: JSON.stringify(body),
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
.catch(err => error(err));
}
async function put(url, body, token) {
const options = {
method: 'PUT',
mode: 'cors',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
body: JSON.stringify(body),
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
.catch(err => error(err));
}
export {
get,
post,
patch,
put,
};

86
Modules/htmlManager.js Normal file
View File

@@ -0,0 +1,86 @@
import fs from 'fs';
import { log } from './logManager';
function stylizeHTML(html) {
return `<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f2f2f2;
}
div {
background-color: #fff;
padding: 20px;
border: 1px solid #000;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
h2 {
color: #333;
font-size: 20px;
}
p {
color: #666;
font-size: 14px;
}
img {
max-width: 100%;
height: auto;
}
a {
color: #007bff;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #333;
color: #fff;
}
div {
background-color: #444;
border-color: #000;
}
h2 {
color: #fff;
}
p {
color: #ccc;
}
a {
color: #007bff;
}
}
</style>
</head>
<body>
${html}
</body>
</html>`;
}
function generatePostHtml(posts) {
return posts.map(post => `
<div>
<h2>${post.title}</h2>
<p>Date: ${post.date}</p>
<p>Flair: ${post.flair}</p>
<p>Linked: <a href="${post.linked}">${post.linked}</a></p>
<p>${post.description}</p>
${post.tldr ? `<p>TLDR: ${post.tldr}</p>` : ''}
${post.image ? `<img src="${post.image}" /><br><br>` : ''}
<a href="${post.url}">Post Link</a>
</div>
`).join('');
}
function saveHtmlToFile(html) {
fs.writeFileSync('index.html', html);
log('HTML file saved.');
}
export function generateHtml(posts) {
const html = stylizeHTML(generatePostHtml(posts));
saveHtmlToFile(html);
}

19
Modules/logManager.js Normal file
View File

@@ -0,0 +1,19 @@
import pino from 'pino';
const logger = pino();
export function log(x) {
logger.info(x);
}
export function debug(x) {
logger.debug(x);
}
export function warn(x) {
logger.warn(x);
}
export function error(x) {
logger.error(x);
}

24
README.md Normal file
View File

@@ -0,0 +1,24 @@
# Chromanews
## How to use
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.js
```
## What is Chromanews
Chromanews allows you to fetch all the top posts of news subs you follow from reddit and generate an HTML offline page for you to read. The file can also be pushed to a webhook such as discord.
You can edit the followed subs and the discord webhook in
```bash
./config.json
```

BIN
bun.lockb Executable file

Binary file not shown.

12
config.json.sample Normal file
View File

@@ -0,0 +1,12 @@
{
"subs": [
"Bitcoin",
"ethereum",
"CryptoCurrency",
"Crypto_Currency_News"
],
"discord": {
"avatar_url": "https://aostia.me/portfolio/assets/avatar.png",
"webhook": "https://discord.com/api/webhooks/id/token"
}
}

1713
index.html Normal file

File diff suppressed because it is too large Load Diff

42
index.js Normal file
View File

@@ -0,0 +1,42 @@
import { exec } from 'child_process';
const { log } = require('./Modules/logManager');
const { get } = require('./Modules/fetchManager');
const { createCron } = require('./Modules/cronManager');
const { generateHtml } = require('./Modules/htmlManager');
const { subs, discord } = require('./config.json');
async function main() {
const posts = [];
for (const sub of subs) {
log(`Fetching data from ${sub}...`);
const res = await get(`https://www.reddit.com/r/${sub}/top/.json?t=week`);
log(`Fetched ${res.data.children.length} posts.`);
for (const post of res.data.children) {
let tldr = null;
if (sub == 'CryptoCurrency' && post.data.link_flair_text == 'GENERAL-NEWS') {
const comments = await get(`https://www.reddit.com${post.data.permalink}.json`);
for (const comment of comments[1].data.children) {
if (comment.data.author == 'coinfeeds-bot') tldr = comment.data.body;
}
}
posts.push({
title: post.data.title,
url: `https://www.reddit.com${post.data.permalink}`,
linked: post.data.url,
description: `${post.data.selftext ? post.data.selftext : 'No text'}`,
image: post.data.thumbnail != 'self' ? post.data.thumbnail : null,
flair: post.data.link_flair_text ? post.data.link_flair_text : 'No flair',
date: new Date(post.data.created_utc * 1000),
tldr: tldr,
});
}
}
log('Generating HTML...');
generateHtml(posts);
log('Sending webhook...');
discord.webhook ? exec(`curl -X POST -F 'payload_json={"username":"ChromaNews","avatar_url":"${discord.avatar_url}","content":"New weekly news!"}' -F 'file1=@index.html' ${discord.webhook}`) : log('No webhook found.');
}
main();
createCron('monday 0 0', main);

27
jsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "chromanews",
"module": "index.js",
"type": "module",
"scripts": {
"start": "bun run ./index.js",
"dev": "bun run ./index.js --debug"
},
"devDependencies": {
"@types/bun": "latest",
"eslint": "^8.56.0",
"prettier": "^3.2.5"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"form-data": "^4.0.0",
"node-cron": "^3.0.3",
"pino": "^8.19.0"
}
}