Compare commits

...

8 Commits

Author SHA1 Message Date
Robert Webb
0c05f3d357 Fixed some more linting
Removed eslint errors related to the after variable being undefined.
2019-02-17 19:58:20 -08:00
notnull
98adc24389 separated api path from main path 2019-02-15 17:59:55 -05:00
24ae647142 adding a comment backend, experimenting with static html frontend (plus bash) 2019-02-14 08:59:14 -08:00
df2afd36e2 F 2019-02-13 14:28:27 -08:00
26524549c1 Merge branch 'master' of ssh://irc.anarchyplanet.org:2222/notnull/hacker-news-cli 2019-02-13 08:13:08 -08:00
8db4e1ef54 added votes 2019-02-13 08:08:41 -08:00
notnull
284be0ecdd added models for comment and user; added .eslintrc and .prettierrc 2019-02-13 11:07:11 -05:00
10091f28ff Merge branch 'master' of totally_not_fb/hacker-news-cli into master 2019-02-12 01:12:54 -08:00
26 changed files with 506 additions and 168 deletions

24
.eslintrc Normal file
View File

@ -0,0 +1,24 @@
{
"extends": ["eslint:recommended"],
"parserOptions": {
"ecmaVersion": 8
},
"globals": {
"after": "readable"
},
"env": {
"es6": true,
"node": true
},
"rules": {
"quotes": ["warn", "single"],
"semi": ["error", "never"],
"indent": ["warn", 2],
"no-unused-vars": ["warn"],
"no-console": ["off"],
}
}

View File

@ -1,43 +1,37 @@
const router = require('express').Router(); const router = require('express').Router()
const { Article } = require('../db/models'); const { Article } = require('../db/models')
const buildPage = require('./buildPage'); const buildPage = require('../scripts/buildPage')
const buildTable = require('../scripts/buildArticleTable')
module.exports = router; module.exports = router
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {
const articles = await Article.findAll(); const articles = await Article.findAll()
const tbl = articles res.status(201).send(articles)
.map(
article => `<tr><td>${article.title}</td><td>${article.link}</td></tr>`
)
.join();
const page = buildPage(tbl);
console.log(page);
res.status(201).send(page);
} catch (err) { } catch (err) {
next(err); next(err)
} }
}); })
router.get('/:id', async (req, res, next) => { router.get('/:id', async (req, res, next) => {
try { try {
const article = await Article.findById(req.params.id); const article = await Article.findById(req.params.id)
console.log(article.title); console.log(article.title)
console.log(`by: ${article.author}`); console.log(`by: ${article.author}`)
console.log(article.text); console.log(article.text)
res.status(201).send(article); res.status(201).send(article)
} catch (err) { } catch (err) {
next(err); next(err)
} }
}); })
router.post('/', async (req, res, next) => { router.post('/', async (req, res, next) => {
const body = req.body; const body = req.body
try { try {
const article = await Article.create(body); const article = await Article.create(body)
res.redirect(article.id); res.redirect(article.id)
} catch (err) { } catch (err) {
next(err); next(err)
} }
}); })

View File

@ -1,22 +0,0 @@
module.exports = listString =>
`
<!DOCTYPE html>
<html>
<body>
<h2>Haxor Newz</h2>
<h3>"uber l337"</h3>
<table>
<thead>
<th>Title</th>
<th>Link</th>
</thead>
${listString}
<tfoot>
ATTACK!
</tfoot>
</table>
<footer>&#9398; anarchy planet</footer>
</body>
</html>
`;

25
api/comments.js Normal file
View File

@ -0,0 +1,25 @@
const router = require('express').Router()
const { Comment } = require('../db/models')
const buildPage = require('../scripts/buildPage')
module.exports = router
router.get('/', async (req, res, next) => {
try {
const comments = await Comment.findAll({
attributes: ['id', 'title', 'content', 'userId', 'parentId']
})
} catch (err) {
next(err)
}
})
router.post('/', async (req, res, next) => {
const newComment = req.body // good sanitization
try {
const comment = await Comment.create(newComment)
res.redirect('http://localhost:1337')
} catch (err) {
next(err)
}
})

View File

