jschan - Anonymous imageboard software. Classic look, modern features and feel. Works without JavaScript and supports Tor, I2P, Lokinet, etc.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

701 lines
14 KiB

5 years ago
'use strict';
const Mongo = require(__dirname+'/db.js')
, { isIP } = require('net')
, Boards = require(__dirname+'/boards.js')
, Stats = require(__dirname+'/stats.js')
, db = Mongo.db.collection('posts')
, config = require(__dirname+'/../config.js');
5 years ago
module.exports = {
5 years ago
db,
getThreadPage: async (board, thread) => {
const threadsBefore = await db.countDocuments({
'board': board,
'thread': null,
'bumped': {
'$gte': thread.bumped
}
});
return Math.ceil(threadsBefore/10) || 1; //1 because 0 threads before is page 1
},
3 years ago
getBoardRecent: async (offset=0, limit=20, ip, board, permLevel) => {
const query = {};
3 years ago
if (board) {
query['board'] = board;
}
const projection = {
'salt': 0,
'password': 0,
};
3 years ago
if (!board) {
projection['reports'] = 0;
} else {
projection['globalreports'] = 0;
}
if (ip != null) {
if (isIP(ip)) {
query['ip.raw'] = ip;
} else {
query['ip.single'] = ip;
}
}
3 years ago
if (permLevel > config.get.ipHashPermLevel) {
projection['ip.raw'] = 0;
3 years ago
//MongoError, why cant i just projection['reports.ip.raw'] = 0;
if (board) {
projection['reports'] = { ip: { raw: 0 } };
} else {
projection['globalreports'] = { ip: { raw: 0 } };
}
}
3 years ago
const posts = await db.find(query, {
projection
}).sort({
'_id': -1
}).skip(offset).limit(limit).toArray();
3 years ago
return posts;
},
getRecent: async (board, page, limit=10, getSensitive=false, sortSticky=true) => {
5 years ago
// get all thread posts (posts with null thread id)
const projection = {
'salt': 0,
'password': 0,
'reports': 0,
'globalreports': 0,
};
if (!getSensitive) {
projection['ip'] = 0;
}
const threadsQuery = {
5 years ago
'thread': null,
};
if (board) {
if (Array.isArray(board)) {
//array for overboard
threadsQuery['board'] = {
'$in': board
}
} else {
threadsQuery['board'] = board;
}
}
let threadsSort = {
'bumped': -1,
};
if (sortSticky === true) {
threadsSort = {
'sticky': -1,
'bumped': -1
}
}
const threads = await db.find(threadsQuery, {
projection
})
.sort(threadsSort)
.skip(10*(page-1))
.limit(limit)
.toArray();
5 years ago
// add last n posts in reverse order to preview
5 years ago
await Promise.all(threads.map(async thread => {
const { stickyPreviewReplies, previewReplies } = config.get;
const previewRepliesLimit = thread.sticky ? stickyPreviewReplies : previewReplies;
const replies = previewRepliesLimit === 0 ? [] : await db.find({
5 years ago
'thread': thread.postId,
'board': thread.board
5 years ago
},{
projection
5 years ago
}).sort({
'postId': -1
}).limit(previewRepliesLimit).toArray();
//reverse order for board page
thread.replies = replies.reverse();
5 years ago
//if enough replies, show omitted count
if (thread.replyposts > previewRepliesLimit) {
//dont show all backlinks on OP for previews on index page
thread.previewbacklinks = [];
if (previewRepliesLimit > 0) {
const firstPreviewId = thread.replies[0].postId;
const latestPreviewBacklink = thread.backlinks.find(bl => { return bl.postId >= firstPreviewId });
if (latestPreviewBacklink != null) {
const latestPreviewIndex = thread.backlinks.map(bl => bl.postId).indexOf(latestPreviewBacklink.postId);
thread.previewbacklinks = thread.backlinks.slice(latestPreviewIndex);
}
}
//count omitted image and posts
const numPreviewFiles = replies.reduce((acc, post) => { return acc + post.files.length }, 0);
thread.omittedfiles = thread.replyfiles - numPreviewFiles;
thread.omittedposts = thread.replyposts - replies.length;
}
}));
5 years ago
return threads;
},
5 years ago
resetThreadAggregates: (ors) => {
return db.aggregate([
{
'$match': {
'$or': ors
}
}, {
'$set': {
'replyposts': 0,
'replyfiles': 0,
'bumped': '$date'
}
}, {
'$project': {
'_id': 1,
'board': 1,
'replyposts': 1,
'replyfiles': 1,
'bumped': 1
}
}
]).toArray();
},
getThreadAggregates: (ors) => {
return db.aggregate([
{
'$match': {
'$or': ors
}
}, {
5 years ago
'$group': {
'_id': {
'thread': '$thread',
'board': '$board'
},
5 years ago
'replyposts': {
'$sum': 1
},
'replyfiles': {
'$sum': {
'$size': '$files'
}
},
'bumped': {
'$max': {
'$cond': [
{ '$ne': [ '$email', 'sage' ] },
'$date',
0 //still need to improve this to ignore bump limitthreads
]
}
}
5 years ago
}
}
]).toArray();
},
getPages: (board) => {
5 years ago
return db.countDocuments({
'board': board,
'thread': null
5 years ago
});
},
getThread: async (board, id, getSensitive=false) => {
5 years ago
// get thread post and potential replies concurrently
const projection = {
'salt': 0,
'password': 0,
'reports': 0,
'globalreports': 0,
};
if (!getSensitive) {
projection['ip'] = 0;
}
const [thread, replies] = await Promise.all([
5 years ago
db.findOne({
'postId': id,
'board': board,
'thread': null,
}, {
projection,
5 years ago
}),
module.exports.getThreadPosts(board, id, projection)
5 years ago
])
// attach the replies to the thread post
if (thread && replies) {
thread.replies = replies;
5 years ago
}
return thread;
},
5 years ago
getThreadPosts: (board, id, projection) => {
// all posts within a thread
5 years ago
return db.find({
'thread': id,
'board': board
}, {
projection
}).sort({
'postId': 1
}).toArray();
},
getMultipleThreadPosts: (board, ids) => {
//all posts from multiple threads in a single board
return db.find({
'board': board,
'thread': {
'$in': ids
}
}, {
'projection': {
'salt': 0 ,
'password': 0,
'ip': 0,
'reports': 0,
'globalreports': 0,
}
}).toArray();
},
getCatalog: (board, sortSticky=true, catalogLimit=0) => {
5 years ago
const threadsQuery = {
thread: null,
}
if (board) {
if (Array.isArray(board)) {
//array for overboard catalog
threadsQuery['board'] = {
'$in': board
}
} else {
threadsQuery['board'] = board;
}
}
let threadsSort = {
'bumped': -1,
};
if (sortSticky === true) {
threadsSort = {
'sticky': -1,
'bumped': -1
}
}
5 years ago
// get all threads for catalog
return db.find(threadsQuery, {
5 years ago
'projection': {
'salt': 0,
'password': 0,
'ip': 0,
'reports': 0,
'globalreports': 0,
5 years ago
}
})
.limit(catalogLimit)
.sort(threadsSort)
.toArray();
5 years ago
},
5 years ago
getPost: (board, id, getSensitive=false) => {
5 years ago
// get a post
if (getSensitive) {
5 years ago
return db.findOne({
'postId': id,
'board': board
});
}
5 years ago
return db.findOne({
'postId': id,
'board': board
}, {
5 years ago
'projection': {
'salt': 0,
'password': 0,
'ip': 0,
'reports': 0,
'globalreports': 0,
5 years ago
}
5 years ago
});
},
5 years ago
checkExistingMessage: async (board, thread = null, hash) => {
const query = {
'board': board,
'messagehash': hash,
}
if (thread !== null) {
query['$or'] = [
{ 'thread': thread },
{ 'postId': thread },
]
}
const postWithExistingMessage = await db.findOne(query, {
'projection': {
'messagehash': 1,
}
});
return postWithExistingMessage;
},
checkExistingFiles: async (board, thread = null, hashes) => {
const query = {
'board': board,
'files.hash': {
'$in': hashes
}
}
if (thread !== null) {
query['$or'] = [
{ 'thread': thread },
{ 'postId': thread },
]
}
const postWithExistingFiles = await db.findOne(query, {
'projection': {
'files.hash': 1,
}
});
return postWithExistingFiles;
},
allBoardPosts: (board) => {
return db.find({
'board': board
}).toArray();
},
//takes array "ids" of post ids
getPosts: (board, ids, getSensitive=false) => {
if (getSensitive) {
5 years ago
return db.find({
'postId': {
'$in': ids
5 years ago
},
'board': board
}).toArray();
}
5 years ago
return db.find({
'postId': {
'$in': ids
5 years ago
},
'board': board
}, {
5 years ago
'projection': {
'salt': 0,
'password': 0,
'ip': 0,
'reports': 0,
'globalreports': 0,
5 years ago
}
}).toArray();
},
// get only thread and post id for use in quotes
getPostsForQuotes: (queryOrs) => {
const { quoteLimit } = config.get;
return db.find({
'$or': queryOrs
}, {
'projection': {
'postId': 1,
'board': 1,
'thread': 1,
}
}).limit(quoteLimit).toArray();
},
//takes array "ids" of mongo ids to get posts from any board
globalGetPosts: (ids) => {
return db.find({
'_id': {
'$in': ids
},
}).toArray();
},
insertOne: async (board, data, thread, tor) => {
const sageEmail = data.email === 'sage';
const bumpLocked = thread && thread.bumplocked === 1;
const bumpLimited = thread && thread.replyposts >= board.settings.bumpLimit;
const cyclic = thread && thread.cyclic === 1;
const saged = sageEmail || bumpLocked || (bumpLimited && !cyclic);
if (data.thread !== null) {
const filter = {
5 years ago
'postId': data.thread,
'board': board._id
}
//update thread reply and reply file count
const query = {
'$inc': {
'replyposts': 1,
'replyfiles': data.files.length
5 years ago
}
}
//if post email is not sage, and thread not bumplocked, set bump date
if (!saged) {
query['$set'] = {
'bumped': new Date()
}
} else if (bumpLimited && !cyclic) {
query['$set'] = {
'bumplocked': 1
}
}
//update the thread
await db.updateOne(filter, query);
} else {
//this is a new thread so just set the bump date
data.bumped = new Date();
5 years ago
}
//get the postId and add it to the post
const postId = await Boards.getNextId(board._id, saged);
5 years ago
data.postId = postId;
//insert the post itself
const postMongoId = await db.insertOne(data).then(result => result.insertedId); //_id of post
3 years ago
const statsIp = (config.get.statsCountAnonymizers === false && res.locals.anonymizer === true) ? null : data.ip.single;
await Stats.updateOne(board._id, statsIp, data.thread == null);
//add backlinks to the posts this post quotes
if (data.thread && data.quotes.length > 0) {
await db.updateMany({
'_id': {
'$in': data.quotes.map(q => q._id)
}
}, {
'$push': {
'backlinks': { _id: postMongoId, postId: postId }
}
});
}
return { postMongoId, postId };
},
5 years ago
getBoardReportCounts: (boards) => {
return db.aggregate([
{
'$match': {
'board': {
'$in': boards
},
'reports.0': {
'$exists': true
},
}
}, {
'$group': {
'_id': '$board',
'count': {
'$sum': 1
}
}
}
]).toArray();
},
getGlobalReportsCount: () => {
return db.countDocuments({
'globalreports.0': {
'$exists': true
}
})
},
getReports: async (board, permLevel) => {
const projection = {
'salt': 0,
'password': 0,
'globalreports': 0,
};
if (permLevel > config.get.ipHashPermLevel) {
projection['ip.raw'] = 0;
projection['reports'] = { ip: { raw: 0 } };
}
const posts = await db.find({
'reports.0': {
'$exists': true
},
'board': board
}, { projection }).toArray();
return posts;
},
getGlobalReports: async (offset=0, limit, ip, permLevel) => {
const projection = {
'salt': 0,
'password': 0,
'reports': 0,
};
if (permLevel > config.get.ipHashPermLevel) {
projection['ip.raw'] = 0;
projection['globalreports'] = { ip: { raw: 0 } };
}
const query = {
'globalreports.0': {
'$exists': true
}
}
if (ip != null) {
if (isIP(ip)) {
query['$or'] = [
{ 'ip.raw': ip },
{ 'globalreports.ip.raw': ip }
];
} else {
query['$or'] = [
{ 'ip.single': ip },
{ 'globalreports.ip.single': ip }
];
}
}
const posts = await db.find(query, { projection }).skip(offset).limit(limit).toArray();
return posts;
},
deleteOne: (board, options) => {
5 years ago
return db.deleteOne(options);
},
5 years ago
pruneThreads: async (board) => {
//get threads that have been bumped off last page
const oldThreads = await db.find({
'thread': null,
'board': board._id
}).sort({
'sticky': -1,
'bumped': -1
}).skip(board.settings.threadLimit).toArray();
let early404Threads = [];
if (board.settings.early404 === true) {
early404Threads = await db.aggregate([
{
//get all the threads for a board
'$match': {
'thread': null,
'board': board._id
}
}, {
//in bump date order
'$sort': {
'sticky': -1,
'bumped': -1
}
}, {
//skip the first (board.settings.threadLimit/early404Fraction)
3 years ago
'$skip': Math.ceil(board.settings.threadLimit/config.get.early404Fraction)
}, {
//then any that have less than early404Replies replies get matched again
'$match': {
'sticky':0,
'replyposts': {
3 years ago
'$lt': config.get.early404Replies
}
}
}
]).toArray();
}
return oldThreads.concat(early404Threads);
},
fixLatest: (boards) => {
return db.aggregate([
{
'$match': {
//going to match against thread bump date instead
'thread': null,
'board': {
'$in': boards
},
}
}, {
'$group': {
'_id': '$board',
'lastPostTimestamp': {
'$max':'$bumped'
}
}
}, {
'$merge': {
'into': 'boards'
}
}
]).toArray();
},
deleteMany: (ids) => {
5 years ago
return db.deleteMany({
'_id': {
'$in': ids
}
});
},
5 years ago
5 years ago
deleteAll: (board) => {
return db.deleteMany();
},
5 years ago
move: (ids, dest) => {
return db.updateMany({
'_id': {
'$in': ids
}
}, {
'$set': {
'thread': dest
},
'$unset': {
'replyposts': '',
'replyfiles': '',
'sticky': '',
'locked': '',
'bumplocked': '',
'cyclic': '',
'salt': ''
}
});
},
threadExists: (board, thread) => {
return db.findOne({
'postId': thread,
'board': board,
'thread': null,
}, {
'projection': {
'_id': 1,
'postId': 1,
'salt': 1,
}
});
},
exists: async (req, res, next) => {
const thread = await module.exports.threadExists(req.params.board, req.params.id);
if (!thread) {
return res.status(404).render('404');
}
res.locals.thread = thread; // can acces this in views or next route handlers
next();
}
}