diff --git a/packages/blog/index.ts b/packages/blog/index.ts new file mode 100644 index 000000000..50f0d6377 --- /dev/null +++ b/packages/blog/index.ts @@ -0,0 +1,244 @@ +import { + _, Context, DiscussionNotFoundError, DocumentModel, Filter, + Handler, NumberKeys, ObjectId, OplogModel, paginate, + param, PRIV, Types, UserModel, +} from 'hydrooj'; + +export const TYPE_BLOG: 70 = 70; +export interface BlogDoc { + docType: 70; + docId: ObjectId; + owner: number; + title: string; + content: string; + ip: string; + updateAt: Date; + nReply: number; + views: number; + reply: any[]; + react: Record; +} +declare module 'hydrooj' { + interface Model { + blog: typeof BlogModel; + } + interface DocType { + [TYPE_BLOG]: BlogDoc; + } +} + +export class BlogModel { + static async add( + owner: number, title: string, content: string, ip?: string, + ): Promise { + const payload: Partial = { + content, + owner, + title, + ip, + nReply: 0, + updateAt: new Date(), + views: 0, + }; + const res = await DocumentModel.add( + 'system', payload.content!, payload.owner!, TYPE_BLOG, + null, null, null, _.omit(payload, ['domainId', 'content', 'owner']), + ); + payload.docId = res; + return payload.docId; + } + + static async get(did: ObjectId): Promise { + return await DocumentModel.get('system', TYPE_BLOG, did); + } + + static edit(did: ObjectId, title: string, content: string): Promise { + const payload = { title, content }; + return DocumentModel.set('system', TYPE_BLOG, did, payload); + } + + static inc(did: ObjectId, key: NumberKeys, value: number): Promise { + return DocumentModel.inc('system', TYPE_BLOG, did, key, value); + } + + static del(did: ObjectId): Promise { + return Promise.all([ + DocumentModel.deleteOne('system', TYPE_BLOG, did), + DocumentModel.deleteMultiStatus('system', TYPE_BLOG, { docId: did }), + ]) as any; + } + + static count(query: Filter) { + return DocumentModel.count('system', TYPE_BLOG, query); + } + + static getMulti(query: Filter = {}) { + return DocumentModel.getMulti('system', TYPE_BLOG, query) + .sort({ _id: -1 }); + } + + static async addReply(did: ObjectId, owner: number, content: string, ip: string): Promise { + const [[, drid]] = await Promise.all([ + DocumentModel.push('system', TYPE_BLOG, did, 'reply', content, owner, { ip }), + DocumentModel.incAndSet('system', TYPE_BLOG, did, 'nReply', 1, { updateAt: new Date() }), + ]); + return drid; + } + + static setStar(did: ObjectId, uid: number, star: boolean) { + return DocumentModel.setStatus('system', TYPE_BLOG, did, uid, { star }); + } + + static getStatus(did: ObjectId, uid: number) { + return DocumentModel.getStatus('system', TYPE_BLOG, did, uid); + } + + static setStatus(did: ObjectId, uid: number, $set) { + return DocumentModel.setStatus('system', TYPE_BLOG, did, uid, $set); + } +} + +global.Hydro.model.blog = BlogModel; + +class BlogHandler extends Handler { + ddoc?: BlogDoc; + + @param('did', Types.ObjectId, true) + async _prepare(domainId: string, did: ObjectId) { + if (did) { + this.ddoc = await BlogModel.get(did); + if (!this.ddoc) throw new DiscussionNotFoundError(domainId, did); + } + } +} + +class BlogUserHandler extends BlogHandler { + @param('uid', Types.Int) + @param('page', Types.PositiveInt, true) + async get(domainId: string, uid: number, page = 1) { + const [ddocs, dpcount] = await paginate( + BlogModel.getMulti({ owner: uid }), + page, + 10, + ); + const udoc = await UserModel.getById(domainId, uid); + this.response.template = 'blog_main.html'; + this.response.body = { + ddocs, + dpcount, + udoc, + page, + }; + } +} + +class BlogDetailHandler extends BlogHandler { + @param('did', Types.ObjectId) + async get(domainId: string, did: ObjectId) { + const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE) + ? await BlogModel.getStatus(did, this.user._id) + : null; + const udoc = await UserModel.getById(domainId, this.ddoc!.owner); + if (!dsdoc?.view) { + await Promise.all([ + BlogModel.inc(did, 'views', 1), + BlogModel.setStatus(did, this.user._id, { view: true }), + ]); + } + this.response.template = 'blog_detail.html'; + this.response.body = { + ddoc: this.ddoc, dsdoc, udoc, + }; + } + + async post() { + this.checkPriv(PRIV.PRIV_USER_PROFILE); + } + + @param('did', Types.ObjectId) + async postStar(domainId: string, did: ObjectId) { + await BlogModel.setStar(did, this.user._id, true); + this.back({ star: true }); + } + + @param('did', Types.ObjectId) + async postUnstar(domainId: string, did: ObjectId) { + await BlogModel.setStar(did, this.user._id, false); + this.back({ star: false }); + } +} + +class BlogEditHandler extends BlogHandler { + async get() { + this.response.template = 'blog_edit.html'; + this.response.body = { ddoc: this.ddoc }; + } + + @param('title', Types.Title) + @param('content', Types.Content) + async postCreate(domainId: string, title: string, content: string) { + await this.limitRate('add_blog', 3600, 60); + const did = await BlogModel.add(this.user._id, title, content, this.request.ip); + this.response.body = { did }; + this.response.redirect = this.url('blog_detail', { uid: this.user._id, did }); + } + + @param('did', Types.ObjectId) + @param('title', Types.Title) + @param('content', Types.Content) + async postUpdate(domainId: string, did: ObjectId, title: string, content: string) { + if (!this.user.own(this.ddoc!)) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); + await Promise.all([ + BlogModel.edit(did, title, content), + OplogModel.log(this, 'blog.edit', this.ddoc), + ]); + this.response.body = { did }; + this.response.redirect = this.url('blog_detail', { uid: this.user._id, did }); + } + + @param('did', Types.ObjectId) + async postDelete(domainId: string, did: ObjectId) { + if (!this.user.own(this.ddoc!)) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); + await Promise.all([ + BlogModel.del(did), + OplogModel.log(this, 'blog.delete', this.ddoc), + ]); + this.response.redirect = this.url('blog_main', { uid: this.ddoc!.owner }); + } +} + +export async function apply(ctx: Context) { + ctx.Route('blog_main', '/blog/:uid', BlogUserHandler); + ctx.Route('blog_create', '/blog/:uid/create', BlogEditHandler, PRIV.PRIV_USER_PROFILE); + ctx.Route('blog_detail', '/blog/:uid/:did', BlogDetailHandler); + ctx.Route('blog_edit', '/blog/:uid/:did/edit', BlogEditHandler, PRIV.PRIV_USER_PROFILE); + ctx.inject('UserDropdown', 'blog_main', { + icon: 'book', displayName: 'Blog', args: (h) => ({ uid: h.user._id }), + }, PRIV.PRIV_USER_PROFILE); + ctx.i18n.load('zh', { + "{0}'s blog": '{0} 的博客', + Blog: '博客', + blog_detail: '博客详情', + blog_edit: '编辑博客', + blog_main: '博客', + }); + ctx.i18n.load('zh_TW', { + "{0}'s blog": '{0} 的部落格', + Blog: '部落格', + blog_detail: '部落格詳情', + blog_edit: '編輯部落格', + blog_main: '部落格', + }); + ctx.i18n.load('kr', { + "{0}'s blog": '{0}의 블로그', + Blog: '블로그', + blog_main: '블로그', + blog_detail: '블로그 상세', + blog_edit: '블로그 수정', + }); + ctx.i18n.load('en', { + blog_main: 'Blog', + blog_detail: 'Blog Detail', + blog_edit: 'Edit Blog', + }); +} diff --git a/packages/blog/package.json b/packages/blog/package.json new file mode 100644 index 000000000..50d81c0fd --- /dev/null +++ b/packages/blog/package.json @@ -0,0 +1,5 @@ +{ + "name": "@hydrooj/blog", + "version": "0.0.1", + "license": "AGPL-3.0-or-later" +} diff --git a/packages/ui-default/templates/blog_detail.html b/packages/blog/templates/blog_detail.html similarity index 100% rename from packages/ui-default/templates/blog_detail.html rename to packages/blog/templates/blog_detail.html diff --git a/packages/ui-default/templates/blog_edit.html b/packages/blog/templates/blog_edit.html similarity index 100% rename from packages/ui-default/templates/blog_edit.html rename to packages/blog/templates/blog_edit.html diff --git a/packages/ui-default/templates/blog_main.html b/packages/blog/templates/blog_main.html similarity index 100% rename from packages/ui-default/templates/blog_main.html rename to packages/blog/templates/blog_main.html diff --git a/packages/hydrooj/locales/ko.yaml b/packages/hydrooj/locales/ko.yaml index 45ddf47d9..988e69b7b 100644 --- a/packages/hydrooj/locales/ko.yaml +++ b/packages/hydrooj/locales/ko.yaml @@ -84,7 +84,6 @@ Begin Time: 시작 시각 Belongs to: 소유 Bio Visibility: 소개 공개 Bio: 소개 -Blog: Blog Bold: 굵게 Boom!: 펑! Browser: 브라우저 diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 395591c2f..36935acf9 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -97,10 +97,6 @@ Begin Time: 开始时间 Belongs to: 属于 Bio Visibility: 个人简介可见性 Bio: 个人简介 -blog_detail: 博客详情 -blog_edit: 编辑博客 -blog_main: 博客 -Blog: 博客 Bold: 加粗 Boom!: 炸了! Browser: 浏览器 diff --git a/packages/hydrooj/locales/zh_TW.yaml b/packages/hydrooj/locales/zh_TW.yaml index 7ac227eb7..51b604ef2 100644 --- a/packages/hydrooj/locales/zh_TW.yaml +++ b/packages/hydrooj/locales/zh_TW.yaml @@ -50,7 +50,6 @@ Begin Time: 開始時間 Belongs to: 屬於 Bio Visibility: 個人簡介可見性 Bio: 個人簡介 -Blog: 部落格 Boom!: 炸了! Browser: 瀏覽器 Built-in: 內建 diff --git a/packages/hydrooj/src/handler/blog.ts b/packages/hydrooj/src/handler/blog.ts deleted file mode 100644 index 7f1b93283..000000000 --- a/packages/hydrooj/src/handler/blog.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { ObjectId } from 'mongodb'; -import { DiscussionNotFoundError } from '../error'; -import { BlogDoc } from '../interface'; -import paginate from '../lib/paginate'; -import * as blog from '../model/blog'; -import { PRIV } from '../model/builtin'; -import * as oplog from '../model/oplog'; -import * as system from '../model/system'; -import user from '../model/user'; -import { Handler, param, Types } from '../service/server'; - -class BlogHandler extends Handler { - ddoc?: BlogDoc; - - @param('did', Types.ObjectId, true) - async _prepare(domainId: string, did: ObjectId) { - if (did) { - this.ddoc = await blog.get(did); - if (!this.ddoc) throw new DiscussionNotFoundError(domainId, did); - } - } -} - -class BlogUserHandler extends BlogHandler { - @param('uid', Types.Int) - @param('page', Types.PositiveInt, true) - async get(domainId: string, uid: number, page = 1) { - const [ddocs, dpcount] = await paginate( - blog.getMulti({ owner: uid }), - page, - 10, - ); - const udoc = await user.getById(domainId, uid); - this.response.template = 'blog_main.html'; - this.response.body = { - ddocs, - dpcount, - udoc, - page, - }; - } -} - -class BlogDetailHandler extends BlogHandler { - @param('did', Types.ObjectId) - async get(domainId: string, did: ObjectId) { - const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE) - ? await blog.getStatus(did, this.user._id) - : null; - const udoc = await user.getById(domainId, this.ddoc.owner); - if (!dsdoc?.view) { - await Promise.all([ - blog.inc(did, 'views', 1), - blog.setStatus(did, this.user._id, { view: true }), - ]); - } - this.response.template = 'blog_detail.html'; - this.response.body = { - ddoc: this.ddoc, dsdoc, udoc, - }; - } - - async post() { - this.checkPriv(PRIV.PRIV_USER_PROFILE); - } - - @param('did', Types.ObjectId) - async postStar(domainId: string, did: ObjectId) { - await blog.setStar(did, this.user._id, true); - this.back({ star: true }); - } - - @param('did', Types.ObjectId) - async postUnstar(domainId: string, did: ObjectId) { - await blog.setStar(did, this.user._id, false); - this.back({ star: false }); - } -} - -class BlogEditHandler extends BlogHandler { - async get() { - this.response.template = 'blog_edit.html'; - this.response.body = { ddoc: this.ddoc }; - } - - @param('title', Types.Title) - @param('content', Types.Content) - async postCreate(domainId: string, title: string, content: string) { - await this.limitRate('add_blog', 3600, 60); - const did = await blog.add(this.user._id, title, content, this.request.ip); - this.response.body = { did }; - this.response.redirect = this.url('blog_detail', { uid: this.user._id, did }); - } - - @param('did', Types.ObjectId) - @param('title', Types.Title) - @param('content', Types.Content) - async postUpdate(domainId: string, did: ObjectId, title: string, content: string) { - if (!this.user.own(this.ddoc)) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); - await Promise.all([ - blog.edit(did, title, content), - oplog.log(this, 'blog.edit', this.ddoc), - ]); - this.response.body = { did }; - this.response.redirect = this.url('blog_detail', { uid: this.user._id, did }); - } - - @param('did', Types.ObjectId) - async postDelete(domainId: string, did: ObjectId) { - if (!this.user.own(this.ddoc)) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); - await Promise.all([ - blog.del(did), - oplog.log(this, 'blog.delete', this.ddoc), - ]); - this.response.redirect = this.url('blog_main', { uid: this.ddoc.owner }); - } -} - -export async function apply(ctx) { - if (!system.get('server.blog')) return; - ctx.Route('blog_main', '/blog/:uid', BlogUserHandler); - ctx.Route('blog_create', '/blog/:uid/create', BlogEditHandler, PRIV.PRIV_USER_PROFILE); - ctx.Route('blog_detail', '/blog/:uid/:did', BlogDetailHandler); - ctx.Route('blog_edit', '/blog/:uid/:did/edit', BlogEditHandler, PRIV.PRIV_USER_PROFILE); -} diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index d1d0a57f4..e1d0d880a 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -486,20 +486,6 @@ export interface DiscussionTailReplyDoc { editor?: number; } -export interface BlogDoc { - docType: document['TYPE_BLOG']; - docId: ObjectId; - owner: number; - title: string; - content: string; - ip: string; - updateAt: Date; - nReply: number; - views: number; - reply: any[]; - react: Record; -} - export interface TokenDoc { _id: string, tokenType: number, @@ -677,7 +663,6 @@ declare module './service/db' { export interface Model { blacklist: typeof import('./model/blacklist').default, - blog: typeof import('./model/blog'), builtin: typeof import('./model/builtin'), contest: typeof import('./model/contest'), discussion: typeof import('./model/discussion'), diff --git a/packages/hydrooj/src/model/blog.ts b/packages/hydrooj/src/model/blog.ts deleted file mode 100644 index 477ca797d..000000000 --- a/packages/hydrooj/src/model/blog.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { omit } from 'lodash'; -import { Filter, ObjectId } from 'mongodb'; -import { BlogDoc } from '../interface'; -import { NumberKeys } from '../typeutils'; -import * as document from './document'; - -export async function add( - owner: number, title: string, content: string, - ip: string | null = null, -): Promise { - const payload: Partial = { - content, - owner, - title, - ip, - nReply: 0, - updateAt: new Date(), - views: 0, - }; - const res = await document.add( - 'system', payload.content!, payload.owner!, document.TYPE_BLOG, - null, null, null, omit(payload, ['domainId', 'content', 'owner']), - ); - payload.docId = res; - return payload.docId; -} - -export async function get(did: ObjectId): Promise { - return await document.get('system', document.TYPE_BLOG, did); -} - -export function edit(did: ObjectId, title: string, content: string): Promise { - const payload = { title, content }; - return document.set('system', document.TYPE_BLOG, did, payload); -} - -export function inc(did: ObjectId, key: NumberKeys, value: number): Promise { - return document.inc('system', document.TYPE_BLOG, did, key, value); -} - -export function del(did: ObjectId): Promise { - return Promise.all([ - document.deleteOne('system', document.TYPE_BLOG, did), - document.deleteMultiStatus('system', document.TYPE_BLOG, { docId: did }), - ]) as any; -} - -export function count(query: Filter) { - return document.count('system', document.TYPE_BLOG, query); -} - -export function getMulti(query: Filter = {}) { - return document.getMulti('system', document.TYPE_BLOG, query) - .sort({ _id: -1 }); -} - -export async function addReply(did: ObjectId, owner: number, content: string, ip: string): Promise { - const [[, drid]] = await Promise.all([ - document.push('system', document.TYPE_BLOG, did, 'reply', content, owner, { ip }), - document.incAndSet('system', document.TYPE_BLOG, did, 'nReply', 1, { updateAt: new Date() }), - ]); - return drid; -} - -export function setStar(did: ObjectId, uid: number, star: boolean) { - return document.setStatus('system', document.TYPE_BLOG, did, uid, { star }); -} - -export function getStatus(did: ObjectId, uid: number) { - return document.getStatus('system', document.TYPE_BLOG, did, uid); -} - -export function setStatus(did: ObjectId, uid: number, $set) { - return document.setStatus('system', document.TYPE_BLOG, did, uid, $set); -} - -global.Hydro.model.blog = { - add, - get, - inc, - edit, - del, - count, - getMulti, - addReply, - setStar, - getStatus, - setStatus, -}; diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index 72eac4279..9987308e1 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -5,7 +5,7 @@ import { } from 'mongodb'; import { Context } from '../context'; import { - BlogDoc, Content, DiscussionDoc, + Content, DiscussionDoc, DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc, Tdoc, TrainingDoc, } from '../interface'; @@ -30,7 +30,6 @@ export const TYPE_CONTEST: 30 = 30; export const TYPE_TRAINING: 40 = 40; /** @deprecated use `TYPE_CONTEST` with rule `homework` instead. */ export const TYPE_HOMEWORK: 60 = 60; -export const TYPE_BLOG: 70 = 70; export interface DocType { [TYPE_PROBLEM]: ProblemDoc; @@ -41,7 +40,6 @@ export interface DocType { [TYPE_DISCUSSION_REPLY]: DiscussionReplyDoc; [TYPE_CONTEST]: Tdoc; [TYPE_TRAINING]: TrainingDoc; - [TYPE_BLOG]: BlogDoc; } export interface DocStatusType { @@ -499,5 +497,4 @@ global.Hydro.model.document = { TYPE_PROBLEM_LIST, TYPE_PROBLEM_SOLUTION, TYPE_TRAINING, - TYPE_BLOG, }; diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index affbdf47b..4d51f8c28 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -254,7 +254,6 @@ SystemSetting( Setting('setting_server', 'server.language', 'zh_CN', langRange, 'server.language', 'Default display language'), Setting('setting_server', 'server.login', true, 'boolean', 'server.login', 'Allow builtin-login', FLAG_PRO), Setting('setting_server', 'server.message', true, 'boolean', 'server.message', 'Allow users send messages'), - Setting('setting_server', 'server.blog', true, 'boolean', 'server.blog', 'Allow users post blog'), Setting('setting_server', 'server.checkUpdate', true, 'boolean', 'server.checkUpdate', 'Daily update check'), Setting('setting_server', 'server.ignoreUA', ignoreUA, 'textarea', 'server.ignoreUA', 'ignoredUA'), ServerLangSettingNode, diff --git a/packages/hydrooj/src/plugin-api.ts b/packages/hydrooj/src/plugin-api.ts index cefc15f8f..26289ead7 100644 --- a/packages/hydrooj/src/plugin-api.ts +++ b/packages/hydrooj/src/plugin-api.ts @@ -12,7 +12,6 @@ export * as SystemModel from './model/system'; export * as TrainingModel from './model/training'; export * as OpcountModel from './model/opcount'; export * as OplogModel from './model/oplog'; -export * as BlogModel from './model/blog'; export * as SettingModel from './model/setting'; export * as DiscussionModel from './model/discussion'; export * as DocumentModel from './model/document'; diff --git a/packages/ui-default/locales/ko.yaml b/packages/ui-default/locales/ko.yaml index a480d1651..2e8758716 100644 --- a/packages/ui-default/locales/ko.yaml +++ b/packages/ui-default/locales/ko.yaml @@ -87,7 +87,6 @@ Begin Time: 시작 시각 Belongs to: 소유 Bio Visibility: 소개 공개 Bio: 소개 -Blog: Blog Bold: 굵게 Boom!: 펑! Browser: 브라우저 diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 85fc01d7d..460def7a0 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -41,7 +41,6 @@ __langname: 简体中文 'Solved {0} problems, RP: {1} (No. {2})': '解决了 {0} 道题目,RP: {1} (No. {2})' 'The value `{1}` of {0} already exists.': '{0} 的值 `{1}` 已经存在。' 'Yes': 是 -"{0}'s blog": "{0} 的博客" "Effects only when Difficulty is not 'Use algorithm calculated'.": 仅当难度不为“使用算法计算”时才起效。 "Split by ', '.": 由“, ”或“,”分隔。 "What's file?": 什么是文件? @@ -112,7 +111,6 @@ Begin Time: 开始时间 Belongs to: 属于 Bio Visibility: 个人简介可见性 Bio: 个人简介 -Blog: 博客 Bold: 加粗 Boom!: 炸了! Browser: 浏览器 diff --git a/packages/ui-default/locales/zh_TW.yaml b/packages/ui-default/locales/zh_TW.yaml index b83bbd57c..d9ce6715b 100644 --- a/packages/ui-default/locales/zh_TW.yaml +++ b/packages/ui-default/locales/zh_TW.yaml @@ -52,7 +52,6 @@ Begin Time: 開始時間 Belongs to: 屬於 Bio Visibility: 個人簡介可見性 Bio: 個人簡介 -Blog: 部落格 Boom!: 炸了! Browser: 瀏覽器 Built-in: 內建 diff --git a/packages/ui-default/templates/partials/nav.html b/packages/ui-default/templates/partials/nav.html index 3344ce2c9..d01beb901 100644 --- a/packages/ui-default/templates/partials/nav.html +++ b/packages/ui-default/templates/partials/nav.html @@ -79,18 +79,11 @@ {% endif %} - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) and model.system.get('server.blog') %} - - {% endif %} {% if ui.getNodes('UserDropdown').length %} {%- for item in ui.getNodes('UserDropdown') -%}