@ -1,20 +1,21 @@
const router = require('express').Router(); const router = require('express').Router()
module.exports = router; module.exports = router
router.use('/items', require('./items')); router.use('/items', require('./items'))
router.use('/articles', require('./articles')); router.use('/articles', require('./articles'))
router.use('/comments', require('./comments'))
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {
res.send('/n-------/nHello from Express!/n--------/n'); res.send('/n-------/nHello from Express!/n--------/n')
} catch (err) { } catch (err) {
next(err); next(err)
} }
}); })
router.use((req, res, next) => { router.use((req, res, next) => {
const error = new Error('Not Found!!!!!!!'); const error = new Error('Not Found!!!!!!!')
error.status = 404; error.status = 404
next(error); next(error)
}); })

View File

@ -1,24 +1,24 @@
const router = require('express').Router(); const router = require('express').Router()
const { Item } = require('../db/models'); const { Item } = require('../db/models')
module.exports = router; module.exports = router
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {
const items = await Item.findAll(); const items = await Item.findAll()
res.status(201).send(items); res.status(201).send(items)
} catch (err) { } catch (err) {
next(err); next(err)
} }
}); })
router.post('/', async (req, res, next) => { router.post('/', async (req, res, next) => {
try { try {
const item = await Item.create(req.body); const item = await Item.create(req.body)
res.status(201).json(item); res.status(201).json(item)
} catch (err) { } catch (err) {
next(err); next(err)
} }
}); })

View File

@ -1,13 +1,13 @@
const router = require('express').Router(); const router = require('express').Router()
module.exports = router; module.exports = router
// what you will hit at /api/robots // what you will hit at /api/robots
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {
console.log('NSA was here'); console.log('NSA was here')
res.status(201).send('FUCK YOU NSA\n'); res.status(201).send('FUCK YOU NSA\n')
} catch (err) { } catch (err) {
next(err); next(err)
} }
}); })

13
api/users.js Normal file
View File

@ -0,0 +1,13 @@
const router = require('express').Router()
const { User } = require('../db/models')
module.exports = router
router.get('/', async (req, res, next) => {
try {
const users = await User.findAll({ include: User })
res.status(201).json(users)
} catch (err) {
next(err)
}
})

View File

@ -21,6 +21,6 @@ const ascii = String.raw`
____^/\___^--____/\____O______________/\/\---/\___________-- ____^/\___^--____/\____O______________/\/\---/\___________--
/\^ ^ ^ ^ ^^ ^ '\ ^ ^ /\^ ^ ^ ^ ^^ ^ '\ ^ ^
-- - -- - - --- __ ^ -- - -- - - --- __ ^
-- __ ___-- ^ ^`; -- __ ___-- ^ ^`
module.exports = ascii; module.exports = ascii

View File

@ -1,26 +1,24 @@
const Sequelize = require('sequelize'); const Sequelize = require('sequelize')
const pkg = require('../package.json'); const pkg = require('../package.json')
const databaseName = const databaseName = pkg.name + (process.env.NODE_ENV === 'test' ? '-test' : '')
pkg.name + (process.env.NODE_ENV === 'test' ? '-test' : '');
const createDB = () => { const createDB = () => {
const db = new Sequelize( const db = new Sequelize(
process.env.DATABASE_URL || `postgres://localhost:5432/${databaseName}`, process.env.DATABASE_URL || `postgres://localhost:5432/${databaseName}`,
{ {
logging: false, operatorsAliases: false,
operatorsAliases: false }
)
return db
} }
);
return db;
};
const db = createDB(); const db = createDB()
module.exports = db; module.exports = db
// This is a global Mocha hook used for resource cleanup. // This is a global Mocha hook used for resource cleanup.
// Otherwise, Mocha v4+ does not exit after tests. // Otherwise, Mocha v4+ does not exit after tests.
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
after('close database connection', () => db.close()); after('close database connection', () => db.close())
} }

View File

@ -1,6 +1,20 @@
const db = require('./db'); const db = require('./db')
// the questions are:
// 1. how to store votes in the database? as a count on the article? this would be less accurate but ok for the beginning <- I am not a fan of this approach
// 2. what votes do we count? (anon, per ip address, cookies?) to build relations between votes we need a concept of users based identification <- yep!
// 3. also downvotes? <- so I think to start we shoudl copy what HN does agreed :)
// TODO figure out how HN implements votes (I think non-registered voters can't vote for example)
// re: users: I can implement some hacky user auth to start with... or maybe that's something we delegate to rw (totally_not_fb)
// hn voting
// regiistration required
// min karma required for downvoting (comments only?)
// so we could start writing tickets at this point ... ? makes sense
// view content: https://irc.anarchyplanet.org/git/notnull/hacker-news-cli/issues/4
// post content: https://irc.anarchyplanet.org/git/notnull/hacker-news-cli/issues/3
// votes: https://irc.anarchyplanet.org/git/notnull/hacker-news-cli/issues/5
// register models // register models
require('./models'); require('./models')
module.exports = db; module.exports = db

