core&ui: add discussion history (#450)

Co-authored-by: 无限UCW <45730483+wuxianucw@users.noreply.github.com>
Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
authored and GitHub committed 2022-10-27 13:55:17 +08:00
1 parent 3e918346ff
commit f863d80ee0
14 files changed
+227 -45

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "4.2.12",
"version": "4.2.13",
"bin": "bin/hydrooj.js",
"main": "src/plugin-api",
"module": "src/plugin-api",
+16 -7
View File
@@ -272,7 +272,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
if (!this.user.own(this.drdoc)) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
await Promise.all([
discussion.editReply(domainId, drid, content),
discussion.editReply(domainId, drid, content, this.user._id, this.request.ip),
oplog.log(this, 'discussion.reply.edit', this.drdoc),
]);
this.back();
@@ -312,7 +312,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
if (!this.user.own(this.drrdoc)) throw new PermissionError(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
await Promise.all([
discussion.editTailReply(domainId, drid, drrid, content),
discussion.editTailReply(domainId, drid, drrid, content, this.user._id, this.request.ip),
oplog.log(this, 'discussion.tailReply.edit', this.drrdoc),
]);
this.back();
@@ -354,11 +354,20 @@ class DiscussionDetailHandler extends DiscussionHandler {
}
class DiscussionRawHandler extends DiscussionHandler {
@param('did', Types.ObjectID, true)
@param('drid', Types.ObjectID, true)
@param('drrid', Types.ObjectID, true)
async get(domainId: string, drid: ObjectID, drrid: ObjectID) {
this.response.type = 'text/markdown';
this.response.body = drrid ? this.drrdoc.content : drid ? this.drdoc.content : this.ddoc.content;
@param('time', Types.UnsignedInt, true)
@param('all', Types.Boolean, true)
async get(domainId: string, did: ObjectID, drid: ObjectID, drrid: ObjectID, ts: number, all = false) {
if (all) {
this.response.body.history = await discussion.getHistory(domainId, drrid || drid || did);
} else {
const [doc] = await discussion.getHistory(domainId, drrid || drid || did, ts ? { time: new Date(ts) } : {});
if (!doc && ts) throw new DiscussionNotFoundError(drrid || drid || did);
this.response.type = 'text/markdown';
this.response.body = doc ? doc.content : drrid ? this.drrdoc.content : drid ? this.drdoc.content : this.ddoc.content;
}
}
}
@@ -390,7 +399,7 @@ class DiscussionEditHandler extends DiscussionHandler {
if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin;
await Promise.all([
discussion.edit(domainId, did, {
title, content, highlight, pin,
title, highlight, pin, content, editor: this.user._id, edited: true,
}),
oplog.log(this, 'discussion.edit', this.ddoc),
]);
@@ -427,7 +436,7 @@ export async function apply(ctx) {
ctx.Route('discussion_main', '/discuss', DiscussionMainHandler);
ctx.Route('discussion_detail', '/discuss/:did', DiscussionDetailHandler);
ctx.Route('discussion_edit', '/discuss/:did/edit', DiscussionEditHandler);
ctx.Route('discussion_detail', '/discuss/:did/raw', DiscussionRawHandler);
ctx.Route('discussion_raw', '/discuss/:did/raw', DiscussionRawHandler);
ctx.Route('discussion_reply_raw', '/discuss/:did/:drid/raw', DiscussionRawHandler);
ctx.Route('discussion_tail_reply_raw', '/discuss/:did/:drid/:drrid/raw', DiscussionRawHandler);
ctx.Route('discussion_node', '/discuss/:type/:name', DiscussionNodeHandler);
+22 -12
View File
@@ -405,11 +405,6 @@ export interface BlacklistDoc {
expireAt: Date;
}
export interface HistoryDoc {
content: string;
time: Date;
}
// Discussion
export type { DiscussionDoc } from './model/discussion';
declare module './model/discussion' {
@@ -426,7 +421,8 @@ declare module './model/discussion' {
updateAt: Date;
nReply: number;
views: number;
history: HistoryDoc[];
edited?: boolean;
editor?: number;
react: Record<string, number>;
sort: number;
lastRCount: number;
@@ -442,16 +438,18 @@ export interface DiscussionReplyDoc extends Document {
ip: string;
content: string;
reply: DiscussionTailReplyDoc[];
history: HistoryDoc[];
edited?: boolean;
editor?: number;
react: Record<string, number>;
}
export interface DiscussionTailReplyDoc {
_id: ObjectID,
owner: number,
content: string,
ip: string,
history: HistoryDoc[],
_id: ObjectID;
owner: number;
content: string;
ip: string;
edited?: boolean;
editor?:number;
}
export interface BlogDoc {
@@ -602,6 +600,17 @@ export interface OauthMap {
uid: number;
}
export interface DiscussionHistoryDoc {
title?: string;
content: string;
domainId: string;
docId: ObjectID;
/** Create time */
time: Date;
uid: number;
ip: string;
}
declare module './service/db' {
interface Collections {
'blacklist': BlacklistDoc;
@@ -610,6 +619,7 @@ declare module './service/db' {
'record': RecordDoc;
'document': any;
'document.status': StatusDocBase;
'discussion.history': DiscussionHistoryDoc;
'user': Udoc;
'user.preference': UserPreferenceDoc;
'vuser': VUdoc;
+75 -19
View File
@@ -3,8 +3,11 @@ import moment from 'moment';
import { FilterQuery, ObjectID } from 'mongodb';
import { Context } from '../context';
import { DiscussionNodeNotFoundError, DocumentNotFoundError } from '../error';
import { DiscussionReplyDoc, DiscussionTailReplyDoc, Document } from '../interface';
import {
DiscussionHistoryDoc, DiscussionReplyDoc, DiscussionTailReplyDoc, Document,
} from '../interface';
import * as bus from '../service/bus';
import db from '../service/db';
import { NumberKeys } from '../typeutils';
import { buildProjection } from '../utils';
import * as contest from './contest';
@@ -22,9 +25,12 @@ export const PROJECTION_LIST: Field[] = [
'parentId', 'parentType', 'title',
];
export const PROJECTION_PUBLIC: Field[] = [
...PROJECTION_LIST, 'content', 'history', 'react', 'maintainer',
...PROJECTION_LIST, 'content', 'edited', 'react', 'maintainer',
'lock',
];
export const HISTORY_PROJECTION_PUBLIC: (keyof DiscussionHistoryDoc)[] = [
'title', 'content', 'docId', 'uid', 'time',
];
export const typeDisplay = {
[document.TYPE_PROBLEM]: 'problem',
@@ -34,15 +40,19 @@ export const typeDisplay = {
[document.TYPE_HOMEWORK]: 'homework',
};
export const coll = db.collection('discussion.history');
export async function add(
domainId: string, parentType: number, parentId: ObjectID | number | string,
owner: number, title: string, content: string,
ip: string | null = null, highlight: boolean, pin: boolean,
): Promise<ObjectID> {
const time = new Date();
const payload: Partial<DiscussionDoc> = {
domainId,
content,
owner,
editor: owner,
parentType,
parentId,
title,
@@ -50,7 +60,7 @@ export async function add(
nReply: 0,
highlight,
pin,
updateAt: new Date(),
updateAt: time,
views: 0,
sort: 100,
};
@@ -70,7 +80,10 @@ export async function get<T extends Field>(
return await document.get(domainId, document.TYPE_DISCUSSION, did, projection);
}
export function edit(domainId: string, did: ObjectID, $set: Partial<DiscussionDoc>) {
export async function edit(domainId: string, did: ObjectID, $set: Partial<DiscussionDoc>) {
await coll.insertOne({
domainId, docId: did, content: $set.content, uid: $set.editor, ip: $set.ip, time: new Date(),
});
return document.set(domainId, document.TYPE_DISCUSSION, did, $set);
}
@@ -80,13 +93,20 @@ export function inc(
return document.inc(domainId, document.TYPE_DISCUSSION, did, key, value);
}
export function del(domainId: string, did: ObjectID): Promise<never> {
return Promise.all([
export async function del(domainId: string, did: ObjectID): Promise<void> {
const [ddoc, drdocs] = await Promise.all([
document.get(domainId, document.TYPE_DISCUSSION, did),
document.getMulti(domainId, document.TYPE_DISCUSSION_REPLY, {
parentType: document.TYPE_DISCUSSION, parentId: did,
}).project({ _id: 1, 'reply._id': 1 }).toArray(),
]) as any;
await Promise.all([
document.deleteOne(domainId, document.TYPE_DISCUSSION, did),
document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, {
parentType: document.TYPE_DISCUSSION, parentId: did,
}),
document.deleteMultiStatus(domainId, document.TYPE_DISCUSSION, { docId: did }),
coll.deleteMany({ domainId, docId: { $in: [ddoc._id, ...(drdocs.reply?.map((i) => i._id) || [])] } }),
]) as any;
}
@@ -104,13 +124,17 @@ export async function addReply(
domainId: string, did: ObjectID, owner: number,
content: string, ip: string,
): Promise<ObjectID> {
const time = new Date();
const [drid] = await Promise.all([
document.add(
domainId, content, owner, document.TYPE_DISCUSSION_REPLY,
null, document.TYPE_DISCUSSION, did, { ip },
null, document.TYPE_DISCUSSION, did, { ip, editor: owner },
),
document.incAndSet(domainId, document.TYPE_DISCUSSION, did, 'nReply', 1, { updateAt: new Date() }),
document.incAndSet(domainId, document.TYPE_DISCUSSION, did, 'nReply', 1, { updateAt: time }),
]);
await coll.insertOne({
domainId, docId: drid, content, uid: owner, ip, time,
});
return drid;
}
@@ -119,9 +143,12 @@ export function getReply(domainId: string, drid: ObjectID): Promise<DiscussionRe
}
export async function editReply(
domainId: string, drid: ObjectID, content: string,
domainId: string, drid: ObjectID, content: string, uid: number, ip: string,
): Promise<DiscussionReplyDoc | null> {
return document.set(domainId, document.TYPE_DISCUSSION_REPLY, drid, { content });
await coll.insertOne({
domainId, docId: drid, content, uid, ip, time: new Date(),
});
return document.set(domainId, document.TYPE_DISCUSSION_REPLY, drid, { content, edited: true, editor: uid });
}
export async function delReply(domainId: string, drid: ObjectID) {
@@ -130,6 +157,7 @@ export async function delReply(domainId: string, drid: ObjectID) {
return await Promise.all([
document.deleteOne(domainId, document.TYPE_DISCUSSION_REPLY, drid),
document.inc(domainId, document.TYPE_DISCUSSION, drdoc.parentId, 'nReply', -1),
coll.deleteMany({ domainId, docId: { $in: [drid, ...(drdoc.reply?.map((i) => i._id) || [])] } }),
]);
}
@@ -161,14 +189,20 @@ export async function addTailReply(
domainId: string, drid: ObjectID,
owner: number, content: string, ip: string,
): Promise<[DiscussionReplyDoc, ObjectID]> {
const time = new Date();
const [drdoc, subId] = await document.push(
domainId, document.TYPE_DISCUSSION_REPLY, drid,
'reply', content, owner, { ip },
);
await document.set(
domainId, document.TYPE_DISCUSSION, drdoc.parentId,
{ updateAt: new Date() },
'reply', content, owner, { ip, editor: owner },
);
await Promise.all([
coll.insertOne({
domainId, docId: subId, content, uid: owner, ip, time: new Date(),
}),
document.set(
domainId, document.TYPE_DISCUSSION, drdoc.parentId,
{ updateAt: time },
),
]);
return [drdoc, subId];
}
@@ -178,14 +212,33 @@ export function getTailReply(
return document.getSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid);
}
export function editTailReply(
domainId: string, drid: ObjectID, drrid: ObjectID, content: string,
export async function editTailReply(
domainId: string, drid: ObjectID, drrid: ObjectID, content: string, uid: number, ip: string,
): Promise<DiscussionTailReplyDoc> {
return document.setSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid, { content });
const [, drrdoc] = await Promise.all([
coll.insertOne({
domainId, docId: drrid, content, uid, time: new Date(), ip,
}),
document.setSub(domainId, document.TYPE_DISCUSSION_REPLY, drid,
'reply', drrid, { content, edited: true, editor: uid }),
]);
return drrdoc;
}
export async function delTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) {
return document.deleteSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid);
return Promise.all([
document.deleteSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid),
coll.deleteMany({ domainId, docId: drrid }),
]);
}
export function getHistory(
domainId: string, docId: ObjectID, query: FilterQuery<DiscussionHistoryDoc> = {},
projection = HISTORY_PROJECTION_PUBLIC,
) {
return coll.find({ domainId, docId, ...query })
.sort({ time: -1 }).project(buildProjection(projection))
.toArray();
}
export function setStar(domainId: string, did: ObjectID, uid: number, star: boolean) {
@@ -306,9 +359,11 @@ export function apply(ctx: Context) {
}
global.Hydro.model.discussion = {
coll,
typeDisplay,
PROJECTION_LIST,
PROJECTION_PUBLIC,
HISTORY_PROJECTION_PUBLIC,
apply,
add,
@@ -330,6 +385,7 @@ global.Hydro.model.discussion = {
delTailReply,
react,
getReaction,
getHistory,
setStar,
getStatus,
setStatus,
+11 -4
View File
@@ -1,7 +1,7 @@
/* eslint-disable object-curly-newline */
import assert from 'assert';
import {
Cursor, FilterQuery, ObjectID, OnlyFieldsOfType, UpdateQuery,
Cursor, FilterQuery, ObjectID, OnlyFieldsOfType, PushOperator, UpdateQuery,
} from 'mongodb';
import { Context } from '../context';
import {
@@ -15,6 +15,7 @@ import { ArrayKeys, MaybeArray, NumberKeys, Projection } from '../typeutils';
import { buildProjection } from '../utils';
type DocID = ObjectID | string | number;
type NormalArrayKeys<O, P = any> = Exclude<ArrayKeys<O, P>, Symbol>;
export const coll = db.collection('document');
export const collStatus = db.collection('document.status');
@@ -95,12 +96,18 @@ export async function get<K extends keyof DocType>(domainId: string, docType: K,
}
export async function set<K extends keyof DocType>(
domainId: string, docType: K, docId: DocType[K]['docId'], $set?: Partial<DocType[K]>, $unset?: OnlyFieldsOfType<DocType[K], any, true | '' | 1>,
domainId: string,
docType: K,
docId: DocType[K]['docId'],
$set?: Partial<DocType[K]>,
$unset?: OnlyFieldsOfType<DocType[K], any, true | '' | 1>,
$push?: PushOperator<DocType[K]>,
): Promise<DocType[K]> {
await bus.parallel('document/set', domainId, docType, docId, $set, $unset);
const update: UpdateQuery<DocType[K]> = {};
if ($set) update.$set = $set;
if ($unset) update.$unset = $unset;
if ($push) update.$push = $push;
const res = await coll.findOneAndUpdate(
{ domainId, docType, docId },
update,
@@ -234,7 +241,7 @@ export async function getSub<T extends keyof DocType, K extends ArrayKeys<DocTyp
return [doc, null];
}
export async function setSub<T extends keyof DocType, K extends string & ArrayKeys<DocType[T]>>(
export async function setSub<T extends keyof DocType, K extends NormalArrayKeys<DocType[T]>>(
domainId: string, docType: T, docId: DocType[T]['docId'],
key: K, subId: DocType[T][K][0]['_id'], args: Partial<DocType[T][K][0]>,
): Promise<DocType[T]> {
@@ -368,7 +375,7 @@ export async function incStatus<T extends keyof DocStatusType>(
export async function revPushStatus<T extends keyof DocStatusType>(
domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number,
key: string & ArrayKeys<DocStatusType[T]>, value: any, id = '_id',
key: NormalArrayKeys<DocStatusType[T]>, value: any, id = '_id',
): Promise<DocStatusType[T]> {
let res = await collStatus.findOneAndUpdate(
{ domainId, docType, docId, uid, [`${key}.${id}`]: value[id] },