添加站内消息,修复讨论节点丢失的问题

This commit is contained in:
undefined committed 2020-05-03 15:33:54 +08:00
1 parent 33e277f28f
commit 627e64a901
44 files changed
+582 -540

No files matched your search

+1 -40
View File
@@ -9,46 +9,7 @@ process.stdin.on('data', async (input) => {
}
});
global.Hydro = {
handler: {},
template: {},
};
require('./lib/i18n');
require('./utils');
const bus = require('./service/bus');
const loader = require('./lib/loader');
async function run() {
await loader.prepare();
await new Promise((resolve) => {
const h = () => {
console.log('Database connected');
bus.unsubscribe(['system_database_connected'], h);
resolve();
};
bus.subscribe(['system_database_connected'], h);
require('./service/db');
});
require('./service/gridfs');
require('./service/queue');
const server = require('./service/server');
const builtinHandler = [
'home', 'problem', 'record', 'judge', 'user',
'contest', 'training', 'discussion', 'manage', 'import',
];
for (const i of builtinHandler) require(`./handler/${i}`);
await loader.model();
await loader.handler();
for (const i in global.Hydro.handler) {
await global.Hydro.handler[i].apply();
}
const notfound = require('./handler/notfound');
await notfound.apply();
server.start();
}
run().catch((e) => {
require('./loader')().catch((e) => {
console.error(e);
process.exit(1);
});
+37 -24
View File
@@ -4,9 +4,13 @@ const {
} = require('../error');
const options = require('../options');
const bus = require('../service/bus');
const { Route, Handler } = require('../service/server');
const {
Route, Connection, Handler, ConnectionHandler,
} = require('../service/server');
const misc = require('../lib/misc');
const md5 = require('../lib/md5');
const contest = require('../model/contest');
const message = require('../model/message');
const user = require('../model/user');
const setting = require('../model/setting');
const discussion = require('../model/discussion');
@@ -114,6 +118,7 @@ class HomeSecurityHandler extends Handler {
const sessions = await token.getSessionListByUid(this.user._id);
for (const session in sessions) {
if (tokenDigest === md5(session._id)) {
// eslint-disable-next-line no-await-in-loop
await token.delete(session._id, token.TYPE_SESSION);
return this.back();
}
@@ -175,7 +180,7 @@ class HomeMessagesHandler extends Handler {
udoc(udict, key) { // eslint-disable-line class-methods-use-this
const udoc = udict[key];
if (!udoc) return;
const gravatar_url = misc.gravatar_url(udoc.gravatar);
const gravatar_url = misc.gravatar(udoc.gravatar);
if (udoc.gravatar) udict[key] = { ...udoc, gravatar_url, gravatar: '' };
}
@@ -192,11 +197,11 @@ class HomeMessagesHandler extends Handler {
]);
// TODO(twd2): improve here:
for (const mdoc of mdocs) {
this.modify_udoc(udict, mdoc.from);
this.modify_udoc(udict, mdoc.to);
this.udoc(udict, mdoc.from);
this.udoc(udict, mdoc.to);
}
this.response.body = { messages: mdocs, udict };
this.response.template = 'home_message.html';
this.response.template = 'home_messages.html';
}
async postSendMessage({ uid, content }) {
@@ -204,10 +209,10 @@ class HomeMessagesHandler extends Handler {
const mdoc = await message.add(this.user._id, udoc._id, content);
// TODO(twd2): improve here:
// projection
mdoc.from = this.user;
this.modify_udoc(mdoc, 'from');
mdoc.to = udoc;
this.modify_udoc(mdoc, 'to');
mdoc.from_udoc = this.user;
this.udoc(mdoc, 'from');
mdoc.to_udoc = udoc;
this.udoc(mdoc, 'to');
if (this.user._id !== uid) {
await bus.publish(`user_message-${uid}`, { type: 'new', data: mdoc });
}
@@ -231,32 +236,40 @@ class HomeMessagesHandler extends Handler {
}
}
class HomeMessagesConnectionHandler extends ConnectionHandler {
async prepare() {
bus.subscribe([`message_received-${this.user._id}`], this.onMessageReceived);
}
async onMessageReceived(e) {
this.send(...e.value);
}
async clearup() {
bus.unsubscribe(this.onMessageReceived);
}
}
async function apply() {
Route('/', module.exports.HomeHandler);
Route('/home/security', module.exports.HomeSecurityHandler);
Route('/home/changeMail/:code', module.exports.UserChangemailWithCodeHandler);
Route('/home/settings/:category', module.exports.HomeSettingsHandler);
Route('/home/messages', module.exports.HomeMessagesHandler);
Connection('/home/messages-conn', module.exports.HomeMessagesConnectionHandler);
}
global.Hydro.handler.home = module.exports = {
HomeHandler, HomeSecurityHandler, HomeSettingsHandler, UserChangemailWithCodeHandler, apply,
HomeHandler,
HomeSecurityHandler,
HomeSettingsHandler,
UserChangemailWithCodeHandler,
HomeMessagesHandler,
HomeMessagesConnectionHandler,
apply,
};
/*
@app.connection_route('/home/messages-conn', 'home_messages-conn', global_route=True)
class HomeMessagesConnection(base.Connection):
@base.require_priv(builtin.PRIV_USER_PROFILE)
async def on_open(this):
await super(HomeMessagesConnection, this).on_open()
bus.subscribe(this.on_message_received, ['message_received-' + str(this.user['_id'])])
async def on_message_received(this, e):
this.send(**e['value'])
async def on_close(this):
bus.unsubscribe(this.on_message_received)
@app.route('/home/file', 'home_file', global_route=True)
class HomeFileHandler(base.OperationHandler):
def file_url(this, fdoc):
+2 -2
View File
@@ -2,8 +2,8 @@ const { Route, Handler } = require('../service/server');
const { NotFoundError } = require('../error');
class NotFoundHandler extends Handler {
async prepare() {
throw new NotFoundError(); // eslint-disable-line class-methods-use-this
async prepare() { // eslint-disable-line class-methods-use-this
throw new NotFoundError();
}
}
+1 -1
View File
@@ -169,7 +169,7 @@ class UserSearchHandler extends Handler {
}
for (const i in udocs) {
if (udocs[i].gravatar) {
udocs[i].gravatar_url = misc.gravatar_url(udocs[i].gravatar);
udocs[i].gravatar_url = misc.gravatar(udocs[i].gravatar);
}
}
this.response.body = udocs;
+4 -3
View File
@@ -10,7 +10,10 @@ async function run() {
let mongourl = 'mongodb://';
if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`;
mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`;
const Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true });
const Database = await Mongo.MongoClient.connect(
mongourl,
{ useNewUrlParser: true, useUnifiedTopology: true },
);
const db = Database.db(options.db.name);
const collUser = db.collection('user');
const collRole = db.collection('role');
@@ -48,8 +51,6 @@ async function run() {
}, udoc),
]);
}
await collUser.createIndex('unameLower', { unique: true });
await collUser.createIndex('mailLower', { sparse: true });
await collRole.insertMany(builtin.BUILTIN_ROLES);
await collBlacklist.createIndex('expireAt', { expireAfterSeconds: 0 });
await collToken.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true });
+11 -1
View File
@@ -21,13 +21,23 @@ String.prototype.format = function formatStr(...args) {
}
return result;
};
String.prototype.rawformat = function rawFormat(object) {
const res = this.split('{@}');
return [res[0], object, res[1]];
};
String.prototype.translate = function translate(language = 'zh_CN') {
if (locales[language]) return locales[language][this] || this;
return this;
};
module.exports = locales;
function load(data) {
for (const i in data) {
if (!locales[i]) locales[i] = data[i];
else locales[i] = Object.assign(locales[i], data[i]);
}
}
global.Hydro.locales = locales;
global.Hydro.lib.i18n = module.exports = load;
-49
View File
@@ -1,49 +0,0 @@
const fs = require('fs');
const path = require('path');
let installed;
function root(name) {
return path.resolve(process.cwd(), name);
}
function exist(name) {
try {
fs.statSync(root(name));
} catch (e) {
return false;
}
return true;
}
const superRequire = (name) => {
let m;
try {
m = require(root(name)); // eslint-disable-line import/no-dynamic-require
} catch (e) {
m = __non_webpack_require__(root(name)); // eslint-disable-line no-undef
}
return m;
};
async function prepare() {
installed = fs.readdirSync(root('.build/module'));
}
async function handler() {
for (const i of installed) {
if (exist(`.build/module/${i}/handler.js`)) {
superRequire(`.build/module/${i}/handler.js`);
console.log(`Handler init: ${i}`);
}
}
}
async function model() {
for (const i of installed) {
if (exist(`.build/module/${i}/model.js`)) {
superRequire(`.build/module/${i}/model.js`);
console.log(`Model init: ${i}`);
}
}
}
global.Hydro['lib.loader'] = module.exports = { prepare, handler, model };
+7 -3
View File
@@ -1,15 +1,19 @@
const hljs = require('highlight.js');
const MarkdownIt = require('markdown-it');
const katex = require('markdown-it-katex');
const Prism = require('prismjs');
const loadLanguages = require('prismjs/components/');
// FIXME doesnt work in webpack
loadLanguages(['js', 'javascript', 'go', 'c', 'pascal', 'hs', 'java', 'cs', 'cpp', 'py', 'ruby']);
class Markdown extends MarkdownIt {
constructor() {
super({
linkify: true,
highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) {
if (lang && Prism.languages[lang]) {
try {
return hljs.highlight(lang, str).value;
return Prism.highlight(str, Prism.languages[lang], lang);
} catch (__) { } // eslint-disable-line no-empty
}
return '';
+1 -1
View File
@@ -1,6 +1,6 @@
const md5 = require('./md5');
exports.gravatar_url = function gravatarUrl(email, size = 32) {
exports.gravatar = function gravatar(email, size = 32) {
return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${size}`;
};
+18
View File
@@ -0,0 +1,18 @@
const permission = require('../permission');
global.Hydro.ui.nav = [];
const Item = (path, name, prefix, perm) => {
global.Hydro.ui.nav.push({
path, name, prefix, perm,
});
};
Item('/', 'domain_main', 'domain_main');
Item('/p', 'problem_main', 'problem', permission.PERM_VIEW_PROBLEM);
Item('/t', 'training_main', 'training', permission.PERM_VIEW_TRAINING);
Item('/discuss', 'discussion_main', 'discussion', permission.PERM_VIEW_DISCUSSION);
Item('/c', 'contest_main', 'contest', permission.PERM_VIEW_CONTEST);
Item('/manage', 'domain_manage', 'domain_manage', permission.PERM_MANAGE);
global.Hydro.lib.nav = module.exports = Item;
+32 -18
View File
@@ -1,35 +1,49 @@
const path = require('path');
const nunjucks = require('nunjucks');
const markdown = require('./markdown');
const options = require('../options');
const perm = require('../permission');
const builtin = require('../model/builtin');
const model = require('../model');
const misc = require('./misc');
function Loader() { }
Loader.prototype.getSource = function getSource(name) {
if (!global.Hydro.template[name]) throw new Error();
return {
src: global.Hydro.template[name],
path: name,
};
};
class Nunjucks extends nunjucks.Environment {
constructor() {
super(
new nunjucks.FileSystemLoader(path.resolve(options.template.path)),
{ autoescape: true, trimBlocks: true },
);
super(new Loader(), { autoescape: true, trimBlocks: true });
this.addFilter('json', (self) => JSON.stringify(self), false);
this.addFilter('assign', (self, data) => Object.assign(self, data));
this.addFilter('markdown', (self) => markdown.render(self));
this.addFilter('base64_encode', (s) => Buffer.from(s).toString('base64'));
this.addFilter('gravatar_url', misc.gravatar_url);
this.addFilter('format_size', misc.format_size);
this.addGlobal('typeof', (o) => typeof o);
this.addGlobal('console', console);
this.addGlobal('static_url', (str) => `/${str}`);
this.addGlobal('reverse_url', (str) => str);
this.addGlobal('datetime_span', misc.datetime_span);
this.addGlobal('paginate', misc.paginate);
this.addGlobal('perm', perm);
this.addGlobal('status', builtin.STATUS);
this.addGlobal('model', model);
}
}
const env = new Nunjucks();
module.exports = env;
module.exports = {
render(name, state) {
Object.assign(state, {
typeof: (o) => typeof o,
static_url: (str) => `/${str}`,
datetime_span: misc.datetime_span,
paginate: misc.paginate,
perm,
status: builtin.status,
size: misc.size,
gravatar: misc.gravatar,
model,
Context: global.Hydro.ui,
});
return new Promise((resolve, reject) => {
env.render(name, state, (err, res) => {
if (err) reject(err);
else resolve(res);
});
});
},
};
+134
View File
@@ -0,0 +1,134 @@
/* eslint-disable import/no-dynamic-require */
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
function root(name) {
return path.resolve(process.cwd(), name);
}
const installed = fs.readdirSync(root('.build/module'));
function exist(name) {
try {
fs.statSync(root(name));
} catch (e) {
return false;
}
return true;
}
const superRequire = (name) => {
let m;
try {
m = require(root(name));
} catch (e) {
m = __non_webpack_require__(root(name)); // eslint-disable-line no-undef
}
return m;
};
async function handler() {
for (const i of installed) {
if (exist(`.build/module/${i}/handler.js`)) {
superRequire(`.build/module/${i}/handler.js`);
console.log(`Handler init: ${i}`);
}
}
}
async function locale() {
if (exist('.build/locale.js')) {
global.Hydro.lib.i18n(superRequire('.build/locale.js'));
console.log('Locale init: builtin');
}
for (const i of installed) {
if (exist(`.build/module/${i}/locale.js`)) {
global.Hydro.lib.i18n(superRequire(`.build/module/${i}/locale.js`));
console.log(`Locale init: ${i}`);
}
}
}
async function template() {
if (exist('.build/template.yaml')) {
const file = fs.readFileSync(root('.build/template.yaml')).toString();
Object.assign(global.Hydro.template, yaml.safeLoad(file));
console.log('Template init: builtin');
}
for (const i of installed) {
if (exist(`.build/module/${i}/locale.js`)) {
const file = fs.readFileSync(root(`.build/module/${i}/template.yaml`)).toString();
Object.assign(global.Hydro.template, yaml.safeLoad(file));
console.log(`Template init: ${i}`);
}
}
}
async function model() {
for (const i of installed) {
if (exist(`.build/module/${i}/model.js`)) {
const m = superRequire(`.build/module/${i}/model.js`);
if (m.index) await m.index(); // eslint-disable-line no-await-in-loop
console.log(`Model init: ${i}`);
}
}
}
async function load() {
global.Hydro = {
handler: {},
service: {},
model: {},
lib: {},
template: {},
ui: {},
};
await template();
require('./lib/i18n');
require('./utils');
require('./error');
require('./permission');
require('./options');
const bus = require('./service/bus');
await new Promise((resolve) => {
const h = () => {
console.log('Database connected');
bus.unsubscribe(['system_database_connected'], h);
resolve();
};
bus.subscribe(['system_database_connected'], h);
require('./service/db');
});
const builtinLib = [
'axios', 'download', 'i18n', 'mail', 'markdown',
'md5', 'misc', 'paginate', 'pwhash', 'rank',
'template', 'validator', 'nav',
];
for (const i of builtinLib) require(`./lib/${i}`);
await locale();
require('./service/gridfs');
require('./service/queue');
const server = require('./service/server');
const builtinModel = [
'blacklist', 'builtin', 'contest', 'discussion', 'message',
'opcount', 'problem', 'record', 'setting', 'solution',
'system', 'token', 'training', 'user',
];
for (const i of builtinModel) {
const m = require(`./model/${i}`);
if (m.index) await m.index(); // eslint-disable-line no-await-in-loop
}
const builtinHandler = [
'home', 'problem', 'record', 'judge', 'user',
'contest', 'training', 'discussion', 'manage', 'import',
];
for (const i of builtinHandler) require(`./handler/${i}`);
await model();
await handler();
for (const i in global.Hydro.handler) {
await global.Hydro.handler[i].apply(); // eslint-disable-line no-await-in-loop
}
const notfound = require('./handler/notfound');
await notfound.apply();
server.start();
}
module.exports = load;
+5 -2
View File
@@ -1,5 +1,7 @@
const { ObjectID } = require('bson');
const problem = require('./problem');
const contest = require('./contest');
const { PERM_VIEW_PROBLEM_HIDDEN } = require('../permission');
const { DocumentNotFoundError } = require('../error');
const db = require('../service/db.js');
@@ -8,7 +10,7 @@ const collReply = db.collection('discussion.reply');
const collStatus = db.collection('discussion.status');
async function add(parentType, parentId, owner, title, content, ip = null, highlight = false) {
let res = await coll.insertOne({
const res = await coll.insertOne({
owner,
title,
content,
@@ -145,7 +147,8 @@ async function getVnode(ddoc, handler) {
async function getListVnodes(ddocs, handler) {
const res = {};
for (const ddoc of ddocs) {
res[ddoc._id] = await getVnode(ddoc, handler);
// FIXME no-await-in-loop
res[ddoc._id] = await getVnode(ddoc, handler); // eslint-disable-line no-await-in-loop
}
return res;
}
+72
View File
@@ -0,0 +1,72 @@
const { MessageNotFoundError } = require('../error');
const db = require('../service/db.js');
const coll = db.collection('message');
async function add(from, to, content) {
const res = await coll.insertOne({
from,
to,
fromUnread: false,
toUnread: true,
reply: [
{
from, unread: true, content, at: new Date(),
},
],
});
return await coll.findOne({ _id: res.insertedId }); // eslint-disable-line no-return-await
}
async function get(_id) {
const doc = await coll.findOne({ _id });
if (!doc) throw new MessageNotFoundError();
return doc;
}
function getMany(query, sort, page, limit) {
return coll.find(query).sort(sort)
.skip((page - 1) * limit).limit(limit)
.toArray();
}
function del(_id) {
return coll.deleteOne({ _id });
}
function count(query) {
return coll.find(query).count();
}
function getMulti(uid) {
return coll.find({ $or: [{ from: uid }, { to: uid }] });
}
async function addReply(_id, from, content) {
const reply = {
from,
content,
unread: true,
at: new Date(),
};
await coll.updateOne({ _id }, { $push: { reply } });
return await coll.findOne({ _id }); // eslint-disable-line no-return-await
}
function index() {
return Promise.all([
coll.createIndex({ to: 1, _id: -1 }),
coll.createIndex({ from: 1, _id: -1 }),
]);
}
global.Hydro.model.message = module.exports = {
count,
add,
get,
del,
getMany,
getMulti,
addReply,
index,
};
+12 -8
View File
@@ -26,8 +26,8 @@ class USER {
this.nSubmit = user.nSubmit || 0;
}
hasPerm(perm) {
return this.perm.includes(perm);
hasPerm(p) {
return this.perm.includes(p);
}
checkPassword(password) {
@@ -144,7 +144,7 @@ async function getPrefixList(prefix, limit = 50) {
async function setRole(uid, role) {
const udoc = await getById(uid);
return await Promise.all([
return await Promise.all([ // eslint-disable-line no-return-await
coll.findOneAndUpdate({ _id: uid }, { $set: { role } }),
collRole.updateOne({ _id: udoc.role }, { $inc: { count: -1 } }),
collRole.updateOne({ _id: role }, { $inc: { count: 1 } }),
@@ -159,8 +159,8 @@ function getRole(name) {
return collRole.findOne({ _id: name });
}
function addRole(name, perm) {
return collRole.insertOne({ _id: name, perm, count: 0 });
function addRole(name, permission) {
return collRole.insertOne({ _id: name, perm: permission, count: 0 });
}
function deleteRoles(roles) {
@@ -170,8 +170,12 @@ function deleteRoles(roles) {
]);
}
function init() {
return collRole.update({ _id: 'root' }, { $set: { perm: perm.PERM_ALL } });
function index() {
return Promise.all([
collRole.updateOne({ _id: 'root' }, { $set: { perm: perm.PERM_ALL } }),
coll.createIndex('unameLower', { unique: true }),
coll.createIndex('mailLower', { sparse: true }),
]);
}
module.exports = {
@@ -192,5 +196,5 @@ module.exports = {
getRoles,
addRole,
deleteRoles,
init,
index,
};
+1 -1
View File
@@ -114,4 +114,4 @@ perm.PERM_DEFAULT = perm.PERM_BASIC
perm.PERM_ADMIN = perm.PERM_ALL;
module.exports = perm;
global.Hydro.permission = module.exports = perm;
+33
View File
@@ -0,0 +1,33 @@
class Cron {
constructor() {
this.tasks = [];
}
plan(time, task) {
const now = new Date();
if (!(time instanceof Date)) time = new Date(time);
this.tasks.push({
id: setTimeout(() => {
for (const i in this.tasks) {
if (this.tasks[i].func === task) {
this.tasks.splice(i, 1);
break;
}
}
task();
}, time - now),
func: task,
});
}
interval(time, task) {
const now = new Date();
if (!(time instanceof Date)) time = new Date(time);
this.tasks.push({
id: setTimeout(() => { task(); }, time - now),
func: task,
});
}
}
module.exports = Cron;
+1 -1
View File
@@ -16,6 +16,6 @@ Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology:
bus.publish('system_database_connected', null);
});
module.exports = {
global.Hydro.service.db = module.exports = {
collection: (c) => db.collection(c),
};
+21 -29
View File
@@ -65,20 +65,16 @@ class Handler {
this.session = {};
}
renderHTML(name, context) {
async renderHTML(name, context) {
console.time(name);
this.hasPerm = (perm) => this.user.hasPerm(perm);
return new Promise((resolve, reject) => {
template.render(name, Object.assign(context, {
handler: this,
_: (str) => (str ? str.toString().translate(this.user.language) : ''),
user: this.user,
}), (error, res) => {
console.timeEnd(name);
if (error) reject(error);
else resolve(res);
});
});
const res = await template.render(name, Object.assign(context, {
handler: this,
_: (str) => (str ? str.toString().translate(this.user.language) : ''),
user: this.user,
}));
console.timeEnd(name);
return res;
}
async render(name, context) {
@@ -320,20 +316,16 @@ class ConnectionHandler {
for (const i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]);
}
renderHTML(name, context) {
async renderHTML(name, context) {
console.time(name);
this.hasPerm = (perm) => this.user.hasPerm(perm);
return new Promise((resolve, reject) => {
template.render(name, Object.assign(context, {
handler: this,
_: (str) => (str ? str.toString().translate(this.user.language) : ''),
user: this.user,
}), (error, res) => {
console.timeEnd(name);
if (error) reject(error);
else resolve(res);
});
});
const res = await template.render(name, Object.assign(context, {
handler: this,
_: (str) => (str ? str.toString().translate(this.user.language) : ''),
user: this.user,
}));
console.timeEnd(name);
return res;
}
send(data) {
@@ -401,14 +393,14 @@ function Connection(prefix, RouteConnHandler) {
sock.installHandlers(server);
}
exports.Handler = Handler;
exports.ConnectionHandler = ConnectionHandler;
exports.Route = Route;
exports.Connection = Connection;
exports.start = function start() {
function start() {
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
app.use(router.routes()).use(router.allowedMethods());
Route('*', Handler);
server.listen(options.listen.port);
console.log('Server listening at: %s', options.listen.port);
}
global.Hydro.service.server = module.exports = {
Handler, ConnectionHandler, Route, Connection, start,
};