View File

@ -1,5 +1,5 @@
const Sequelize = require('sequelize'); const Sequelize = require('sequelize')
const db = require('../db'); const db = require('../db')
const Article = db.define('articles', { const Article = db.define('articles', {
title: { title: {
@ -9,6 +9,6 @@ const Article = db.define('articles', {
link: { link: {
type: Sequelize.STRING type: Sequelize.STRING
} }
}); })
module.exports = Article; module.exports = Article

15
db/models/comment.js Normal file
View File

@ -0,0 +1,15 @@
const Sequelize = require('sequelize')
const db = require('../db')
const Comment = db.define('comment', {
title: {
type: Sequelize.STRING
},
content: {
type: Sequelize.TEXT,
allowNull: false
}
})
module.exports = Comment

View File

@ -1,4 +1,19 @@
const Item = require('./item'); const Item = require('./item')
const Article = require('./article'); const Article = require('./article')
const Comment = require('./comment')
const User = require('./user')
module.exports = { Item, Article }; Article.hasMany(Comment) // allows for addComment
Comment.belongsTo(Article)
User.hasMany(Article)
Article.belongsTo(User) // allows for setUser
User.hasMany(Comment)
Comment.belongsTo(User)
// i understand more now: parent must be set instead of reply.
Comment.belongsTo(Comment, { as: 'parent' }) // setParent
module.exports = { Item, Article, Comment, User }

View File

@ -1,11 +1,11 @@
const Sequelize = require('sequelize'); const Sequelize = require('sequelize')
const db = require('../db'); const db = require('../db')
const Item = db.define('items', { const Item = db.define('items', {
name: { name: {
type: Sequelize.STRING, type: Sequelize.STRING,
allowNull: false allowNull: false
} }
}); })
module.exports = Item; module.exports = Item

84
db/models/user.js Normal file
View File

@ -0,0 +1,84 @@
/*
note: this currently isn't exported because I will
have to investigate the salting etc.
*/
const crypto = require('crypto')
const Sequelize = require('sequelize')
const db = require('../db')
const User = db.define('user', {
email: {
type: Sequelize.STRING,
unique: true,
},
firstName: {
type: Sequelize.STRING,
},
lastName: {
type: Sequelize.STRING,
},
username: {
type: Sequelize.STRING,
unique: true,
allowNull: false,
},
imageUrl: {
type: Sequelize.STRING,
defaultValue: 'novatore.jpg',
},
password: {
type: Sequelize.STRING,
// Making `.password` act like a func hides it when serializing to JSON.
// This is a hack to get around Sequelize's lack of a "private" option.
get() {
return () => this.getDataValue('password')
},
},
salt: {
type: Sequelize.STRING,
// Making `.salt` act like a function hides it when serializing to JSON.
// This is a hack to get around Sequelize's lack of a "private" option.
get() {
return () => this.getDataValue('salt')
},
},
})
module.exports = User
/**
* instanceMethods
*/
User.prototype.correctPassword = function(candidatePwd) {
return User.encryptPassword(candidatePwd, this.salt()) === this.password()
}
/**
* classMethods
*/
User.generateSalt = function() {
return crypto.randomBytes(16).toString('base64')
}
User.encryptPassword = function(plainText, salt) {
return crypto
.createHash('RSA-SHA256')
.update(plainText)
.update(salt)
.digest('hex')
}
/**
* hooks
*/
const setSaltAndPassword = user => {
if (user.changed('password')) {
user.salt = User.generateSalt()
user.password = User.encryptPassword(user.password(), user.salt())
}
}
User.beforeCreate(setSaltAndPassword)
User.beforeUpdate(setSaltAndPassword)

13
db/models/votes.js Normal file
View File

@ -0,0 +1,13 @@
const Sequelize = require('sequelize')
const db = require('../db')
// votes should be up or down, could either do 'up/'down' or 1/-1
const Vote = db.define('votes', {
valence: {
type: Sequelize.ENUM,
allowNull: false
}
})
module.exports = Vote

View File

@ -1,27 +1,63 @@
const db = require('../db'); const db = require('../db')
const { Article } = require('./models'); const { Article, Comment, User } = require('./models')
const testArticle = { const testArticle = {
title: 'read desert', title: 'read desert',
link: 'https://readdesert.org' link: 'https://readdesert.org',
}; }
const testArticle2 = {
title: 'the best place ever',
link: 'https://irc.anarchyplanet.org',
}
const testComment = {
title: 'best essay ever',
content: 'read the sand book already!',
}
const testReply = {
title: 'u r so dumb',
content: 'i hate anews :P',
}
const testReply2 = {
title: 'best essay ever',
content: 'read the sand book already!',
}
const testUser = {
username: 'nn',
}
console.log(Article);
async function runSeed() { async function runSeed() {
await db.sync({ force: true }); await db.sync({ force: true })
console.log('db synced!'); console.log('db synced!')
console.log('seeding...'); console.log('seeding...')
try { try {
await Article.create(testArticle); const article = await Article.create(testArticle)
console.log('seeded successfully');
const user = await User.create(testUser)
const c1 = await Comment.create(testComment)
const c2 = await Comment.create(testReply)
const c3 = await Comment.create(testReply2)
await article.setUser(user)
await c1.setUser(user)
await c2.setUser(user)
await article.addComment(c1)
await c2.setParent(c1)
await c3.setParent(c2)
// await c2.setParent(c1)
console.log('seeded successfully')
} catch (err) { } catch (err) {
console.error(err); console.error(err)
process.exitCode = 1; process.exitCode = 1
} finally { } finally {
console.log('closing db connection'); console.log('closing db connection')
await db.close(); await db.close()
console.log('db connection closed'); console.log('db connection closed')
} }
} }
runSeed(); runSeed()

View File

@ -1,47 +1,45 @@
const fetch = require('node-fetch'); const fetch = require('node-fetch')
// implemented from: https://github.com/HackerNews/API // implemented from: https://github.com/HackerNews/API
const HN_PREFIX = 'https://hacker-news.firebaseio.com/v0/'; const HN_PREFIX = 'https://hacker-news.firebaseio.com/v0/'
const TOP_STORIES = 'topstories'; const TOP_STORIES = 'topstories'
const ITEM = 'item'; const ITEM = 'item'
function hnFetch(type, id = '') { function hnFetch(type, id = '') {
const url = id const url = id ? `${HN_PREFIX}${type}/${id}.json` : `${HN_PREFIX}${type}.json`
? `${HN_PREFIX}${type}/${id}.json`
: `${HN_PREFIX}${type}.json`;
return fetch(url, { return fetch(url, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
} },
}) })
.then(res => { .then(res => {
if (!isStatusOk(res.status)) { if (!isStatusOk(res.status)) {
throw res; throw res
} }
return res.json(); return res.json()
}) })
.then(res => res) .then(res => res)
.catch(error => console.error(error)); .catch(error => console.error(error))
} }
function isStatusOk(statusCode) { function isStatusOk(statusCode) {
return statusCode === 200 || statusCode === 304; return statusCode === 200 || statusCode === 304
} }
async function main() { async function main() {
const storyIds = await hnFetch(TOP_STORIES); const storyIds = await hnFetch(TOP_STORIES)
const stories = await Promise.all( const stories = await Promise.all(
storyIds.slice(0, 20).map(storyId => hnFetch(ITEM, storyId)) storyIds.slice(0, 20).map(storyId => hnFetch(ITEM, storyId))
); )
console.log( console.log(
stories.map(story => { stories.map(story => {
delete story.kids; delete story.kids
return story; return story
}) })
); )
} }
main(); main()

View File

@ -1,41 +1,52 @@
const express = require('express'); const express = require('express')
const path = require('path'); const path = require('path')
const morgan = require('morgan'); const morgan = require('morgan')
const ascii = require('./ascii'); const ascii = require('./ascii')
const db = require('./db'); const db = require('./db')
const axios = require('axios')
const app = express(); const app = express()
const port = process.env.PORT || 1337; const port = process.env.PORT || 1337
const buildPage = require('./scripts/buildPage')
db.authenticate() db.authenticate()
.then(() => { .then(() => {
console.log('Connection has been established successfully.'); console.log('Connection has been established successfully.')
}) })
.catch(err => { .catch(err => {
console.error('Unable to connect to the database:', err); console.error('Unable to connect to the database:', err)
}); })
app.use(morgan('tiny')); app.use(morgan('tiny'))
// body parsing middleware // body parsing middleware
app.use(express.json()); app.use(express.json())
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }))
app.use(require('body-parser').text()); app.use(require('body-parser').text())
app.use('/api', require('./api')); app.use('/api', require('./api'))
app.get('*', (req, res) => app.get('/articles', async (req, res) => {
res.sendFile(path.resolve(__dirname, 'public', 'articles.html')) console.log('path:', app.mountpath)
); try {
const { data } = await axios.get(`http://localhost:${port}/api/articles`)
console.log(data)
const page = buildPage(data)
res.send(page)
} catch (e) {
console.log(e)
}
})
app.get('*', (req, res) => res.send('try again.'))
// error handling endware // error handling endware
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
console.error(err); console.error(err)
console.error(err.stack); console.error(err.stack)
res.status(err.status || 500).send(err.message || 'Internal server error.'); res.status(err.status || 500).send(err.message || 'Internal server error.')
next(); next()
}); })
app.listen(port, () => { app.listen(port, () => {
console.log(`\n${ascii}\n`); console.log(`\n${ascii}\n`)
console.log(`Doin' haxor stuff on port ${port}`); console.log('mountpath:', app.mountpath)
}); console.log(`Doin' haxor stuff on port ${port}`)
})

