mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
添加addon-ts.md,添加defaultUdoc
This commit is contained in:
1 parent
8b956575ee
commit
d458c231a4
5 files changed
+230
-23
No files matched your search
@@ -0,0 +1,208 @@
|
||||
# Hydro 附加组件开发
|
||||
|
||||
前置条件:NodeJS>10.10
|
||||
此教程将以编写剪贴板插件为例进行说明。
|
||||
|
||||
## Step1 初始化项目
|
||||
|
||||
在一个空文件夹中运行 `yarn init` 并按照提示填写相关信息。
|
||||
|
||||
```sh
|
||||
/workspace/hydro-plugin $ yarn init
|
||||
yarn init v1.22.4
|
||||
question name (hydro-plugin): @hydrooj/pastebin
|
||||
question version (1.0.0): 0.0.1
|
||||
question description: HydroOJ的剪贴板组件
|
||||
question entry point (index.js): package.json
|
||||
question repository url: https://github.com/hydro-dev/pastebin.git
|
||||
question author: undefined <masnn0@outlook.com>
|
||||
question license (MIT): MIT
|
||||
question private:
|
||||
success Saved package.json
|
||||
```
|
||||
|
||||
## Step2 准备编写组件
|
||||
|
||||
分析:剪贴板组件需要以下功能:
|
||||
|
||||
- 与数据库交互来存储/检索相应文档。
|
||||
- 提供 /paste/create 路由以创建新文档。
|
||||
- 提供 /paste/show/:ID 来查看已创建的文档。
|
||||
- 根据用户ID进行鉴权,允许将文档设置为私密以防止他人查看。
|
||||
|
||||
Hydro的推荐架构如下:
|
||||
|
||||
- handler.ts: 用于处理路由
|
||||
- model.ts: 数据库模型
|
||||
- lib.ts: 不依赖于数据库等的库(如md5函数)
|
||||
- script.ts: 可能会被用户多次使用到的脚本(如重新计算rp)
|
||||
- locale/: 翻译文件
|
||||
- template/: 页面模板
|
||||
- setting.yaml: 模块所用到的设置,格式在下方说明
|
||||
|
||||
## Step3 tsconfig.json
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"composite": true,
|
||||
"strictBindCallApply": true,
|
||||
"experimentalDecorators": true,
|
||||
"outDir": ".",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"*.ts"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
```
|
||||
|
||||
## Step3 model.js
|
||||
|
||||
提示:若不便于使用 import 引入 Hydro 的文件,可以从 global.Hydro 中取出需要的模块。
|
||||
|
||||
```ts
|
||||
import 'hydrooj';
|
||||
import * as db from 'hydrooj/dist/service/db';
|
||||
|
||||
const coll = db.collection('paste');
|
||||
|
||||
interface Paste {
|
||||
_id: string,
|
||||
owner: number,
|
||||
content: string,
|
||||
isPrivate: boolean,
|
||||
}
|
||||
|
||||
declare module 'hydrooj' {
|
||||
interface Collections {
|
||||
paste: Paste,
|
||||
}
|
||||
}
|
||||
|
||||
export async function add(userId: number, content: string, isPrivate: boolean): Promise<string> {
|
||||
const pasteId = String.random(16); // Hydro提供了此方法,创建一个长度为16的随机字符串
|
||||
// 使用 mongodb 为数据库驱动,相关操作参照其文档
|
||||
const result = await coll.insertOne({
|
||||
_id: pasteId,
|
||||
owner: userId,
|
||||
content,
|
||||
isPrivate,
|
||||
});
|
||||
return result.insertedId; // 返回插入的文档ID
|
||||
}
|
||||
|
||||
export async function get(pasteId: string): Promise<Paste> {
|
||||
return await coll.findOne({ _id: pasteId });
|
||||
}
|
||||
|
||||
// 暴露这些接口
|
||||
global.Hydro.model.pastebin = { add, get };
|
||||
|
||||
```
|
||||
|
||||
## Step4 handler.js
|
||||
|
||||
在路由中定义所有的函数应均为异步函数,支持的函数有:prepare, get, post, post[Operation], cleanup
|
||||
具体流程如下:
|
||||
|
||||
```
|
||||
先执行 prepare(args) (如果存在)
|
||||
args 为传入的参数集合(包括 QueryString, Body, Path)中的全部参数,
|
||||
再执行 prepare(args) (如果存在)
|
||||
检查请求类型:
|
||||
|
||||
为 GET ?
|
||||
-> 执行 get(args)
|
||||
为 POST ?
|
||||
-> 执行 post(args)
|
||||
-> 含有 operation 字段?
|
||||
-> 执行 post[Operation]
|
||||
```
|
||||
|
||||
执行 cleanup()
|
||||
|
||||
如果在 this.response.template 指定模板则渲染,否则直接返回 this.response.body 中的内容。
|
||||
|
||||
* 在表单提交时的 operation 字段使用下划线,函数名使用驼峰命名。
|
||||
|
||||
如 `<input type="hidden" name="operation" value="confirm_delete">` 对应 `postConfirmDelete` 函数。
|
||||
|
||||
应当提供 `apply` 函数,并与定义的 Handler 一同挂载到 `global.Hydro.handler[模块名]` 位置。
|
||||
`apply` 函数将在初始化阶段被调用。
|
||||
|
||||
```ts
|
||||
import { Route, Handler } from 'hydrooj/dist/service/server';
|
||||
import { PRIV } from 'hydrooj/dist/model/builtin'; // 内置 Privilege 权限节点
|
||||
import { isContent } from 'hydrooj/dist/lib/validator'; // 用于检查用户输入是否合法
|
||||
import { NotFoundError } from 'hydrooj/dist/error';
|
||||
import * as pastebin from './pastebin'; // 刚刚编写的pastebin模型
|
||||
|
||||
// 创建新路由
|
||||
class PasteCreateHandler extends Handler {
|
||||
// Get请求时触发该函数
|
||||
async get() {
|
||||
// 检查用户是否登录,此处为多余(因为底部注册路由时已声明所需权限)
|
||||
// 此方法适用于权限的动态检查
|
||||
// this.checkPriv(PRIV.PRIV_USER_PROFILE);
|
||||
this.response.template = 'paste_create.html'; // 返回此页面
|
||||
}
|
||||
|
||||
// 使用 isContent 检查输入
|
||||
@param('content', Types.String, isContent)
|
||||
@param('private', Types.Boolean)
|
||||
// 从用户提交的表单中取出content和private字段
|
||||
// domainId 为固定传入参数
|
||||
async post(domainId: string, content: string, private = false) {
|
||||
// 在HTML表单提交的多选框中,选中值为 'on',未选中则为空,需要进行转换
|
||||
await pastebin.add(this.user._id, content, !!private);
|
||||
// 将用户重定向到创建完成的url
|
||||
this.response.redirect = this.url('paste_show', { id: pasteid });
|
||||
}
|
||||
}
|
||||
|
||||
class PasteShowHandler extends Handler {
|
||||
@param('id', Types.String)
|
||||
async get(domainId: string, id: string) {
|
||||
const doc = await pastebin.get(id);
|
||||
if (!doc) throw new NotFoundError(id);
|
||||
if (doc.isPrivate && this.user._id !== doc.owner) {
|
||||
throw new PermissionError();
|
||||
}
|
||||
this.response.body = { doc };
|
||||
this.response.template = 'paste_show.html';
|
||||
}
|
||||
|
||||
@param('id', Types.String)
|
||||
async postDelete(domainId: string, id: string){
|
||||
// 当提交表单并存在 operation 值为 delete 时执行。
|
||||
// 本例中未实现删除功能,仅作为说明。
|
||||
}
|
||||
}
|
||||
|
||||
// Hydro会在服务初始化完成后调用该函数。
|
||||
export async function apply(){
|
||||
// 注册一个名为 paste_create 的路由,匹配 '/paste/create',
|
||||
// 使用PasteCreateHandler处理,访问改路由需要PRIV.PRIV_USER_PROFILE权限
|
||||
// 提示:路由匹配基于 path-to-regexp
|
||||
Route('paste_create', '/paste/create', PasteCreateHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Route('paste_show', '/paste/show/:id', PasteShowHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.pastebin = apply;
|
||||
```
|
||||
|
||||
## Step5 template
|
||||
|
||||
TODO
|
||||
|
||||
## Step6 Locale
|
||||
|
||||
用于提供多国翻译。格式与 Hydro 的 locale 文件夹格式相同。
|
||||
@@ -166,26 +166,6 @@ async function apply(){
|
||||
global.Hydro.handler.pastebin = apply;
|
||||
```
|
||||
|
||||
对于 Typescript 用户的额外说明:Hydro为Typescript提供了表单验证API。
|
||||
`@param` 会修改 arguments,首个参数为请求所在的 domainId,剩余参数为指定的内容。
|
||||
使用如下:
|
||||
|
||||
```ts
|
||||
const { Handler, Route, Types, param } = global.Hydro.service.server;
|
||||
const { user } = global.Hydro.model;
|
||||
const { isPassword } = global.Hydro.lib.validator;
|
||||
|
||||
class UserLoginHandler extends Handler {
|
||||
@param('username', Types.String)
|
||||
@param('password', Types.String, isPassword)
|
||||
async post(domainId:string, username:string, password:string) {
|
||||
const udoc = await user.getByUname(username);
|
||||
udoc.checkPassword(password);
|
||||
this.response.body = { udoc };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step5 template
|
||||
|
||||
TODO
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "2.13.7",
|
||||
"version": "2.13.8",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "dist/loader.js",
|
||||
"typings": "dist/loader.d.ts",
|
||||
|
||||
@@ -304,10 +304,11 @@ class HomeMessagesHandler extends Handler {
|
||||
async get() {
|
||||
// TODO(iceboy): projection, pagination.
|
||||
const messages = await message.getByUser(this.user._id);
|
||||
const udict = await user.getList('system', [
|
||||
const uids = new Set<number>([
|
||||
...messages.map((mdoc) => mdoc.from),
|
||||
...messages.map((mdoc) => mdoc.to),
|
||||
]);
|
||||
const udict = await user.getList('system', Array.from(uids));
|
||||
// TODO(twd2): improve here:
|
||||
const parsed = {};
|
||||
for (const m of messages) {
|
||||
|
||||
@@ -22,6 +22,23 @@ export async function setPassword(uid: number, password: string): Promise<Udoc>
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export const defaultUser: Udoc = {
|
||||
_id: 0,
|
||||
uname: 'Unknown User',
|
||||
unameLower: 'unknown user',
|
||||
gravatar: 'unknown@hydro.local',
|
||||
mail: 'unknown@hydro.local',
|
||||
mailLower: 'unknown@hydro.local',
|
||||
salt: '',
|
||||
hash: '',
|
||||
hashType: 'hydro',
|
||||
priv: 0,
|
||||
regat: new Date('2000-01-01'),
|
||||
loginat: new Date('2000-01-01'),
|
||||
regip: '127.0.0.1',
|
||||
loginip: '127.0.0.1',
|
||||
};
|
||||
|
||||
export class User implements _User {
|
||||
udoc: () => any;
|
||||
|
||||
@@ -125,7 +142,7 @@ export async function getList(domainId: string, uids: number[]): Promise<Udict>
|
||||
const _uids = new Set(uids);
|
||||
const r = {};
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
for (const uid of _uids) r[uid] = await getById(domainId, uid);
|
||||
for (const uid of _uids) r[uid] = (await getById(domainId, uid)) || defaultUser;
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -252,6 +269,7 @@ function ensureIndexes() {
|
||||
bus.once('app/started', ensureIndexes);
|
||||
global.Hydro.model.user = {
|
||||
User,
|
||||
defaultUser,
|
||||
create,
|
||||
getByEmail,
|
||||
getById,
|
||||
|
||||
Reference in new issue
Block a user