43
public/articles.js Normal file
View File

@ -0,0 +1,43 @@
const router = require('express').Router()
const { Article } = require('../db/models')
const buildPage = require('./buildPage')
module.exports = router
router.get('/', async (req, res, next) => {
try {
const articles = await Article.findAll()
const tbl = articles
.map(
article => `<tr><td>${article.title}</td><td>${article.link}</td></tr>`
)
.join()
const page = buildPage(tbl)
console.log(page)
res.status(201).send(page)
} catch (err) {
next(err)
}
})
router.get('/:id', async (req, res, next) => {
try {
const article = await Article.findById(req.params.id)
console.log(article.title)
console.log(`by: ${article.author}`)
console.log(article.text)
res.status(201).send(article)
} catch (err) {
next(err)
}
})
router.post('/', async (req, res, next) => {
const body = req.body
try {
const article = await Article.create(body)
res.redirect(article.id)
} catch (err) {
next(err)
}
})

21
public/form.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<body>
<h2>Haxor Newz</h2>
<h3>much l337. very inform.</h3>
<form action="http://localhost:1337/api/comments" method="POST">
title:<br>
<input type="text" name="title" value="">
<br>
content:<br>
<textarea rows="4" cols="50" name="content"> </textarea>
<br>
<input type="submit" value="Submit">
</form>
<p> &#9398; anarchy planet </p>
</body>
</html>

View File

@ -0,0 +1,11 @@
const buildTable = articles =>
articles
.map(
article =>
`<tr><td><a href="${article.link}">${article.title}</a></td><td>${
article.content
}</td></tr>`
)
.join()
module.exports = buildTable

28
scripts/buildPage.js Normal file
View File

@ -0,0 +1,28 @@
const buildTable = require('./buildArticleTable')
const buildPage = arr => {
const listString = buildTable(arr)
return `
<!DOCTYPE html>
<html>
<body>
<h2>Haxor Newz</h2>
<h3>"uber l337"</h3>
<table>
<thead>
<th>Title</th>
<th>Link</th>
</thead>
${listString}
<tfoot>
ATTACK!
</tfoot>
</table>
<footer>&#9398; anarchy planet</footer>
</body>
</html>
`
}
module.exports = buildPage

16
scripts/post.sh Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# read user input: http://mywiki.wooledge.org/BashFAQ/078
URL='http://localhost:1337/api/comments'
read -p "title: " title
read -p "content: " content
DATA="{\"title\": \"$title\", \"content\": \"$content\"}"
echo "Posting $DATA to $URL"
curl -H "Content-Type: application/json" -X POST -d "$DATA" "$URL"
exit 0
#http://goinbigdata.com/using-curl-for-ad-hoc-testing-of-restful-microservices/
#https://stackoverflow.com/questions/7172784/how-to-post-json-data-with-curl-from-terminal-commandline-to-test-spring-rest