mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
文件结构更改
This commit is contained in:
1 parent
b3af28dc43
commit
1f1d12ad73
109 files changed
+544
-3296
No files matched your search
@@ -0,0 +1 @@
|
||||
这是Hydro的核心文件。
|
||||
@@ -0,0 +1,7 @@
|
||||
@requireCsrfToken
|
||||
discussion前端权限节点显示异常?
|
||||
举报功能
|
||||
SETTINGS_PRIVACY.allowRegisteredUsers
|
||||
model.setting.ui.name
|
||||
pdoc.secretConfig?
|
||||
message.markAsRead
|
||||
@@ -0,0 +1,93 @@
|
||||
__langname: English
|
||||
Can be Extended For: Extension
|
||||
contest_create: Contest Create
|
||||
contest_detail_problem_submit: Submit Contest Problem
|
||||
contest_edit: Contest Edit
|
||||
contest_main: Contest
|
||||
contest_scoreboard: Scoreboard
|
||||
discussion_create: Create Discussion
|
||||
discussion_edit: Discussion Edit
|
||||
discussion_main: Discussion
|
||||
discussion_node: Discussion
|
||||
display_name: Display Name
|
||||
domain_dashboard: Dashboard
|
||||
domain_discussion: Discussion Nodes
|
||||
domain_edit: Edit Info
|
||||
domain_join_applications: Join Applications
|
||||
domain_main: Main
|
||||
domain_permission: System Permission
|
||||
domain_role: System Role
|
||||
domain_user: System User
|
||||
fs_upload: File Upload
|
||||
home_account: Account Settings
|
||||
home_domain_account: Profile @ Domain
|
||||
home_domain_create: Create Domain
|
||||
home_domain: My Domains
|
||||
home_file: My Files
|
||||
home_messages: Messages
|
||||
home_preference: Preference
|
||||
home_security: Security
|
||||
homepage: Home
|
||||
homework_create: Homework Create
|
||||
homework_detail_problem_submit: Submit Homework Problem
|
||||
homework_edit: Homework Edit
|
||||
homework_main: Homework
|
||||
homework_scoreboard: Scoreboard
|
||||
judge_playground: Judge Playground
|
||||
main: Home
|
||||
manage_module: Module Management
|
||||
manage_script: Scripts
|
||||
manage_setting: System Settings
|
||||
manage: System Manage
|
||||
no_translation_warn: <blockquote class="warn">This part of content is under translation.</blockquote>
|
||||
page.problem_detail.sidebar.show_category: Click to Show
|
||||
page.training_detail.invalid_when_not_enrolled: You cannot view problem details unless enrolled.
|
||||
pager_first: « First
|
||||
pager_last: Last »
|
||||
pager_next: Next ›
|
||||
pager_previous: ‹ Previous
|
||||
perm_contest: Contests
|
||||
perm_discussion: Discussions
|
||||
perm_general: General
|
||||
perm_problem_solution: Problem Solutions
|
||||
perm_problem: Problems
|
||||
perm_record: Records
|
||||
perm_training: Trainings
|
||||
problem_create: Problem Create
|
||||
problem_detail: Problem Detail
|
||||
problem_edit: Problem Edit
|
||||
problem_import: Import Problem
|
||||
problem_main: Problem Set
|
||||
problem_settings: Problem Settings
|
||||
problem_solution: Problem Solution
|
||||
problem_statistics: Problem Statistics
|
||||
problem_submit: Problem Submit
|
||||
problem-category-delim: '|'
|
||||
problem-subcategory-delim: ', '
|
||||
problem.import.additional_file: '## Additional Files'
|
||||
problem.import.hint: '## Hint'
|
||||
problem.import.input_format: '## Input Format'
|
||||
problem.import.limit_and_hint: '## Limits'
|
||||
problem.import.output_format: '## Output Format'
|
||||
problem.import.problem_description: '## Description'
|
||||
ranking: Ranking
|
||||
record_detail: Record Detail
|
||||
record_main: Judging Queue
|
||||
setting_customize: Customize
|
||||
setting_display: Display Preference
|
||||
setting_function: Function
|
||||
setting_info_domain: Personal Info
|
||||
setting_info: Personal Info
|
||||
setting_preference: Preference
|
||||
setting_privacy: Privacy
|
||||
setting_usage: Usage Preference
|
||||
timeago_locale: en_US
|
||||
training_create: Training Create
|
||||
training_edit: Training Edit
|
||||
training_main: Training
|
||||
user_detail: User Detail
|
||||
user_login: Login
|
||||
user_logout: Logout
|
||||
user_lostpass: Lost Password
|
||||
user_register: Register
|
||||
wiki_help: Help
|
||||
@@ -0,0 +1,803 @@
|
||||
__langname: 简体中文
|
||||
'"{data}" copied to clipboard!': “{data}”已复制到剪贴板!
|
||||
'{} is a moderator of this domain.': '{}是这个域的管理者之一。'
|
||||
'{0} comments': '{0} 条评论'
|
||||
'{0} discussions': '{0} 条讨论'
|
||||
'{0} limit exceeded (limit: {2} operations in {1} seconds).': '{0} 超过频率限制。限制:{1} 秒内最多 {2} 次操作。'
|
||||
'{0} problems': '{0} 道题'
|
||||
'{0} sections': '{0} 小节'
|
||||
'{0} solutions': '{0} 条题解'
|
||||
'{0} views': '{0} 次查看'
|
||||
'{0}: Permissions': '{0}: 权限'
|
||||
'{0}: Roles': '{0}: 角色'
|
||||
'{0}: Users': '{0}: 用户'
|
||||
'# My Submissions': '我的递交数'
|
||||
'# Submissions': '递交数'
|
||||
'↓ # Comments': '↓ 评论数'
|
||||
'↓ # Enrollees': '↓ 参与人数'
|
||||
'↓ # Likes': '↓ 赞数'
|
||||
'↓ # Stars': '↓ 收藏数'
|
||||
'↓ Create Time': '↓ 创建时间'
|
||||
'↓ Update Time': '↓ 更新时间'
|
||||
'A domain ID cannot be changed after creation. It is supposed to be:': '创建后将无法更改 ID。ID 必须满足以下条件:'
|
||||
'Copy "{data}" failed :(': '复制“{data}”失败 :('
|
||||
'Copy failed :(': '复制失败 :('
|
||||
'Current dataset: {0}': '当前测试数据: {0}'
|
||||
'Discussion: Terms Of Service': 讨论区服务条款
|
||||
'Format: category 1, sub category 1.1, sub category 1.2, ..., sub category 1.x, ..., category n, sub category n.1, sub category n.2, ..., sub category n.m, ...': 格式:分类1, 子分类1.1, 子分类1.2, ..., 子分类1.x, ..., 分类n, 子分类n.1, 子分类n.2, ..., 子分类n.m, ...
|
||||
'Hello, {0}! You can click following link to active your new email of your Hydro account:': 您好,{0}!您可以点击以下链接来激活您 Hydro 账户的新电子邮件地址:
|
||||
'Hello, {0}! You can click following link to reset the password of your Hydro account:': 您好,{0}!您可以点击以下链接来重置您 Hydro 账户的密码:
|
||||
'Hello! You can click following link to sign up your Hydro account:': 您好!您可以点击以下链接来注册您的 Hydro 账户:
|
||||
'Join {0}': '加入 {0}'
|
||||
'No': 否
|
||||
'Note: Problem title may not be hidden.': 注意:题目标题可能不会被隐藏。
|
||||
'Role {1} already exists in domain {0}.': '域 {0} 中已存在角色 {1}。'
|
||||
'Solved {0} problems, RP: {1} (No. {2})': '解决了 {0} 道题目,RP: {1} (No. {2})'
|
||||
'The value `{1}` of {0} already exists.': '{0} 的值 `{1}` 已经存在。'
|
||||
'We use <a href="https://en.gravatar.com/" target="_blank">Gravatar</a> to present your avatar icon.': '我们使用 <a href="https://cn.gravatar.com/" target="_blank">Gravatar</a> 服务来呈现您的头像。'
|
||||
'Yes': 是
|
||||
"Effects only when Difficulty is not 'Use algorithm calculated'.": 仅当难度不为“使用算法计算”时才起效。
|
||||
"Splitted by ', '.": 由“, ”或“,”分隔。
|
||||
"What's file?": 什么是文件?
|
||||
(All Problems): (所有题目)
|
||||
(None): (无)
|
||||
(Not changed): 未更改
|
||||
Aborted: 已放弃
|
||||
About Hydro: 关于 Hydro
|
||||
About Markdown: 关于 Markdown
|
||||
About test data: 关于测试数据
|
||||
about: 关于
|
||||
About: 关于
|
||||
AC Message: 通过消息
|
||||
Accepted Ratio: 通过率
|
||||
Accepted: 已通过
|
||||
Access Control: 访问控制
|
||||
Action: 动作
|
||||
Active Sessions: 活动会话
|
||||
Add module: 添加模块
|
||||
Add new data: 添加新数据
|
||||
Add User: 添加用户
|
||||
Add: 添加
|
||||
All {0} Contests: 所有 {0} 比赛
|
||||
All Contests: 所有比赛
|
||||
All Homeworks: 所有作业
|
||||
All Submissions: 所有递交
|
||||
All Training Plans: 所有训练计划
|
||||
All: 全部
|
||||
Always granted all privileges: 总是拥有所有权限
|
||||
An error has occurred.: 发生了一个错误。
|
||||
An example of dataset: 测试数据集的一个例子
|
||||
Any user is allowed to join this domain with an invitation code: 任意用户都可以通过邀请码加入此域
|
||||
Any user is allowed to join this domain: 任意用户都可以加入此域
|
||||
API: API
|
||||
Argument {0} is invalid.: 非法的参数 {0} 。
|
||||
Arguments: 参数
|
||||
At least 4 characters: 至少 4 个字符
|
||||
Attend Contest: 参加比赛
|
||||
Attend contests: 参加比赛
|
||||
Attended: 已参加
|
||||
author: 作者
|
||||
Be Copied: 被复制
|
||||
Begin at: 开始于
|
||||
Begin Date: 开始日期
|
||||
Begin Time: 开始时间
|
||||
Belongs to: 属于
|
||||
Bio Visibility: 个人简介可见性
|
||||
Bio: 个人简介
|
||||
Blog: 博客
|
||||
Bold: 加粗
|
||||
Boom!: 炸了!
|
||||
Browser: 浏览器
|
||||
Built-in: 内置
|
||||
Bulletin: 公告
|
||||
By clicking the button, you will become a member of the domain {0}.: 点击按钮,您将成为域 {0} 的成员。
|
||||
By Contest: 由比赛
|
||||
By Problem: 由题目
|
||||
By signing up a Hydro universal account, you can submit code and join discussions in all online judging services provided by us.: 注册一个 Hydro 通用账户,您就可以在我们提供的所有在线评测服务上提交代码、参与讨论。
|
||||
By Username / UID: 由用户名或 UID
|
||||
Calendar View: 日历视图
|
||||
Calendar: 日历
|
||||
Can be Extended For: 可延期
|
||||
Cancel Score: 取消成绩
|
||||
Cancel: 取消
|
||||
Categories: 分类
|
||||
Category: 分类
|
||||
Change Email: 修改电子邮件
|
||||
Change Password: 修改密码
|
||||
Chinese: 中文
|
||||
Choose the background image in your profile page.: 选择您资料页面的背景图片。
|
||||
Claim homework: 认领作业
|
||||
Claim Homework: 认领作业
|
||||
Claimed: 已认领
|
||||
Clear Marks: 清除标记
|
||||
Click here to chat with me: 点击这里与我聊天
|
||||
click to add: 点击添加
|
||||
CLOSE: 关闭
|
||||
Code blocks: 代码区块
|
||||
Code copied to clipboard!: 代码已复制到剪贴板!
|
||||
Code language: 代码语言
|
||||
Code: 代码
|
||||
collapse: 收缩
|
||||
Comment: 评论
|
||||
Comments: 评论
|
||||
CommonMark Syntax: CommonMark 语法
|
||||
Compile Error: 编译错误
|
||||
Compilers' Version and Parameters: 编译器版本及参数
|
||||
Complete: 完成
|
||||
Completed: 已完成
|
||||
Confirm deleting the selected roles?: 您确定删除所选角色吗?
|
||||
Confirm deleting this comment? Its replies will be deleted as well.: 确认删除这个评论吗?回复会被同时删除。
|
||||
Confirm deleting this reply?: 确认删除这个回复吗?
|
||||
Confirm removing the selected users?: 您确定将所选用户移除吗?
|
||||
Confirmation mail has been sent to your new email.: 确认邮件已经发送到您的新电子邮箱。
|
||||
Congratulations! Your submission is accepted.: 恭喜!您的递交已评测通过。
|
||||
Contact Us: 联系我们
|
||||
Contact: 联系
|
||||
content: 内容
|
||||
Content: 内容
|
||||
Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。
|
||||
Contest Scoreboard: 比赛成绩表
|
||||
contest_create: 创建比赛
|
||||
contest_detail_problem_submit: 递交比赛题目
|
||||
contest_edit: 编辑比赛
|
||||
contest_main: 比赛
|
||||
contest_scoreboard: 比赛成绩表
|
||||
Contest: 比赛
|
||||
Continue: 继续
|
||||
Contributed Problems: 贡献的题目
|
||||
Contributions: 贡献
|
||||
Copy Email: 复制电子邮件
|
||||
Copy from: 复制自
|
||||
Copy QQ Number: 复制QQ号
|
||||
Copy WeChat Account: 复制微信号
|
||||
Copy: 复制
|
||||
Copyright Complaint: 版权申诉
|
||||
CPU Info: CPU信息
|
||||
Create a contest: 创建一个比赛
|
||||
Create a Discussion: 创建一个讨论
|
||||
Create a problem: 创建一个题目
|
||||
Create Contest: 创建比赛
|
||||
Create contests: 创建比赛
|
||||
Create Discussion in {0}: 在 {0} 下创建讨论
|
||||
Create Discussion: 创建讨论
|
||||
Create discussions: 创建讨论
|
||||
Create Domain: 创建域
|
||||
Create homework: 创建作业
|
||||
Create Homework: 创建作业
|
||||
Create Link: 创建链接
|
||||
Create problem solutions: 创建题解
|
||||
Create Problem: 创建题目
|
||||
Create problems: 创建题目
|
||||
Create Role: 创建角色
|
||||
Create Training Plan: 创建训练计划
|
||||
Create training plans: 创建训练计划
|
||||
Create: 创建
|
||||
Created By: 创建人
|
||||
Current dataset comes from: 当前数据集来自
|
||||
current domain: 当前域
|
||||
Current Email: 当前电子邮件
|
||||
Current Location: 当前位置
|
||||
Current password doesn't match.: 当前密码输入错误。
|
||||
Current Password: 当前密码
|
||||
Current Status: 当前状态
|
||||
currently offline: 目前离线
|
||||
Data of Problem {0} not found.: 题目 {0} 的数据缺失。
|
||||
Data of problem {1} not found.: 题目 {1} 的数据未找到。
|
||||
Data of record {0} not found.: 记录 {0} 的数据未找到。
|
||||
Dataset Format: 测试数据集格式
|
||||
Dataset: 测试数据
|
||||
Day: 日
|
||||
Deadline: 截止时间
|
||||
Default Code Language: 默认代码语言
|
||||
Default Code Template: 默认代码模板
|
||||
Delete discussion replies of own discussion: 删除自己讨论中的回复
|
||||
Delete discussion replies: 删除讨论回复
|
||||
Delete discussions: 删除讨论
|
||||
Delete own discussion replies: 删除讨论中自己的回复
|
||||
Delete own discussions: 删除自己的讨论
|
||||
Delete own problem solution replies: 删除题解中自己的回复
|
||||
Delete own problem solutions: 删除自己的题解
|
||||
Delete problem solution replies: 删除题解的回复
|
||||
Delete problem solutions: 删除题解
|
||||
Delete Selected Roles: 删除所选角色
|
||||
delete: 删除
|
||||
Delete: 删除
|
||||
Description: 说明
|
||||
Destination Domain: 目标域
|
||||
Development: 开发
|
||||
Difficulty Algorithm: 难度算法
|
||||
Difficulty by admin: 管理员设定的难度
|
||||
Difficulty by algorithm: 算法计算的难度
|
||||
Difficulty displayed: 显示的难度
|
||||
Difficulty: 难度
|
||||
Disabled: 关闭
|
||||
Disconnected from the server. Reconnecting...: 到服务器的连接断开。正在重新连接...
|
||||
Discussion {1} not found.: 讨论 {1} 不存在。
|
||||
Discussion category {1} already exists.: 讨论分类 {1} 已经存在。
|
||||
Discussion category {1} not found.: 讨论分类 {1} 不存在。
|
||||
Discussion node {1} already exists.: 讨论节点 {1} 已经存在。
|
||||
Discussion node {1} not found.: 讨论节点 {1} 不存在。
|
||||
Discussion Nodes: 讨论节点
|
||||
discussion_create: 创建讨论
|
||||
discussion_edit: 编辑讨论
|
||||
discussion_main: 讨论
|
||||
discussion_node: 讨论
|
||||
Discussion: 讨论
|
||||
Discussions: 讨论
|
||||
Display name {1} you want to set is used by others.: 您想要设置的显示名 {1} 已经被其他人使用了。
|
||||
Display Name: 显示名
|
||||
display_name: 显示名
|
||||
Do not discuss solutions otherwise you will be regarded as cheating and then lose qualifications.: 请勿讨论解法,否则以作弊论处并且将失去参赛资格。
|
||||
Document {2} not found.: 文档 {2} 不存在。
|
||||
Domain {0} already exists.: 域 {0} 已经存在。
|
||||
Domain {0} is bulit-in and cannot be modified.: 域 {0} 为内置,不可修改。
|
||||
Domain {0} not found.: 域 {0} 不存在。
|
||||
Domain ID cannot be changed once the domain is created.: 在创建后不能更改 ID。
|
||||
Domain ID: 域 ID
|
||||
domain_dashboard: 管理域
|
||||
domain_discussion: 讨论节点
|
||||
domain_edit: 编辑域资料
|
||||
domain_join: 加入域
|
||||
domain_main: 首页
|
||||
domain_permission: 管理权限
|
||||
domain_role: 管理角色
|
||||
domain_user: 管理用户
|
||||
domain: 域
|
||||
Domain: 域
|
||||
Don't have an account?: 还没有账户?
|
||||
Done: 已结束
|
||||
Download Dataset: 下载数据集
|
||||
Download: 下载
|
||||
Downvote: 差评
|
||||
Duration (hours): 持续时间 (小时)
|
||||
Duration: 持续时间
|
||||
Edit any contests: 修改任意的比赛
|
||||
Edit any homework: 修改任意的作业
|
||||
Edit Contest: 编辑比赛
|
||||
Edit description of this domain: 修改此域描述
|
||||
Edit discussion replies of own discussion: 修改自己讨论中的回复
|
||||
Edit discussion replies: 修改讨论的回复
|
||||
Edit discussions: 修改讨论
|
||||
Edit Domain: 编辑域
|
||||
Edit Homework: 编辑作业
|
||||
Edit own contests: 修改自己的比赛
|
||||
Edit own discussion replies: 修改讨论中自己的回复
|
||||
Edit own discussions: 修改自己的讨论
|
||||
Edit own homework: 修改自己的作业
|
||||
Edit own problem solution replies: 修改题解中自己的回复
|
||||
Edit own problem solutions: 修改自己的题解
|
||||
Edit own problems: 修改自己的题目
|
||||
Edit own training plans: 修改自己的训练计划
|
||||
Edit permissions of a role: 修改角色权限
|
||||
Edit problem solution replies: 修改题解的回复
|
||||
Edit problem solutions: 修改题解
|
||||
Edit Problem: 编辑题目
|
||||
Edit problems: 修改题目
|
||||
Edit Profile: 编辑个人信息
|
||||
Edit training plans: 修改训练计划
|
||||
Edit: 编辑
|
||||
Email Visibility: Email 可见性
|
||||
Email: 电子邮件
|
||||
Enabled: 开启
|
||||
End at: 结束于
|
||||
End Date: 结束日期
|
||||
End Time: 结束时间
|
||||
English: 英文
|
||||
Enroll Training: 参加训练
|
||||
Enrolled: 已参加
|
||||
Enrollees: 参加人数
|
||||
error: 错误
|
||||
Evaluated difficulty: 估计的难度
|
||||
expand: 展开
|
||||
Expire: 过期
|
||||
Export All Code: 导出所有代码
|
||||
Export as CSV: 导出为 CSV
|
||||
Export as HTML: 导出为 HTML
|
||||
Extension (days): 最长延期 (日)
|
||||
Extension Score Penalty: 延期递交扣分规则
|
||||
Failed to join the domain. You are already a member.: 加入域失败,您已是该域的成员。
|
||||
Feedback: 反馈
|
||||
Field {0} or {1} validation failed.: 字段 {0} 或 {1} 验证失败。
|
||||
Field {0} validation failed.: 字段 {0} 验证失败。
|
||||
Filter: 过滤
|
||||
Footer: 页脚
|
||||
Forgot Password and/or Username: 忘记密码和/或用户名
|
||||
Forgot password or username?: 忘记密码或者用户名?
|
||||
Formula blocks: 公式区块
|
||||
fs_upload: 上传文件
|
||||
Gender Visibility: 性别可见性
|
||||
Gender: 性别
|
||||
Gravatar Email: Gravatar Email 地址
|
||||
Hard Deadline: 最终截止时间
|
||||
Hash: 散列
|
||||
Have ALL PERMISSIONS in this domain: 在此域中拥有全部权限
|
||||
He: 他
|
||||
Help: 帮助
|
||||
Hidden: 隐藏
|
||||
Hide all tags: 隐藏所有标签
|
||||
Hide categorical tags: 隐藏分类标签
|
||||
Highlight discussions: 高亮讨论
|
||||
Highlight: 高亮讨论
|
||||
Hint: 提示
|
||||
home_account: 账户设置
|
||||
home_domain_account: 当前域的设置
|
||||
home_domain_create: 创建域
|
||||
home_domain: 我的域
|
||||
home_file: 我的文件
|
||||
home_messages: 站内消息
|
||||
home_preference: 偏好设置
|
||||
home_security: 安全设置
|
||||
homepage: 首页
|
||||
Homework Introduction: 作业介绍
|
||||
Homework scoreboard is not visible.: 目前无法查看作业成绩表。
|
||||
homework_create: 创建作业
|
||||
homework_detail_problem_submit: 递交作业题目
|
||||
homework_edit: 编辑作业
|
||||
homework_main: 作业
|
||||
homework_scoreboard: 作业成绩表
|
||||
homework_status: 作业成绩表
|
||||
Homework: 作业
|
||||
Host: 主持人
|
||||
hour(s): 小时
|
||||
If enabled, source code will be emailed to you after the submission is accepted.: 如果启用,在您通过题目后,源代码会以 Email 的形式发送给您。
|
||||
If left blank, the built-in template of the corresponding language will be used.: 若留空,则将使用对应语言的内置代码模板。
|
||||
Images: 图片
|
||||
Import from: 导入自
|
||||
Import problem from syzoj.: 从运行 syzoj 系统的 OJ 导入题目。
|
||||
Import Problem: 导入题目
|
||||
Import: 导入
|
||||
In 1 day: 一天后
|
||||
In 1 month: 一个月后
|
||||
In 1 week: 一周后
|
||||
In 3 days: 三天后
|
||||
In 3 hours: 三个小时后
|
||||
In following contests: 在下列比赛中
|
||||
In following homework: 在以下作业中
|
||||
In following training plans: 在下列训练计划中
|
||||
In Progress: 进行中
|
||||
Information: 信息
|
||||
Initialize: 初始化
|
||||
Inline codes: 行内代码
|
||||
Inline formulas: 行内公式
|
||||
Insert Code: 插入代码
|
||||
Insert Image: 插入图像
|
||||
Installed Modules: 已安装的模块
|
||||
Instead of copying the test data directly, the test data of the copied problems will be linked to the test data of the source problems (called the source test data). Thus, the copied problems can observe the changes in the source test data. The permissions of the test data of the copied problems follow the source test data, e.g., you still might not download them but the judges can. By uploading some new test data, the link will be broken and the new test data will be used.: 虽然测试数据不会被复制,但是系统会将题目的测试数据链接到原题。因此在原题的数据被改动的时候,复制后的题目的数据也会同时改动。链接后的测试数据的权限以原题为准,比如您可能依旧无法下载数据,但是评测机可以。在题目设置页面中可以通过上传新的测试数据的方式,这个数据链接会被删除,以后将使用您的新测试数据。
|
||||
Introduce must not exceed 500 characters and it will be shown in the list view.: 简介不能超过 500 个字符,将显示在列表页面中。
|
||||
Introduce: 简介
|
||||
Invalid password for user {0}.: 用户 {0} 的密码错误。
|
||||
Invalid: 无效
|
||||
Invitation Code: 邀请码
|
||||
IO: 输入输出
|
||||
Is rated: 计入排名
|
||||
It works!: 它成了!
|
||||
Italic: 斜体
|
||||
Join: 加入
|
||||
Judge Status: 评测状态
|
||||
judge_playground: 评测游乐场
|
||||
Judged At: 评测时间
|
||||
Judged By: 评测机
|
||||
Judging Queue: 评测队列
|
||||
Keep current expiration: 保持当前过期设置
|
||||
Language: 语言
|
||||
last active at: 最后活动于
|
||||
last login at: 最后登录于
|
||||
Last Submit At: 最后递交于
|
||||
Last Update At: 最后活动时间
|
||||
LaTeX Expressions: LaTeX 表达式
|
||||
Learn More: 了解更多
|
||||
Leave blank to use numeric pid.: 留空来使用数字题号。
|
||||
Limitations: 限制
|
||||
Links: 链接
|
||||
List View: 列表视图
|
||||
Live: 正在进行
|
||||
Live...: 正在进行…
|
||||
Location: 位置
|
||||
Login to Attend Contest: 登录后参加比赛
|
||||
Login to Claim Homework: 登录后认领作业
|
||||
Login to Create a Discussion: 登录后创建讨论
|
||||
Login to Submit: 登录后递交
|
||||
Login with Github: 使用 Github 登录
|
||||
Login with Google: 使用 Google 登录
|
||||
Login: 登录
|
||||
Logout All Sessions: 注销所有会话
|
||||
Logout This Session: 注销该会话
|
||||
Logout: 登出
|
||||
Lost Password: 忘记密码
|
||||
Lucky: 手气不错
|
||||
manage_dashboard: 控制面板
|
||||
manage_join_applications: 加域申请
|
||||
manage_module: 管理模块
|
||||
manage_script: 脚本管理
|
||||
manage_setting: 系统设置
|
||||
manage: 管理
|
||||
Manage: 管理
|
||||
Manager: 管理
|
||||
Mark Green: 标绿
|
||||
Mark Yellow: 标黄
|
||||
Memory Cost: 内存占用
|
||||
Memory Info: 内存信息
|
||||
Memory: 内存
|
||||
Message {0} not found.: 消息 {0} 不存在。
|
||||
Messages: 消息
|
||||
Method: 方法
|
||||
Month: 月
|
||||
Monthly Popular: 月度最受欢迎
|
||||
Most Upvoted Solutions: 最被赞同的题解
|
||||
My Domains: 我的域
|
||||
My Files: 我的文件
|
||||
My Profile: 我的资料
|
||||
My Recent Submissions: 我的最近递交记录
|
||||
My Role: 我的角色
|
||||
name: 名称
|
||||
Name: 名称
|
||||
Network error: 网络错误
|
||||
Never expire: 从不过期
|
||||
New dataset: 新测试数据
|
||||
New Email: 新电子邮件
|
||||
New file: 新文件
|
||||
New Password: 新密码
|
||||
New Training Plan: 新训练计划
|
||||
New: 创建
|
||||
No comments so far...: 目前还没有评论...
|
||||
No dataset at present.: 暂无测试数据。
|
||||
no discussion yet...: 目前没有讨论…
|
||||
No Permission to Attend Contest: 您没有权限参加比赛
|
||||
No Permission to Claim Homework: 您没有权限认领作业
|
||||
No Permission to Create a Discussion: 您没有权限创建讨论
|
||||
No Permission to Submit: 您没有权限递交
|
||||
No problem.: 没有题目。
|
||||
No solutions so far...: 目前还没有题解...
|
||||
No Submissions: 没有递交
|
||||
No user is allowed to join this domain: 没有用户可以自助加入此域
|
||||
no_translation_warn: ''
|
||||
None: 无
|
||||
Not Attended: 未参加
|
||||
Not available: 不可用
|
||||
Not Claimed: 未认领
|
||||
Not Enrolled: 未参加
|
||||
Not started with a number: 不以数字开头
|
||||
Not wrapped with space: 两侧不含空格
|
||||
Numeric PID: 数字题号
|
||||
Oh, the user doesn't have any contributions!: 啊哦,这个用户还没贡献过题目~
|
||||
Oh, the user hasn't created any discussions yet!: 这个用户还没有发布过讨论
|
||||
Oh, the user hasn't submitted yet!: 这个用户还没有交过题 _(:зゝ∠)_
|
||||
Oh, there is no task in the queue!: 喔,队列中目前没有任务。
|
||||
Oh, there is no task that matches the filter!: 喔,目前没有符合过滤条件的任务。
|
||||
Ok: 确定
|
||||
Only A-Z, a-z, 0-9 and _ are accepted: 只接受 A-Z, a-z, 0-9 和 _
|
||||
Oops, there are no results.: 呀,没有结果。
|
||||
Oops!: Oops!
|
||||
Open in Problem Set: 在题库中打开
|
||||
Open Scratchpad: 进入在线编程模式
|
||||
Open Since: 开始时间
|
||||
Open Source: 开源
|
||||
Open: 开放
|
||||
Operating System: 操作系统
|
||||
Ops: 运维
|
||||
Or, with automatically filled invitation code: 或者,这是可以自动填写邀请码的
|
||||
Ordered List: 有序列表
|
||||
Original Score: 原始分数
|
||||
OS Info: 系统信息
|
||||
Owner: 所有者
|
||||
page.problem_detail.sidebar.show_category: 点击显示
|
||||
page.training_detail.invalid_when_not_enrolled: 未参加训练计划时您不能查看题目详情。
|
||||
pager_first: « 第一页
|
||||
pager_last: 末页 »
|
||||
pager_next: 下一页 ›
|
||||
pager_previous: ‹ 前一页
|
||||
Params: 参数
|
||||
Partic.: 参赛人数
|
||||
Password reset mail has been sent to your email.: 密码重置邮件已发送到您的电子邮箱。
|
||||
Password: 密码
|
||||
Passwords don't match.: 验证密码不匹配。
|
||||
Path {0} not found.: 路径 {0} 不存在。
|
||||
Peak Memory: 峰值内存
|
||||
perm_contest: 比赛
|
||||
perm_discussion: 讨论
|
||||
perm_general: 一般
|
||||
perm_homework: 作业
|
||||
perm_problem_solution: 题解
|
||||
perm_problem: 题库
|
||||
perm_record: 记录
|
||||
perm_training: 训练
|
||||
Permission: 权限
|
||||
Permissions: 权限
|
||||
Pick a problem randomly based on current filter: 根据当前过滤条件随机选择一道题
|
||||
Plan: 计划
|
||||
Please attend contest to see the problems.: 请参加比赛来查看题目。
|
||||
Please claim the assignment to see the problems.: 认领作业后才可以查看作业内容。
|
||||
Please select at least one role to perform this operation.: 请选择至少一个角色来进行操作。
|
||||
Please select at least one user to perform this operation.: 请选择至少一个用户来进行操作。
|
||||
Pretest Data: 自测数据
|
||||
Pretest Your Code: 使用自定义数据测试
|
||||
Pretest: 自测
|
||||
Pretest?: 自测?
|
||||
Privacy: 隐私
|
||||
Problem {1} not found.: 题目 {1} 不存在。
|
||||
Problem Data: 题目数据
|
||||
Problem ID List: 题目 ID 列表
|
||||
Problem Set: 题库
|
||||
Problem Settings: 题目设置
|
||||
Problem Solution List: 题解列表
|
||||
Problem Solution: 题解列表
|
||||
Problem Tags Visibility: 题目标签可见性
|
||||
problem_create: 创建题目
|
||||
problem_detail: 题目详情
|
||||
problem_edit: 编辑题目
|
||||
problem_import: 导入题目
|
||||
problem_main: 题库
|
||||
problem_settings: 题目设置
|
||||
problem_solution: 题解
|
||||
problem_statistics: 题目统计
|
||||
problem_submit: 递交代码
|
||||
problem-category-delim: '|'
|
||||
problem-subcategory-delim: 、
|
||||
Problem: 题目
|
||||
problem.import.additional_file: '## 附加文件'
|
||||
problem.import.hint: '## 提示'
|
||||
problem.import.input_format: '## 输入格式'
|
||||
problem.import.limit_and_hint: '## 限制'
|
||||
problem.import.output_format: '## 输出格式'
|
||||
problem.import.problem_description: '## 题目描述'
|
||||
Problems Sets: 题库
|
||||
Problems: 题目
|
||||
Profile Background Image: 背景图片
|
||||
Progress: 进度
|
||||
Properties: 属性
|
||||
Public: 公开
|
||||
Published at: 发表于
|
||||
QQ Group: QQ 群
|
||||
QQ Visibility: QQ 可见性
|
||||
Quit Scratchpad: 退出在线编程模式
|
||||
Quote: 引用
|
||||
Rank: 排名
|
||||
ranking: 排名
|
||||
Recommended: 推荐
|
||||
Record {0} not found.: 记录 {0} 不存在。
|
||||
record_detail: 记录详情
|
||||
record_main: 评测队列
|
||||
Records: 评测记录
|
||||
Refresh Records: 刷新评测记录
|
||||
Refresh: 刷新
|
||||
Registered at: 注册于
|
||||
Rejudge problems: 重测题目
|
||||
Rejudge records: 重测记录
|
||||
Rejudge: 重测
|
||||
Related Discussions: 相关讨论
|
||||
Related: 相关
|
||||
Remember me: 记住我
|
||||
Remove Selected User: 将所选用户移出
|
||||
Remove this data: 移除这组数据
|
||||
Remove: 移除
|
||||
Repeat Password: 重复密码
|
||||
Reply discussions: 回复讨论
|
||||
Reply problem solutions: 回复题解
|
||||
reply: 回复
|
||||
Reply: 回复
|
||||
Report: 举报
|
||||
required: 必填
|
||||
Reset Password or Find Username: 重置密码或找回用户名
|
||||
Reset Password: 重置密码
|
||||
Reset: 重置
|
||||
Restart: 重新启动
|
||||
Role Assignment: 角色分配
|
||||
Role has been updated to {0} for selected users.: 所选用户角色已更新为 {0}。
|
||||
Role has been updated to {0}.: 角色已更新为 {0}。
|
||||
Role name can only contains numbers, letters and underscores.: 角色名称只能包含数字、字母和下划线。
|
||||
Role: 角色
|
||||
Roles: 角色
|
||||
RP Algorithm: RP 算法
|
||||
Rule: 规则
|
||||
Run Pretest: 运行自测
|
||||
Run: 运行
|
||||
Sample Input: 输入数据
|
||||
Sample Output: 输出数据
|
||||
Save All Changes: 保存所有修改
|
||||
Saved session: 已保存的会话
|
||||
score canceled: 成绩取消
|
||||
Score: 分数
|
||||
Scoreboard (Hidden): 成绩表 (隐藏)
|
||||
Scoreboard: 成绩表
|
||||
Scripts: 脚本
|
||||
Search: 搜索
|
||||
Secret (visible to admins): 保密(对管理员可见)
|
||||
Secret: 保密
|
||||
Section: 章节
|
||||
Security: 安全
|
||||
Select a node to create discussion.: 选择一个节点来发表讨论。
|
||||
Select a role: 选择一个角色
|
||||
Select User: 选择用户
|
||||
Selected roles have been deleted.: 所选角色已删除。
|
||||
Selected users have been removed from the domain.: 所选用户已从此域中移除。
|
||||
Send Code after acceptance: 通过题目后发送源代码
|
||||
Send Message: 发送站内信息
|
||||
Send Password Reset Email: 发送密码重置邮件
|
||||
Send Verification Email: 发送验证邮件
|
||||
Send: 发送
|
||||
Service Status: 服务状态
|
||||
Set Role: 设置角色
|
||||
Set Roles for Selected User: 设置所选用户的角色
|
||||
Set Roles for selected users: 为所选用户设置角色
|
||||
setting_customize: 个性化
|
||||
setting_display: 显示偏好
|
||||
setting_domain: 域设置
|
||||
setting_info_domain: 个人信息
|
||||
setting_info: 个人信息
|
||||
setting_privacy: 隐私
|
||||
setting_usage: 使用偏好
|
||||
Settings: 设置
|
||||
Share: 分享
|
||||
She: 她
|
||||
Show all tags: 显示所有标签
|
||||
Show MOD badge: 展示 MOD 徽章
|
||||
Show: 显示
|
||||
Sign In: 登录
|
||||
SIGN IN: 登录
|
||||
Sign up mail has been sent to your email.: 注册邮件已经发送到您的电子邮箱。
|
||||
Sign Up Now: 现在注册
|
||||
Sign Up: 注册
|
||||
Size: 大小
|
||||
Sorry: 对不起
|
||||
Sort by: 排序
|
||||
Source type: 来源类型
|
||||
Star Topic: 收藏讨论
|
||||
Star: 收藏
|
||||
Start at: 开始于
|
||||
Statistics: 统计
|
||||
Status is hidden: 状态已隐藏
|
||||
Status: 状态
|
||||
Submission Statistics: 递交统计
|
||||
Submission: 递交
|
||||
Submissions: 递交
|
||||
Submit: 递交
|
||||
Submitted: 已递交
|
||||
Support: 支持
|
||||
Tags: 标签
|
||||
Technical Information: 技术信息
|
||||
Temporary session: 临时会话
|
||||
Terms of Service: 服务条款
|
||||
Test data comes from: 测试数据来自
|
||||
Testdata Config: 测试数据配置
|
||||
Text: 文本
|
||||
The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。
|
||||
The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 加入此域的邀请码。您只能使用字母和数字,并且不能长于 64 个字符。
|
||||
The invitation code you provided is invalid.: 您提供的邀请码无效。
|
||||
The link will be expired at {0}: 链接将于 {0} 过期
|
||||
The role to assign when user joining the domain.: 用户加入此域时分配的角色。
|
||||
The uploaded file is too long.: 上传的文件太长。
|
||||
The user is already a member of the domain.: 该用户已是域的成员。
|
||||
The user is too lazy to leave something here...: 该用户太懒,这里啥也没写 (´・ω・`)
|
||||
Their account will not be deleted and they will be with the default role.: 他们的账号不会被删除,并且之后将以 default 角色访问。
|
||||
There are no contests...: 没有比赛…
|
||||
There is no homework so far ╰(*°▽°*)╯: 目前还没有作业 ╰(*°▽°*)╯
|
||||
This contest is not live.: 比赛没有开始。
|
||||
This email was sent by {0} automatically, and please do not reply directly.: 这封邮件由 {0} 自动发送,请勿直接回复。
|
||||
This homework is not open and you cannot view problems.: 该作业还未到开放时间,您无法查看作业题目。
|
||||
This homework is not open.: 该作业还未开放递交。
|
||||
This is the current session: 这是当前的会话
|
||||
This message will be presented to those whose submissions are accepted.: 这条消息将会被展示给通过此题的用户。
|
||||
This page needs JavaScript to work.: 这个页面需要 Javascript。
|
||||
This section cannot be challenged at present, so please complete the following sections first: 该章节目前不可挑战,请先完成以下章节
|
||||
This type of files are not allowed to be uploaded.: 不允许上传这种文件。
|
||||
Time (Seconds): 耗时 (秒)
|
||||
Time Cost: 耗时
|
||||
Time Extension: 延期
|
||||
Time: 时间
|
||||
timeago_locale: zh_CN
|
||||
Timezone: 时区
|
||||
title: 标题
|
||||
Title: 标题
|
||||
Toggle Pretest Panel: 切换自测面板
|
||||
Toggle Preview: 切换预览
|
||||
Toggle Records Panel: 切换评测记录面板
|
||||
Total Score: 总分数
|
||||
Total Time (Seconds): 总耗时 (秒)
|
||||
Total Time: 总耗时
|
||||
Total: 总计
|
||||
Training requirement is not satisfied.: 不满足训练条件。
|
||||
training_create: 创建训练
|
||||
training_edit: 编辑训练
|
||||
training_main: 训练
|
||||
Training: 训练
|
||||
Type: 类型
|
||||
UI Language: 用户界面语言
|
||||
Uncompleted: 未完成
|
||||
Unique: 唯一
|
||||
Unknown field {0}.: 未知字段 {0}。
|
||||
Unknown: 未知
|
||||
Unordered List: 无序列表
|
||||
Unstar Topic: 取消收藏
|
||||
Unstar: 取消收藏
|
||||
Update Permission: 更新权限
|
||||
Update Settings: 更新设置
|
||||
Update: 更新
|
||||
Upload File: 上传文件
|
||||
Upload Problem: 上传题目
|
||||
Upload: 上传
|
||||
Uploaded By: 上传者
|
||||
Upvote: 好评
|
||||
Usage exceeded.: 用量超限。
|
||||
Use admin specificed: 由管理员指定
|
||||
Use algorithm calculated: 使用算法计算
|
||||
Use average of above: 使用上面的平均值
|
||||
User {0} already exists.: 用户 {0} 已存在。
|
||||
User {0} not found.: 用户 {0} 不存在。
|
||||
User can join this domain by visiting the following URL: 用户可以访问此链接来加入此域
|
||||
User ID: 用户 ID
|
||||
User will no longer be allowed to join the domain after expiration.: 过期后,用户将不能加入此域。
|
||||
user_detail: 用户
|
||||
user_login: 登录
|
||||
user_logout: 登出
|
||||
user_lostpass: 忘记密码
|
||||
user_register: 注册
|
||||
User-defined role: 用户定义的角色
|
||||
User: 用户
|
||||
Username / UID: 用户名或 UID
|
||||
Username: 用户名
|
||||
Users with those roles will be removed from the domain.: 拥有这些角色的用户将从此域中移除。
|
||||
Users: 用户
|
||||
Using your Hydro universal account: 使用您的 Hydro 通用账户
|
||||
Valid for registered users who are not members of the domain: 对域外注册用户有效
|
||||
Valid for visitors: 对未登录的游客有效
|
||||
Version: 版本
|
||||
View contest scoreboard: 查看比赛成绩表
|
||||
View Contest: 查看比赛
|
||||
View contests: 查看比赛
|
||||
View Details: 查看详情
|
||||
View discussions: 查看讨论
|
||||
View hidden contest submission status and scoreboard: 查看隐藏的比赛递交状态和成绩表
|
||||
View hidden homework submission status and scoreboard: 查看隐藏的作业递交状态和成绩表
|
||||
View hidden problems: 查看隐藏的题目
|
||||
View homework scoreboard: 查看作业成绩表
|
||||
View homework: 查看作业
|
||||
View Homework: 查看作业
|
||||
View or create discussion: 查看与发表讨论
|
||||
View or submit solution: 查看与发表题解
|
||||
View Owned Problems: 查看自己的题目
|
||||
View Owned Training Plans: 查看自己的训练计划
|
||||
View problem solutions: 查看题解
|
||||
View Problem: 查看题目
|
||||
View problems: 查看题目
|
||||
View this domain: 查看此域
|
||||
View training plans: 查看训练计划
|
||||
View: 查看
|
||||
Visible to registered users: 对注册用户可见
|
||||
Visit: 查看
|
||||
Vote problem solutions: 为题解投票
|
||||
Vote: 投票
|
||||
We will check code similarity after this contest.: 我们会在赛后检查代码相似度。
|
||||
WeChat Visibility: 微信可见性
|
||||
WeChat: 微信
|
||||
Week: 周
|
||||
What is domain?: 什么是域?
|
||||
What is this?: 这是什么?
|
||||
What's domain?: 什么是域?
|
||||
What's script?: 什么是脚本?
|
||||
What's this?: 这是什么?
|
||||
Whether to show tags in the problem list.: 是否在题目列表中显示题目的标签。
|
||||
Will be used as the domain icon.: 将被用作系统图标。
|
||||
With this feature, you can copy problems that you can view from a domain to some other domain. Their title, content, tags and categories will be copied. However, their test data are not copied directly.: 您可以通过这个功能将某域下您能查看的题目复制到其它域中,题目的标题、描述、标签与分类将被复制过来。但是测试数据不会被直接复制。
|
||||
Write Your Comment: 发表您的评论
|
||||
Write Your Solution: 发表您的题解
|
||||
Year: 年
|
||||
You are not allowed to join the domain. The link is either invalid or expired.: 您无法加入该域,链接无效或已过期。
|
||||
You can create your own training plans and share with others.: 您可以创建您自己的训练计划并且与他人分享。
|
||||
You cannot submit for this problem because the contest is ended. You can click "Open in Problem Set" to view this problem in normal mode.: 该比赛已结束,您无法在比赛模式下递交该题目。您可以点击“在题库中打开”以普通模式查看和递交本题。
|
||||
You cannot submit for this problem because the homework's deadline is due.: 作业已超过截止时间,您无法递交本题目。
|
||||
You cannot visit this domain.: 您不能访问此域。
|
||||
You don't have the required permission ({0}) in this domain.: 您在该域中无相应权限 ({0})。
|
||||
You don't have the required privilege.: 您没有相应的特权。
|
||||
You don't own any domain and don't have privilege to create domain.: 您不拥有任何域也没有创建域的特权。
|
||||
You have not submitted any solutions for this problem: 您在该题下没有递交过任何解答
|
||||
You have permissions to create problems for this domain.: 您有权限为此域创建题目。
|
||||
You have uncompleted domain creation operation. You can click "Continue" to finish the creation.: 您有未完成的创建域操作。您可以点击“继续”来完成创建。
|
||||
You haven't attended this contest yet.: 您还没有参加本次比赛。
|
||||
You haven't claimed this homework yet.: 您还未认领过该作业。
|
||||
You need to enter the invitation code to join the domain.: 您需要输入邀请码来加入此域。
|
||||
You're not logged in.: 您没有登录。
|
||||
You've already attended this contest.: 您已经参加本次比赛。
|
||||
You've already claimed this homework.: 您已认领过该作业。
|
||||
You've already voted.: 您已经投过票。
|
||||
Your permission: 您的权限
|
||||
Your role: 您的角色
|
||||
@@ -0,0 +1,567 @@
|
||||
__langname: 正體中文
|
||||
'{0} comments': '{0} 條評論'
|
||||
'{0} discussions': '{0} 條討論'
|
||||
'{0} limit exceeded (limit: {2} operations in {1} seconds).': '{0} 超過頻率限制。限制:{1} 秒內最多 {2} 次操作。'
|
||||
'{0} solutions': '{0} 條題解'
|
||||
'{0} views': '{0} 次檢視'
|
||||
'{0}: Permissions': '{0}: 許可權'
|
||||
'{0}: Roles': '{0}: 角色'
|
||||
'{0}: Users': '{0}: 使用者'
|
||||
'# My Submissions': '我的遞交數'
|
||||
'# Submissions': '遞交數'
|
||||
'↓ # Comments': '↓ 評論數'
|
||||
'↓ # Enrollees': '↓ 參與人數'
|
||||
'↓ # Likes': '↓ 贊數'
|
||||
'↓ # Stars': '↓ 收藏數'
|
||||
'↓ Create Time': '↓ 創建時間'
|
||||
'↓ Update Time': '↓ 更新時間'
|
||||
'A domain ID cannot be changed after creation. It is supposed to be:': '創建後將無法更改 ID。ID 必須滿足以下條件:'
|
||||
'Current dataset: {0}': '當前測試資料: {0}'
|
||||
'Hello, {0}! You can click following link to active your new email of your Hydro account:': 您好,{0}!您可以點選以下連結來啟用您 Hydro 賬戶的新電子郵件地址:
|
||||
'Hello, {0}! You can click following link to reset the password of your Hydro account:': 您好,{0}!您可以點選以下連結來重置您 Hydro 賬戶的密碼:
|
||||
'Hello! You can click following link to sign up your Hydro account:': 您好!您可以點選以下連結來註冊您的 Hydro 賬戶:
|
||||
'No': 否
|
||||
'Solved {0} problems, RP: {1} (No. {2})': '解決了 {0} 道題目,RP: {1} (No. {2})'
|
||||
'Yes': 是
|
||||
(None): (無)
|
||||
About Markdown: 關於 Markdown
|
||||
About: 關於
|
||||
Accepted Ratio: 通過率
|
||||
Accepted: 已通過
|
||||
Access Control: 訪問控制
|
||||
Action: 動作
|
||||
Active Sessions: 活動會話
|
||||
Add new data: 新增新資料
|
||||
Add User: 新增使用者
|
||||
Add: 新增
|
||||
All {0} Contests: 所有 {0} 比賽
|
||||
All Contests: 所有比賽
|
||||
All Homeworks: 所有功課
|
||||
All: 全部
|
||||
Always granted all privileges: 總是擁有所有域許可權
|
||||
An error has occurred.: 發生了一個錯誤。
|
||||
API: API
|
||||
Arguments: 參數
|
||||
At least 4 characters: 至少 4 個字元
|
||||
Attend Contest: 參加比賽
|
||||
Attend contests: 參加比賽
|
||||
Attended: 已參加
|
||||
author: 作者
|
||||
Begin Date: 開始日期
|
||||
Begin Time: 開始時間
|
||||
Belongs to: 屬於
|
||||
Bio Visibility: 個人簡介可見性
|
||||
Bio: 個人簡介
|
||||
Blog: 部落格
|
||||
Boom!: 炸了!
|
||||
Browser: 瀏覽器
|
||||
Built-in: 內建
|
||||
By signing up a Hydro universal account, you can submit code and join discussions in all online judging services provided by us.: 註冊一個 Hydro 通用賬戶,您就可以在我們提供的所有線上評測服務上提交程式碼、參與討論。
|
||||
Calendar View: 日曆視圖
|
||||
Can be Extended For: 可延期
|
||||
Cancel Score: 取消成績
|
||||
Cancel: 取消
|
||||
Category: 分類
|
||||
Change Email: 修改電子郵件
|
||||
Change Password: 修改密碼
|
||||
Claim homework: 認領功課
|
||||
Claim Homework: 認領功課
|
||||
Claimed: 已認領
|
||||
Click here to chat with me: 點選這裡與我聊天
|
||||
CLOSE: 關閉
|
||||
Code blocks: 程式碼區塊
|
||||
Code language: 程式碼語言
|
||||
Code: 程式碼
|
||||
Comment: 評論
|
||||
Comments: 評論
|
||||
CommonMark Syntax: CommonMark 語法
|
||||
Confirm deleting the selected roles?: 您確定刪除所選角色嗎?
|
||||
Confirm removing the selected users?: 您確定將所選使用者移除嗎?
|
||||
Confirmation mail has been sent to your new email.: 確認郵件已經發送到您的新電子郵箱。
|
||||
Contact Us: 聯絡我們
|
||||
Contact: 聯絡
|
||||
content: 內容
|
||||
Content: 內容
|
||||
Contest scoreboard is not visible.: 當前比賽成績表隱藏,暫不可顯示。
|
||||
Contest Scoreboard: 比賽成績表
|
||||
contest_create: 創建比賽
|
||||
contest_detail_problem_submit: 遞交比賽程式碼
|
||||
contest_edit: 編輯比賽
|
||||
contest_main: 比賽
|
||||
contest_scoreboard: 比賽成績表
|
||||
Contest: 比賽
|
||||
Create a contest: 創建一個比賽
|
||||
Create a Discussion: 創建一個討論
|
||||
Create a problem: 創建一個題目
|
||||
Create Contest: 創建比賽
|
||||
Create contests: 創建比賽
|
||||
Create Discussion in {0}: 在 {0} 下創建討論
|
||||
Create Discussion: 創建討論
|
||||
Create discussions: 創建討論
|
||||
Create Domain: 創建域
|
||||
Create homework: 創建功課
|
||||
Create Homework: 創建功課
|
||||
Create problem solutions: 創建題解
|
||||
Create Problem: 創建題目
|
||||
Create problems: 創建題目
|
||||
Create Role: 創建角色
|
||||
Create Training Plan: 創建訓練計劃
|
||||
Create: 創建
|
||||
current domain: 當前域
|
||||
Current Email: 當前電子郵件
|
||||
Current Location: 當前位置
|
||||
Current password doesn't match.: 當前密碼輸入錯誤。
|
||||
Current Password: 當前密碼
|
||||
currently offline: 目前離線
|
||||
Data of Problem {0} not found.: 題目 {0} 的資料缺失。
|
||||
Dataset: 測試資料
|
||||
Deadline: 截止時間
|
||||
Default Code Language: 預設程式碼語言
|
||||
Default Code Template: 預設程式碼模板
|
||||
Delete discussion replies of own discussion: 刪除自己討論中的回覆
|
||||
Delete discussion replies: 刪除討論回覆
|
||||
Delete discussions: 刪除討論
|
||||
Delete own discussion replies: 刪除討論中自己的回覆
|
||||
Delete own discussions: 刪除自己的討論
|
||||
Delete own problem solution replies: 刪除題解中自己的回覆
|
||||
Delete own problem solutions: 刪除自己的題解
|
||||
Delete problem solution replies: 刪除題解的回覆
|
||||
Delete problem solutions: 刪除題解
|
||||
Delete Selected Roles: 刪除所選角色
|
||||
delete: 刪除
|
||||
Delete: 刪除
|
||||
Description: 說明
|
||||
Development: 開發
|
||||
Difficulty: 難度
|
||||
Disabled: 關閉
|
||||
Discussion {1} not found.: 討論 {1} 不存在。
|
||||
Discussion category {1} already exists.: 討論分類 {1} 已經存在。
|
||||
Discussion category {1} not found.: 討論分類 {1} 不存在。
|
||||
Discussion node {1} already exists.: 討論節點 {1} 已經存在。
|
||||
Discussion node {1} not found.: 討論節點 {1} 不存在。
|
||||
Discussion Nodes: 討論節點
|
||||
discussion_create: 創建討論
|
||||
discussion_edit: 編輯討論
|
||||
discussion_main: 討論
|
||||
discussion_node: 討論
|
||||
Discussions: 討論
|
||||
Do not discuss solutions otherwise you will be regarded as cheating and then lose qualifications.: 請勿討論解法,否則以作弊論處並且將失去參賽資格。
|
||||
Document {2} not found.: 文件 {2} 不存在。
|
||||
Domain {0} already exists.: 域 {0} 已經存在。
|
||||
Domain {0} is bulit-in and cannot be modified.: 域 {0} 為內建,不可修改。
|
||||
Domain {0} not found.: 域 {0} 不存在。
|
||||
Domain ID cannot be changed once the domain is created.: 在創建後不能更改 ID。
|
||||
domain_main: 首頁
|
||||
Don't have an account?: 還沒有賬戶?
|
||||
Done: 已結束
|
||||
Download: 下載
|
||||
Duration (hours): 持續時間 (小時)
|
||||
Duration: 持續時間
|
||||
Edit any homework: 修改任意功課
|
||||
Edit description of this domain: 修改此域描述
|
||||
Edit discussion replies of own discussion: 修改自己討論中的回覆
|
||||
Edit discussion replies: 修改討論的回覆
|
||||
Edit discussions: 修改討論
|
||||
Edit Domain: 編輯域
|
||||
Edit Homework: 編輯功課
|
||||
Edit own discussion replies: 修改討論中自己的回覆
|
||||
Edit own discussions: 修改自己的討論
|
||||
Edit own homework: 修改自己的功課
|
||||
Edit own problem solution replies: 修改題解中自己的回覆
|
||||
Edit own problem solutions: 修改自己的題解
|
||||
Edit own problems: 修改自己的題目
|
||||
Edit permissions of a role: 修改角色許可權
|
||||
Edit problem solution replies: 修改題解的回覆
|
||||
Edit problem solutions: 修改題解
|
||||
Edit Problem: 編輯題目
|
||||
Edit problems: 修改題目
|
||||
Edit: 編輯
|
||||
Email Visibility: Email 可見性
|
||||
Email: 電子郵件
|
||||
Enabled: 開啟
|
||||
End at: 結束於
|
||||
End Date: 結束日期
|
||||
End Time: 結束時間
|
||||
Enrolled: 已參加
|
||||
error: 錯誤
|
||||
Export All Code: 導出所有原始碼
|
||||
Extension (days): 最長延期(日)
|
||||
Extension Score Penalty: 延期遞交扣分規則
|
||||
Field {0} or {1} validation failed.: 欄位 {0} 或 {1} 驗證失敗。
|
||||
Field {0} validation failed.: 欄位 {0} 驗證失敗。
|
||||
Footer: 頁尾
|
||||
Forgot password or username?: 忘記密碼或者使用者名?
|
||||
Formula blocks: 公式區塊
|
||||
Gender Visibility: 性別可見性
|
||||
Gender: 性別
|
||||
Gravatar Email: Gravatar Email 地址
|
||||
Hard Deadline: 最終截止時間
|
||||
Have ALL PERMISSIONS in this domain: 在此域中擁有全部許可權
|
||||
Help: 幫助
|
||||
Hidden: 隱藏
|
||||
Hide all tags: 隱藏所有標籤
|
||||
Hide categorical tags: 隱藏分類標籤
|
||||
Highlight discussions: 高亮討論
|
||||
Highlight: 高亮討論
|
||||
home_account: 賬戶設定
|
||||
home_domain_account: 當前域的設定
|
||||
home_domain_create: 創建域
|
||||
home_domain: 我的域
|
||||
home_messages: 站內訊息
|
||||
home_preference: 偏好設定
|
||||
home_security: 安全設定
|
||||
Homework Introduction: 功課介紹
|
||||
Homework scoreboard is not visible.: 目前無法查看功課成績表。
|
||||
homework_create: 創建功課
|
||||
homework_edit: 編輯功課
|
||||
homework_main: 功課
|
||||
homework_scoreboard: 成績表
|
||||
Homework: 功課
|
||||
Host: 主持人
|
||||
hour(s): 小時
|
||||
If enabled, source code will be emailed to you after the submission is accepted.: 如果啟用,在您通過題目後,原始碼會以 Email 的形式傳送給您。
|
||||
If left blank, the built-in template of the corresponding language will be used.: 若留空,則將使用對應語言的內建程式碼模板。
|
||||
Images: 圖片
|
||||
In following homework: 在以下功課中
|
||||
In following training plans: 在下列訓練計劃中
|
||||
Information: 資訊
|
||||
Inline codes: 行內程式碼
|
||||
Inline formulas: 行內公式
|
||||
Invalid user {0} or password.: 使用者 {0} 不存在或密碼錯誤。
|
||||
It works!: 它成了!
|
||||
judge_playground: 評測遊樂場
|
||||
Judged At: 評測時間
|
||||
Judging Queue: 評測佇列
|
||||
Lang.: 語言
|
||||
Language: 語言
|
||||
last active at: 最後活動於
|
||||
last login at: 最後登入於
|
||||
Last Submit At: 最後遞交於
|
||||
Last Update At: 最後活動時間
|
||||
LaTeX Expressions: LaTeX 表示式
|
||||
Learn More: 瞭解更多
|
||||
Links: 連結
|
||||
List View: 列表視圖
|
||||
Live: 正在進行
|
||||
Live...: 正在進行…
|
||||
Location: 位置
|
||||
Login to Attend Contest: 登入後參加比賽
|
||||
Login to Claim Homework: 登錄以認領功課
|
||||
Login to Create a Discussion: 登入後創建討論
|
||||
Login to Submit: 登入後遞交
|
||||
Login: 登入
|
||||
Logout All Sessions: 登出所有會話
|
||||
Logout This Session: 登出該會話
|
||||
Logout: 登出
|
||||
Lost Password: 忘記密碼
|
||||
manage_dashboard: 概況
|
||||
manage_domain: 管理域
|
||||
manage_edit: 編輯域資料
|
||||
manage_join_applications: 加域申請
|
||||
manage_permission: 管理許可權
|
||||
manage_role: 管理角色
|
||||
manage_user: 管理使用者
|
||||
manage: 管理
|
||||
Manage: 管理
|
||||
Memory Cost: 記憶體佔用
|
||||
Memory: 記憶體
|
||||
Message {0} not found.: 訊息 {0} 不存在。
|
||||
Messages: 訊息
|
||||
Most Upvoted Solutions: 最熱題解
|
||||
My Domains: 我的域
|
||||
My Profile: 我的資料
|
||||
My Recent Submissions: 我最近的遞交記錄
|
||||
My Role: 我的角色
|
||||
Name: 名稱
|
||||
New Email: 新電子郵件
|
||||
New Password: 新密碼
|
||||
New Training Plan: 新訓練計劃
|
||||
New: 創建
|
||||
No dataset at present.: 暫無測試資料。
|
||||
no discussion yet...: 目前沒有討論…
|
||||
No Permission to Attend Contest: 您沒有許可權參加比賽
|
||||
No Permission to Claim Homework: 您沒有權限認領功課
|
||||
No Permission to Create a Discussion: 您沒有許可權創建討論
|
||||
No Permission to Submit: 您沒有許可權遞交
|
||||
No Submissions: 沒有遞交
|
||||
no_translation_warn: <blockquote class="warn">這部分內容正在翻譯。</blockquote>
|
||||
Not Attended: 未參加
|
||||
Not available: 不可用
|
||||
Not Claimed: 未認領
|
||||
Not started with a number: 不以數字開頭
|
||||
Not wrapped with space: 兩側不含空格
|
||||
Oh, the user hasn't provide any discussions yet!: 啊哦,此用戶尚未參與討論......
|
||||
Oh, the user hasn't provide any problems yet!: 啊哦,此用戶尚未提供題目......
|
||||
Oh, the user hasn't provide any solutions yet!: 啊哦,此用戶尚未撰寫解題解......
|
||||
Oh, the user hasn't submitted yet!: 啊哦,此用戶尚未遞交......
|
||||
Oh, there is no task in the queue!: 喔,佇列中目前沒有任務。
|
||||
Ok: 確定
|
||||
Oops, there are no results.: 呀,沒有結果。
|
||||
Oops!: Oops!
|
||||
Open Scratchpad: 進入線上程式設計模式
|
||||
Open Since: 開始時間
|
||||
Open Source: 開源
|
||||
Operating System: 作業系統
|
||||
Original Score: 原始得分
|
||||
Owner: 所有者
|
||||
pager_first: « 第一頁
|
||||
pager_last: 末頁 »
|
||||
pager_next: 下一頁 ›
|
||||
pager_previous: ‹ 前一頁
|
||||
Partic.: 參賽人數
|
||||
Password reset mail has been sent to your email.: 密碼重置郵件已傳送到您的電子郵箱。
|
||||
Password: 密碼
|
||||
Passwords don't match.: 驗證密碼不匹配。
|
||||
Path {0} not found.: 路徑 {0} 不存在。
|
||||
Peak Memory: 峰值記憶體
|
||||
perm_contest: 比賽
|
||||
perm_discussion: 討論
|
||||
perm_general: 一般
|
||||
perm_problem_solution: 題解
|
||||
perm_problem: 題庫
|
||||
perm_record: 記錄
|
||||
perm_training: 訓練
|
||||
Permission: 許可權
|
||||
Permissions: 許可權
|
||||
Please attend contest to see the problems.: 請參加比賽來檢視題目。
|
||||
Please claim the assignment to see the problems.: 認領功課后方可查看題目。
|
||||
Please select at least one role to perform this operation.: 請選擇至少一個角色來進行操作。
|
||||
Please select at least one user to perform this operation.: 請選擇至少一個使用者來進行操作。
|
||||
Pretest Data: 自測資料
|
||||
Pretest Your Code: 使用自定義資料測試
|
||||
Pretest: 自測
|
||||
Pretest?: 自測?
|
||||
Privacy: 隱私
|
||||
Problem {1} not found.: 題目 {1} 不存在。
|
||||
Problem Data: 題目資料
|
||||
Problem List: 供題列表
|
||||
Problem Set: 題庫
|
||||
Problem Settings: 題目設定
|
||||
Problem Solution List: 題解列表
|
||||
Problem Solution: 題解列表
|
||||
Problem Tags Visibility: 題目標籤可見性
|
||||
problem_create: 創建題目
|
||||
problem_edit: 編輯題目
|
||||
problem_main: 題庫
|
||||
problem_settings: 題目設定
|
||||
problem_solution: 題解
|
||||
problem_submit: 遞交程式碼
|
||||
Problem: 題目
|
||||
Problems Sets: 題庫
|
||||
Problems: 題目
|
||||
Properties: 屬性
|
||||
Public: 公開
|
||||
Published at: 發表於
|
||||
QQ Group: QQ 群
|
||||
QQ Visibility: QQ 可見性
|
||||
Quit Scratchpad: 退出線上程式設計模式
|
||||
Rank: 排名
|
||||
Read data of own problems: 讀取自己題目的資料
|
||||
Read data of problem: 讀取題目資料
|
||||
Read record codes: 讀取記錄的程式碼
|
||||
Ready (☆▽☆): 馬上開始 (☆▽☆)
|
||||
Ready: 即將開始
|
||||
Recent {0} Records: 最近 {0} 條記錄
|
||||
Recent Activities: 最近活動
|
||||
Recent Solutions: 最近題解
|
||||
Recent Submissions: 最近遞交
|
||||
Record {0} not found.: 記錄 {0} 不存在。
|
||||
record_detail: 記錄詳情
|
||||
record_main: 評測佇列
|
||||
Records: 評測記錄
|
||||
Refresh Records: 重新整理評測記錄
|
||||
Refresh: 重新整理
|
||||
Registered at: 註冊於
|
||||
Rejudge problems: 重測題目
|
||||
Rejudge records: 重測記錄
|
||||
Rejudge: 重測
|
||||
Related Discussions: 相關討論
|
||||
Related: 相關
|
||||
Remember me: 記住我
|
||||
Remove Selected User: 將所選使用者移出
|
||||
Remove this data: 移除這組資料
|
||||
Remove: 移除
|
||||
Repeat Password: 重複密碼
|
||||
Reply discussions: 回覆討論
|
||||
Reply problem solutions: 回覆題解
|
||||
reply: 回覆
|
||||
Reply: 回覆
|
||||
Report: 舉報
|
||||
required: 必填
|
||||
Resent Discussions: 最近討論
|
||||
Reset Password: 重置密碼
|
||||
Role has been updated to {0} for selected users.: 所選使用者角色已更新為 {0}。
|
||||
Role has been updated to {0}.: 角色已更新為 {0}。
|
||||
Role name can only contains numbers, letters and underscores.: 角色名稱只能包含數字、字母和下劃線。
|
||||
Role: 角色
|
||||
Roles: 角色
|
||||
Rule: 規則
|
||||
Run Pretest: 運行自測
|
||||
Sample Input: 輸入資料
|
||||
Sample Output: 輸出資料
|
||||
Save All Changes: 儲存所有修改
|
||||
Saved session: 已儲存的會話
|
||||
score canceled: 成績取消
|
||||
Score: 分數
|
||||
Scoreboard (Hidden): 成績表 (隱藏)
|
||||
Scoreboard: 成績表
|
||||
Secret (visible to admins): 保密(對管理員可見)
|
||||
Secret: 保密
|
||||
Security: 安全
|
||||
Select a node to create discussion.: 選擇一個節點來發表討論。
|
||||
Select a role: 選擇一個角色
|
||||
Select User: 選擇使用者
|
||||
Selected roles have been deleted.: 所選角色已刪除。
|
||||
Selected users have been removed from the domain.: 所選使用者已從此域中移除。
|
||||
Send Code after acceptance: 通過題目後傳送原始碼
|
||||
Send Password Reset Email: 傳送密碼重置郵件
|
||||
Send Verification Email: 傳送驗證郵件
|
||||
Send: 傳送
|
||||
Service Status: 服務狀態
|
||||
Set Role: 設定角色
|
||||
Set Roles for Selected User: 設定所選使用者的角色
|
||||
Set Roles for selected users: 為所選使用者設定角色
|
||||
setting_customize: 自訂
|
||||
setting_display: 顯示偏好
|
||||
setting_info_domain: 個人資訊
|
||||
setting_info: 個人資訊
|
||||
setting_privacy: 隱私
|
||||
setting_usage: 使用偏好
|
||||
Settings: 設定
|
||||
Share: 分享
|
||||
Show all tags: 顯示所有標籤
|
||||
Show MOD badge: 展示 MOD 徽章
|
||||
Show: 顯示
|
||||
Sign In: 登入
|
||||
SIGN IN: 登入
|
||||
Sign up mail has been sent to your email.: 註冊郵件已經發送到您的電子郵箱。
|
||||
Sign Up Now: 現在註冊
|
||||
Sign Up: 註冊
|
||||
Solutions Liked: 題解被贊
|
||||
Solutions: 題解
|
||||
Solve: 解決
|
||||
Sorry, there is no problem in the problem set: 題庫中沒有題目
|
||||
Sorry: 對不起
|
||||
Sort by: 排序
|
||||
Star Topic: 收藏討論
|
||||
Star: 收藏
|
||||
Start at: 開始於
|
||||
Statistics: 統計
|
||||
Status is hidden: 狀態已隱藏
|
||||
Status: 狀態
|
||||
Submission Statistics: 遞交統計
|
||||
Submission: 遞交
|
||||
Submissions: 遞交
|
||||
Submit At: 遞交時間
|
||||
Submit By: 遞交者
|
||||
Submit problem: 遞交題目
|
||||
Submit Solution: 遞交評測
|
||||
Submit Time: 遞交時間
|
||||
Submit to Judge: 遞交以評測
|
||||
Submit Your Code: 遞交程式碼進行正式評測
|
||||
Submit: 遞交
|
||||
Submitted: 已遞交
|
||||
Support: 支援
|
||||
Technical Information: 技術資訊
|
||||
Temporary session: 臨時會話
|
||||
Text: 文字
|
||||
The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 功課已經截止,但處於延期時間内。您此時遞交的題目無法獲得全部分數。
|
||||
Their account will not be deleted and they will be with the default role.: 他們的賬號不會被刪除,並且之後將以 default 角色訪問。
|
||||
There are no contests...: 沒有比賽…
|
||||
There is no homework so far ╰(*°▽°*)╯: 目前沒有功課 ╰(*°▽°*)╯
|
||||
This contest is not live.: 比賽沒有開始。
|
||||
This email was sent by {0} automatically, and please do not reply directly.: 這封郵件由 {0} 自動傳送,請勿直接回復。
|
||||
This homework is not open and you cannot view problems.: 功課尚未開放,不能查看題目。
|
||||
This homework is not open.: 功課尚未開放遞交。
|
||||
This is the current session: 這是當前的會話
|
||||
Time Cost: 耗時
|
||||
Time Extension: 延期
|
||||
Time: 時間
|
||||
timeago_locale: zh_TW
|
||||
Timezone: 時區
|
||||
title: 標題
|
||||
Title: 標題
|
||||
Toggle Pretest Panel: 切換自測面板
|
||||
Toggle Records Panel: 切換評測記錄面板
|
||||
Total Score: 總分
|
||||
Total Time: 總耗時
|
||||
Training requirement is not satisfied.: 不滿足訓練條件。
|
||||
training_main: 訓練
|
||||
Type: 類型
|
||||
UI Language: 使用者介面語言
|
||||
Unique: 唯一
|
||||
Unknown field {0}.: 未知欄位 {0}。
|
||||
Unknown: 未知
|
||||
Unstar Topic: 取消收藏
|
||||
Unstar: 取消收藏
|
||||
Update Permission: 更新許可權
|
||||
Update: 更新
|
||||
Upload Problem: 上傳題目
|
||||
Upload: 上傳
|
||||
Uploaded By: 上傳者
|
||||
User {0} already exists.: 使用者 {0} 已存在。
|
||||
User {0} not found.: 使用者 {0} 不存在。
|
||||
User ID: 使用者 ID
|
||||
user_detail: 使用者
|
||||
user_login: 登入
|
||||
user_logout: 登出
|
||||
user_lostpass: 忘記密碼
|
||||
user_register: 註冊
|
||||
User-defined role: 使用者定義的角色
|
||||
User: 使用者
|
||||
Username / UID: 使用者名或 UID
|
||||
Username: 使用者名
|
||||
Users with those roles will be removed from the domain.: 擁有這些角色的使用者將從此域中移除。
|
||||
Users: 使用者
|
||||
Using your Hydro universal account: 使用您的 Hydro 通用賬戶
|
||||
Valid for registered users who are not members of the domain: 對域外註冊使用者有效
|
||||
Valid for visitors: 對未登入的遊客有效
|
||||
View contest scoreboard: 檢視比賽成績表
|
||||
View Contest: 檢視比賽
|
||||
View contests: 檢視比賽
|
||||
View Details: 檢視詳情
|
||||
View discussions: 檢視討論
|
||||
View hidden contest submission status and scoreboard: 檢視隱藏的比賽遞交狀態和成績表
|
||||
View hidden homework submission status and scoreboard: 翻閲隱藏的功課遞交狀態和成績表
|
||||
View homework scoreboard: 翻閲功課成績表
|
||||
View homework: 翻閲功課
|
||||
View Homework: 翻閲功課
|
||||
View or create discussion: 檢視與發表討論
|
||||
View or submit solution: 檢視與發表題解
|
||||
View Owned Training Plans: 檢視自己的訓練計劃
|
||||
View problem solutions: 檢視題解
|
||||
View Problem: 檢視題目
|
||||
View problems: 檢視題目
|
||||
View this domain: 檢視此域
|
||||
View training: 檢視訓練
|
||||
View: 檢視
|
||||
Visible to registered users: 對註冊使用者可見
|
||||
Visit: 檢視
|
||||
Vote problem solutions: 為題解投票
|
||||
Vote: 投票
|
||||
We use Gravatar to present your avatar icon.: 我們使用 Gravatar 服務來呈現您的頭像。
|
||||
We will check code similarity after this contest.: 我們會在賽後檢查程式碼相似度。
|
||||
WeChat Visibility: 微信可見性
|
||||
WeChat: 微信
|
||||
What is domain?: 什麼是域?
|
||||
What's domain?: 什麼是域?
|
||||
Whether to show tags in the problem list.: 是否在題目列表中顯示題目的標籤。
|
||||
Will be used as the domain icon.: 將被用作圖示。
|
||||
Without slashes or backslashes: 不含斜槓和反斜槓
|
||||
Write Your Comment: 發表您的評論
|
||||
Write Your Solution: 發表您的題解
|
||||
You can create your own training plans and share with others.: 您可以創建您自己的訓練計劃並且與他人分享。
|
||||
You cannot submit for this problem because the homework's deadline is due.: 功課已到達截止時間,您無法遞交了。
|
||||
You don't have the required permission ({0}) in this domain.: 您在該域中無相應許可權 ({0})。
|
||||
You don't have the required privilege.: 您沒有相應的特權。
|
||||
You don't own any domain and don't have privilege to create domain.: 您不擁有任何域也沒有創建域的特權。
|
||||
You have not submitted any solutions for this problem: 您在該題下沒有遞交過任何解答
|
||||
You have permissions to create problems for this domain.: 您有許可權為此域創建題目。
|
||||
You haven't attended this contest yet.: 您還沒有參加本次比賽。
|
||||
You haven't claimed this homework yet.: 您還未認領該功課。
|
||||
You're not logged in.: 您沒有登入。
|
||||
You've already attended this contest.: 您已經參加本次比賽。
|
||||
You've already claimed this homework.: 您已認領該功課。
|
||||
You've already voted.: 您已經投過票。
|
||||
Your permission: 您的許可權
|
||||
Your role: 您的角色
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "2.12.1",
|
||||
"main": "dist/loader.js",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
"author": "masnn",
|
||||
"license": "GPL-3.0-only",
|
||||
"engines": {
|
||||
"node": ">=10.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.4.16",
|
||||
"ansi_up": "^4.0.4",
|
||||
"cookies": "^0.8.0",
|
||||
"detect-browser": "^5.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"html-to-text": "^5.1.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"koa": "^2.13.0",
|
||||
"koa-body": "^4.2.0",
|
||||
"koa-morgan": "^1.0.1",
|
||||
"koa-router": "^8.0.8",
|
||||
"koa-static-cache": "^5.1.3",
|
||||
"lodash": "^4.17.19",
|
||||
"lru-cache": "^6.0.0",
|
||||
"markdown-it": "^11.0.0",
|
||||
"markdown-it-anchor": "^5.3.0",
|
||||
"markdown-it-footnote": "^3.0.2",
|
||||
"markdown-it-imsize": "^2.0.1",
|
||||
"markdown-it-mark": "^3.0.0",
|
||||
"markdown-it-table-of-contents": "^0.4.4",
|
||||
"moment-timezone": "^0.5.31",
|
||||
"mongodb": "^3.5.9",
|
||||
"mongodb-watcher": "^1.3.0",
|
||||
"nodemailer": "^6.4.10",
|
||||
"nunjucks": "^3.2.2",
|
||||
"prismjs": "^1.20.0",
|
||||
"proxy-agent": "^3.1.1",
|
||||
"ps-list": "^7.2.0",
|
||||
"serialize-javascript": "^4.0.0",
|
||||
"sockjs": "^0.3.20",
|
||||
"superagent": "^5.2.2",
|
||||
"systeminformation": "^4.26.10",
|
||||
"yargs": "^15.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.4.33",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/html-to-text": "^5.1.1",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/katex": "^0.11.0",
|
||||
"@types/koa": "^2.11.3",
|
||||
"@types/koa-morgan": "^1.0.4",
|
||||
"@types/koa-router": "^7.4.1",
|
||||
"@types/koa-static-cache": "^5.1.0",
|
||||
"@types/lodash": "^4.14.158",
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/markdown-it": "^10.0.1",
|
||||
"@types/mongodb": "^3.5.25",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/nunjucks": "^3.1.3",
|
||||
"@types/prismjs": "^1.16.1",
|
||||
"@types/serialize-javascript": "^4.0.0",
|
||||
"@types/sockjs": "^0.3.32",
|
||||
"@types/superagent": "^4.1.8",
|
||||
"@types/yargs": "^15.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc --watch"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// TODO check more
|
||||
|
||||
import os from 'os';
|
||||
import superagent from 'superagent';
|
||||
import { Dictionary } from 'lodash';
|
||||
import * as system from './model/system';
|
||||
import * as db from './service/db';
|
||||
|
||||
const c = {};
|
||||
|
||||
type CheckItem = (context: any, log: Function, warn: Function, error: Function) => Promise<void>;
|
||||
|
||||
const checks: Dictionary<CheckItem> = {
|
||||
async checkDb(ctx, log, warn, error) {
|
||||
try {
|
||||
const coll = db.collection('check');
|
||||
const d = await coll.findOne({ _id: 'check' });
|
||||
if (!d) await coll.insertOne({ _id: 'check', value: 'check' });
|
||||
await coll.createIndex('check');
|
||||
await coll.deleteOne({ _id: 'check' });
|
||||
await coll.drop();
|
||||
} catch (e) {
|
||||
error(`Mongo Error: Database read-write failed.\n${e.message}`);
|
||||
}
|
||||
},
|
||||
async checkPerm(ctx, log, warn) {
|
||||
const { username } = os.userInfo();
|
||||
if (username === 'root') warn('Hydro should not be run as root.');
|
||||
// TODO check cwd read-write
|
||||
},
|
||||
async checkSystem(ctx, log, warn) {
|
||||
const platform = os.platform();
|
||||
if (platform !== 'linux') {
|
||||
warn(`Hydro is running on ${platform}, suggest to use linux4.4+.`);
|
||||
}
|
||||
},
|
||||
async checkMail(ctx, log, warn) {
|
||||
const from = await system.get('smtp.from');
|
||||
if (!from) warn('SMTP account was not provided, email verification disabled.');
|
||||
},
|
||||
async checkProxy(ctx, log, warn) {
|
||||
const [github, google, proxy] = await system.getMany([
|
||||
'oauth.githubappid', 'oauth.googleappid', 'proxy',
|
||||
]);
|
||||
if (proxy) {
|
||||
superagent.get('https://www.google.com/').timeout(10000).proxy(proxy)
|
||||
.catch(() => warn('The proxy configured seem to be invalid.'));
|
||||
} else if (github || google) {
|
||||
warn('OAuth enabled. But for well-known reasons, a proxy is required.');
|
||||
}
|
||||
},
|
||||
async checkIpHeader(ctx, log, warn) {
|
||||
const header = await system.get('server.xff');
|
||||
if (header && !ctx.request.ip) warn('IP header seems incorrect.\nCheck dashboard>settings>server.');
|
||||
},
|
||||
};
|
||||
|
||||
export async function start(ctx, log, warn, error, cb) {
|
||||
const id = String.random(6);
|
||||
cb(id);
|
||||
for (const name in checks) {
|
||||
if (c[id]) {
|
||||
delete c[id];
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await checks[name](ctx, log, warn, error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancel(id: string) {
|
||||
c[id] = true;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
import { argv } from 'yargs';
|
||||
import {
|
||||
lib, service, model,
|
||||
builtinLib, builtinModel,
|
||||
} from './common';
|
||||
|
||||
const COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
|
||||
const ARR = /=>.*$/mg;
|
||||
function parseParameters(fn: Function) {
|
||||
const code = fn.toString()
|
||||
.replace(COMMENTS, '')
|
||||
.replace(ARR, '');
|
||||
const result = code.slice(code.indexOf('(') + 1, code.indexOf(')'))
|
||||
.match(/([^,]+)/g).map((i) => i.trim());
|
||||
return result === null ? [] : result;
|
||||
}
|
||||
|
||||
async function cli() {
|
||||
const [, modelName, func, ...args] = argv._;
|
||||
if (!global.Hydro.model[modelName]) {
|
||||
return console.error(`Model ${modelName} doesn't exist.`);
|
||||
}
|
||||
if (!global.Hydro.model[modelName][func]) {
|
||||
return console.error(`Function ${func} doesn't exist in model ${modelName}.`);
|
||||
}
|
||||
if (typeof global.Hydro.model[modelName][func] !== 'function') {
|
||||
return console.error(`${func} in model ${modelName} is not a function.`);
|
||||
}
|
||||
const parameterMin = global.Hydro.model[modelName][func].length;
|
||||
const parameters = parseParameters(global.Hydro.model[modelName][func]);
|
||||
const parameterMax = parameters.length;
|
||||
if (args.length > parameterMax) {
|
||||
console.error(`Too many arguments. Max ${parameterMax}`);
|
||||
return console.error(parameters.join(', '));
|
||||
}
|
||||
if (args.length < parameterMin) {
|
||||
console.error(`Too few arguments. Min ${parameterMin}`);
|
||||
return console.error(parameters.join(', '));
|
||||
}
|
||||
let result = global.Hydro.model[modelName][func](...args);
|
||||
if (result instanceof Promise) result = await result;
|
||||
return console.log(result);
|
||||
}
|
||||
|
||||
export async function load() {
|
||||
const pending = global.addons;
|
||||
const fail = [];
|
||||
require('../lib/i18n');
|
||||
require('../utils');
|
||||
require('../error');
|
||||
require('../options');
|
||||
const bus = require('../service/bus');
|
||||
await new Promise((resolve) => {
|
||||
const h = () => {
|
||||
bus.unsubscribe(['system_database_connected'], h);
|
||||
resolve();
|
||||
};
|
||||
bus.subscribe(['system_database_connected'], h);
|
||||
require('../service/db');
|
||||
});
|
||||
for (const i of builtinLib) require(`../lib/${i}`);
|
||||
await lib(pending, fail);
|
||||
require('../service/gridfs');
|
||||
await service(pending, fail);
|
||||
for (const i of builtinModel) require(`../model/${i}`);
|
||||
await model(pending, fail);
|
||||
for (const postInit of global.Hydro.postInit) await postInit();
|
||||
await cli();
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-eval */
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import yaml from 'js-yaml';
|
||||
import { argv } from 'yargs';
|
||||
|
||||
export const builtinLib = [
|
||||
'jwt', 'download', 'i18n', 'mail', 'useragent',
|
||||
'md5', 'misc', 'paginate', 'hash.hydro', 'rank',
|
||||
'validator', 'nav', 'sysinfo', 'testdata.convert.ini', 'testdataConfig',
|
||||
'logger',
|
||||
];
|
||||
|
||||
export const builtinModel = [
|
||||
'builtin', 'document', 'domain', 'blacklist', 'opcount',
|
||||
'setting', 'token', 'user', 'problem', 'record',
|
||||
'contest', 'message', 'solution', 'training', 'file',
|
||||
'discussion', 'system',
|
||||
];
|
||||
|
||||
export const builtinHandler = [
|
||||
'home', 'problem', 'record', 'judge', 'user',
|
||||
'contest', 'training', 'discussion', 'manage', 'import',
|
||||
'misc', 'homework', 'domain',
|
||||
];
|
||||
|
||||
export const builtinScript = [
|
||||
'rating', 'register', 'problemStat', 'blacklist', 'setSuperadmin',
|
||||
'deleteUser',
|
||||
'upgrade0_1',
|
||||
];
|
||||
|
||||
function getFiles(folder: string, base = ''): string[] {
|
||||
const files = [];
|
||||
const f = fs.readdirSync(folder);
|
||||
for (const i of f) {
|
||||
if (fs.statSync(path.join(folder, i)).isDirectory()) {
|
||||
files.push(...getFiles(path.join(folder, i), path.join(base, i)));
|
||||
} else files.push(path.join(base, i));
|
||||
}
|
||||
return files.map((item) => item.replace(/\\/gmi, '/'));
|
||||
}
|
||||
|
||||
export async function handler(pending: string[], fail: string[]) {
|
||||
for (const i of pending) {
|
||||
const p = path.resolve(i, 'handler.js');
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
console.log(`Handler init: ${i}`);
|
||||
console.time(`Handler init: ${i}`);
|
||||
eval('require')(p);
|
||||
console.timeEnd(`Handler init: ${i}`);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
console.error(`Handler Load Fail: ${i}`);
|
||||
if (argv.debug) console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function locale(pending: string[], fail: string[]) {
|
||||
for (const i of pending) {
|
||||
let p = path.resolve(i, 'locales');
|
||||
if (!fs.existsSync(p)) p = path.resolve(i, 'locale');
|
||||
if (fs.existsSync(p) && fs.statSync(p).isDirectory() && !fail.includes(i)) {
|
||||
try {
|
||||
const files = fs.readdirSync(p);
|
||||
const locales = {};
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(path.resolve(p, file)).toString();
|
||||
locales[file.split('.')[0]] = yaml.safeLoad(content);
|
||||
}
|
||||
global.Hydro.lib.i18n(locales);
|
||||
console.log(`Locale init: ${i}`);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
console.error(`Locale Load Fail: ${i}`);
|
||||
if (argv.debug) console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setting(pending: string[], fail: string[], modelSetting: typeof import('../model/setting')) {
|
||||
const map = {
|
||||
system: modelSetting.SystemSetting,
|
||||
account: modelSetting.AccountSetting,
|
||||
preference: modelSetting.PreferenceSetting,
|
||||
};
|
||||
for (const i of pending) {
|
||||
let p = path.resolve(i, 'setting.yaml');
|
||||
const t = i.split(path.sep);
|
||||
const name = t[t.length - 1];
|
||||
if (!fs.existsSync(p)) p = path.resolve(i, 'settings.yaml');
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
const cfg: any = yaml.safeLoad(fs.readFileSync(p).toString());
|
||||
for (const key in cfg) {
|
||||
map[cfg[key].category || 'system'](
|
||||
modelSetting.Setting(
|
||||
name, `${name}.${key}`, cfg[key].range, cfg[key].default,
|
||||
cfg[key].type || 'text', cfg[key].name || key, cfg[key].desc || '',
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Config Load Fail: ${i}`);
|
||||
if (argv.debug) console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function template(pending: string[], fail: string[]) {
|
||||
for (const i of pending) {
|
||||
let p = path.resolve(i, 'templates');
|
||||
if (!fs.existsSync(p)) p = path.resolve(i, 'template');
|
||||
if (fs.existsSync(p) && fs.statSync(p).isDirectory() && !fail.includes(i)) {
|
||||
try {
|
||||
const files = getFiles(p);
|
||||
for (const file of files) {
|
||||
global.Hydro.ui.template[file] = fs.readFileSync(
|
||||
path.resolve(p, file),
|
||||
).toString();
|
||||
}
|
||||
console.log(`Template init: ${i}`);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
console.error(`Template Load Fail: ${i}`);
|
||||
if (argv.debug) console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function uistatic(pending: string[], fail: string[]) {
|
||||
for (const i of pending) {
|
||||
const p = path.resolve(i, 'public', 'static-manifest.json');
|
||||
if (fs.existsSync(p) && fs.statSync(p).isFile() && !fail.includes(i)) {
|
||||
try {
|
||||
Object.assign(global.Hydro.ui.manifest, eval('require')(p));
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function model(pending: string[], fail: string[]) {
|
||||
for (const i of pending) {
|
||||
const p = path.resolve(i, 'model.js');
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
console.log(`Model init: ${i}`);
|
||||
console.time(`Model init: ${i}`);
|
||||
eval('require')(p);
|
||||
console.timeEnd(`Model init: ${i}`);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
console.error(`Model Load Fail: ${i}`);
|
||||
if (argv.debug) console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function lib(pending: string[], fail: string[]) {
|
||||
for (const i of pending) {
|
||||
const p = path.resolve(i, 'lib.js');
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
console.log(`Lib init: ${i}`);
|
||||
console.time(`Lib init: ${i}`);
|
||||
eval('require')(p);
|
||||
console.timeEnd(`Lib init: ${i}`);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
console.error(`Lib Load Fail: ${i}`);
|
||||
if (argv.debug) console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function service(pending: string[], fail: string[]) {
|
||||
for (const i of pending) {
|
||||
const p = path.resolve(i, 'service.js');
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
console.log(`Service init: ${i}`);
|
||||
console.time(`Service init: ${i}`);
|
||||
eval('require')(p);
|
||||
console.timeEnd(`Service init: ${i}`);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
console.error(`Service Load Fail: ${i}`);
|
||||
if (argv.debug) console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function script(pending: string[], fail: string[], active: string[]) {
|
||||
for (const i of pending) {
|
||||
const p = path.resolve(i, 'script.js');
|
||||
if (fs.existsSync(p) && !fail.includes(i)) {
|
||||
try {
|
||||
console.time(`Script init: ${i}`);
|
||||
eval('require')(p);
|
||||
console.timeEnd(`Script init: ${i}`);
|
||||
} catch (e) {
|
||||
fail.push(i);
|
||||
console.error(`Script Load Fail: ${i}`);
|
||||
if (argv.debug) console.error(e);
|
||||
}
|
||||
}
|
||||
active.push(i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import pslist from 'ps-list';
|
||||
import { argv } from 'yargs';
|
||||
import { builtinModel } from './common';
|
||||
import { Entry } from '../loader';
|
||||
|
||||
const tmpdir = path.resolve(os.tmpdir(), 'hydro');
|
||||
const lockfile = path.resolve(tmpdir, 'lock.json');
|
||||
|
||||
export async function load(call: Entry) {
|
||||
if (fs.existsSync(lockfile) && !argv.ignorelock) {
|
||||
const file = require(lockfile);
|
||||
const plist = await pslist();
|
||||
if (file.pid && plist.map((i) => i.pid).includes(file.pid)) {
|
||||
console.error(`Lockfile exists, pid=${file.pid}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const context = {
|
||||
addons: global.addons,
|
||||
pid: process.pid,
|
||||
ppid: process.ppid,
|
||||
};
|
||||
await fs.writeFile(lockfile, JSON.stringify(context));
|
||||
global.onDestory.push(() => {
|
||||
fs.removeSync(lockfile);
|
||||
});
|
||||
require('../lib/i18n');
|
||||
require('../utils');
|
||||
require('../error');
|
||||
const config = require('../options').default();
|
||||
if (!config) {
|
||||
console.log('Starting setup');
|
||||
await call({ entry: 'setup', newProcess: true }).catch((err) => {
|
||||
console.error('Cannot start setup process.', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
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');
|
||||
});
|
||||
for (const i of builtinModel) require(`../model/${i}`);
|
||||
const modelSystem = require('../model/system');
|
||||
const dbVer = await modelSystem.get('db.ver');
|
||||
if (dbVer !== 1) {
|
||||
const ins = require('../script/upgrade0_1');
|
||||
await ins.run({ username: 'Root', password: 'rootroot' });
|
||||
}
|
||||
for (const postInit of global.Hydro.postInit) {
|
||||
await postInit().catch(console.error);
|
||||
}
|
||||
return await modelSystem.get('server.worker');
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import http from 'http';
|
||||
import fs from 'fs-extra';
|
||||
import Koa, { Context } from 'koa';
|
||||
import Body from 'koa-body';
|
||||
import cache from 'koa-static-cache';
|
||||
import nunjucks from 'nunjucks';
|
||||
import mongodb from 'mongodb';
|
||||
import { argv } from 'yargs';
|
||||
|
||||
const listenPort = argv.port || 8888;
|
||||
|
||||
class Loader {
|
||||
getSource(name: string) { // eslint-disable-line class-methods-use-this
|
||||
return {
|
||||
src: `
|
||||
<!DOCTYPE html>
|
||||
<html data-page="setup" data-layout="immersive" class="layout--immersive page--setup nojs">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta name="msapplication-TileColor" content="#579e9a">
|
||||
<meta name="theme-color" content="#56758f">
|
||||
<link rel="stylesheet" media="all" href="/vj4.css">
|
||||
<title>{{ _('Setup') }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slideout-panel" id="panel">
|
||||
<div class="main">
|
||||
<div class="row"><div class="columns">
|
||||
<div class="immersive--content immersive--center">
|
||||
<h1>{{ _('Setup') }}</h1>
|
||||
<form method="POST">
|
||||
<div class="row"><div class="columns">
|
||||
<label class="inverse material textbox">
|
||||
{{ _('Database Host') }}
|
||||
<input name="host" type="text" value="127.0.0.1" autofocus>
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label class="inverse material textbox">
|
||||
{{ _('Database Port') }}
|
||||
<input name="port" type="number" value="27017">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label class="inverse material textbox">
|
||||
{{ _('Database Name') }}
|
||||
<input name="name" type="text" value="hydro">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label class="inverse material textbox">
|
||||
{{ _('Database Username') }}
|
||||
<input name="username" type="text" placeholder="{{ _('Leave blank if none') }}">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label class="inverse material textbox">
|
||||
{{ _('Database Password') }}
|
||||
<input name="password" type="password" placeholder="{{ _('Leave blank if none') }}">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<div class="text-center">
|
||||
<input type="submit" value="{{ _('Confirm') }}" class="inverse expanded rounded primary button">
|
||||
</div>
|
||||
</div></div>
|
||||
</form>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
path: name,
|
||||
noCache: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const env = new nunjucks.Environment(new Loader(), { autoescape: true, trimBlocks: true });
|
||||
|
||||
function render(name: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
env.render(name, {
|
||||
_: (str: string) => str,
|
||||
}, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function get(ctx: Context) {
|
||||
ctx.body = await render('setup.html');
|
||||
ctx.response.type = 'text/html';
|
||||
}
|
||||
|
||||
async function post(ctx: Context) {
|
||||
const {
|
||||
host, port, name, username, password,
|
||||
} = ctx.request.body;
|
||||
let mongourl = 'mongodb://';
|
||||
if (username) mongourl += `${username}:${password}@`;
|
||||
mongourl += `${host}:${port}/${name}`;
|
||||
try {
|
||||
const Database = await mongodb.MongoClient.connect(mongourl, {
|
||||
useNewUrlParser: true, useUnifiedTopology: true,
|
||||
});
|
||||
const db = Database.db(name);
|
||||
await Promise.all([
|
||||
db.collection('system').updateOne(
|
||||
{ _id: 'server.host' },
|
||||
{ $set: { value: ctx.request.host } },
|
||||
{ upsert: true },
|
||||
),
|
||||
db.collection('system').updateOne(
|
||||
{ _id: 'server.hostname' },
|
||||
{ $set: { value: ctx.request.hostname } },
|
||||
{ upsert: true },
|
||||
),
|
||||
db.collection('system').updateOne(
|
||||
{ _id: 'server.url' },
|
||||
{ $set: { value: ctx.request.href } },
|
||||
{ upsert: true },
|
||||
),
|
||||
db.collection('system').updateOne(
|
||||
{ _id: 'server.port' },
|
||||
{ $set: { value: parseInt(listenPort as string, 10) } },
|
||||
{ upsert: true },
|
||||
),
|
||||
]);
|
||||
fs.ensureDirSync(path.resolve(os.homedir(), '.hydro'));
|
||||
fs.writeFileSync(path.resolve(os.homedir(), '.hydro', 'config.json'), JSON.stringify({
|
||||
host, port, name, username, password,
|
||||
}));
|
||||
ctx.body = `<h1>This page will reload in 3 secs.</h1>
|
||||
<script>
|
||||
setTimeout(function (){
|
||||
window.location.href = '/';
|
||||
}, 3000);
|
||||
</script>`;
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
} catch (e) {
|
||||
ctx.body = `Error connecting to database: ${e.message}\n${e.stack}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function load() {
|
||||
const app = new Koa();
|
||||
const server = http.createServer(app.callback());
|
||||
app.keys = ['Hydro'];
|
||||
app
|
||||
.use(cache(path.join(os.tmpdir(), 'hydro', 'public'), {
|
||||
maxAge: 365 * 24 * 60 * 60,
|
||||
}))
|
||||
.use(Body())
|
||||
.use((ctx) => {
|
||||
if (ctx.request.method.toLowerCase() === 'post') return post(ctx);
|
||||
return get(ctx);
|
||||
});
|
||||
server.listen(listenPort);
|
||||
console.log('Server listening at:', listenPort);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
import {
|
||||
locale, template, lib, service, model, handler, script, setting, uistatic,
|
||||
builtinLib, builtinScript, builtinHandler, builtinModel,
|
||||
} from './common';
|
||||
|
||||
export async function load() {
|
||||
let pending = global.addons;
|
||||
const fail = [];
|
||||
const active = [];
|
||||
require('../lib/i18n');
|
||||
require('../utils');
|
||||
require('../error');
|
||||
require('../options');
|
||||
await Promise.all([
|
||||
locale(pending, fail),
|
||||
template(pending, fail),
|
||||
uistatic(pending, fail),
|
||||
]);
|
||||
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');
|
||||
});
|
||||
for (const i of builtinLib) require(`../lib/${i}`);
|
||||
await lib(pending, fail);
|
||||
require('../service/gridfs');
|
||||
require('../service/monitor');
|
||||
const server = require('../service/server');
|
||||
await server.prepare();
|
||||
await service(pending, fail);
|
||||
for (const i of builtinModel) require(`../model/${i}`);
|
||||
for (const i of builtinHandler) require(`../handler/${i}`);
|
||||
await model(pending, fail);
|
||||
const modelSetting = require('../model/setting');
|
||||
await setting(pending, fail, modelSetting);
|
||||
await handler(pending, fail);
|
||||
for (const i in global.Hydro.handler) {
|
||||
await global.Hydro.handler[i]();
|
||||
}
|
||||
const notfound = require('../handler/notfound');
|
||||
await notfound.apply();
|
||||
for (const i of builtinScript) require(`../script/${i}`);
|
||||
await script(pending, fail, active);
|
||||
for (const postInit of global.Hydro.postInit) await postInit();
|
||||
pending = [];
|
||||
await server.start();
|
||||
setInterval(() => {
|
||||
process.send({ event: 'stat', count: global.Hydro.stat.reqCount });
|
||||
global.Hydro.stat.reqCount = 0;
|
||||
}, 30 * 1000);
|
||||
return { active, fail };
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/* eslint-disable func-names */
|
||||
import { isClass } from './utils';
|
||||
|
||||
interface IHydroError {
|
||||
new(...args: any[]): HydroError
|
||||
}
|
||||
|
||||
function isHydroError(item: any): item is IHydroError {
|
||||
return isClass(item);
|
||||
}
|
||||
|
||||
const Err = (name: string, ...info: Array<IHydroError | (() => string) | string | number>) => {
|
||||
let Class: IHydroError;
|
||||
let msg: () => string;
|
||||
let code: number;
|
||||
for (const item of info) {
|
||||
if (typeof item === 'number') {
|
||||
code = item;
|
||||
} else if (typeof item === 'string') {
|
||||
msg = function () { return item; };
|
||||
} else if (isHydroError(item)) {
|
||||
Class = item;
|
||||
} else if (typeof item === 'function') {
|
||||
msg = item;
|
||||
}
|
||||
}
|
||||
const HydroError = class extends Class { };
|
||||
HydroError.prototype.name = name;
|
||||
if (msg) HydroError.prototype.msg = msg;
|
||||
if (code) HydroError.prototype.code = code;
|
||||
return HydroError;
|
||||
};
|
||||
|
||||
export class HydroError extends Error {
|
||||
params: any[];
|
||||
|
||||
code: number;
|
||||
|
||||
constructor(...params: any[]) {
|
||||
super();
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
msg() {
|
||||
return 'HydroError';
|
||||
}
|
||||
}
|
||||
|
||||
export const UserFacingError = Err('UserFacingError', HydroError, 'UserFacingError', 400);
|
||||
export const SystemError = Err('SystemError', HydroError, 'SystemError', 500);
|
||||
|
||||
export const BadRequestError = Err('BadRequestError', UserFacingError, 'BadRequestError', 400);
|
||||
export const ForbiddenError = Err('ForbiddenError', UserFacingError, 'ForbiddenError', 403);
|
||||
export const NotFoundError = Err('NotFoundError', UserFacingError, 'NotFoundError', 404);
|
||||
export const MethodNotAllowedError = Err('MethodNotAllowedError', UserFacingError, 'MethodNotAllowedError', 405);
|
||||
export const RemoteOnlineJudgeError = Err('RemoteOnlineJudgeError', UserFacingError, 'RemoteOnlineJudgeError', 500);
|
||||
export const SendMailError = Err('SendMailError', UserFacingError, 'Failed to send mail to {0}.', 500);
|
||||
|
||||
export const AlreadyVotedError = Err('AlreadyVotedError', ForbiddenError, "You've already voted.");
|
||||
export const LoginError = Err('LoginError', ForbiddenError, 'Invalid password for user {0}.');
|
||||
export const UserAlreadyExistError = Err('UserAlreadyExistError', ForbiddenError, 'User {0} already exists.');
|
||||
export const InvalidTokenError = Err('InvalidTokenError', ForbiddenError);
|
||||
export const BlacklistedError = Err('BlacklistedError', ForbiddenError, 'Address or user {0} is blacklisted.');
|
||||
export const VerifyPasswordError = Err('VerifyPasswordError', ForbiddenError, "Passwords don't match.");
|
||||
export const OpcountExceededError = Err('OpcountExceededError', ForbiddenError, 'Too frequent operations of {0} (limit: {2} operations in {1} seconds).');
|
||||
export const PermissionError = Err('PermissionError', ForbiddenError, "You don't have the required permission ({0}) in this domain.");
|
||||
export const PrivilegeError = Err('PrivilegeError', ForbiddenError, function () {
|
||||
if (this.params.includes(global.Hydro.model.builtin.PRIV.PRIV_USER_PROFILE)) {
|
||||
return "You're not logged in.";
|
||||
}
|
||||
return "You don't have the required privilege.";
|
||||
});
|
||||
export const ValidationError = Err('ValidationError', ForbiddenError, function () {
|
||||
if (this.params.length === 1) return 'Field {0} validation failed.';
|
||||
return 'Field {0} or {1} validation failed.';
|
||||
});
|
||||
export const ContestNotAttendedError = Err('ContestNotAttendedError', ForbiddenError, "You haven't attended this contest yet.");
|
||||
export const ContestAlreadyAttendedError = Err('ContestAlreadyAttendedError', ForbiddenError, "You've already attended this contest.");
|
||||
export const ContestNotLiveError = Err('ContestNotLiveError', ForbiddenError, 'This contest is not live.');
|
||||
export const ContestScoreboardHiddenError = Err('ContestScoreboardHiddenError', ForbiddenError, 'Contest scoreboard is not visible.');
|
||||
export const TrainingAlreadyEnrollError = Err('TrainingAlreadyEnrollError', ForbiddenError, "You've already enrolled this training.");
|
||||
export const HomeworkNotLiveError = Err('HomeworkNotLiveError', ForbiddenError, 'This homework is not open.');
|
||||
export const HomeworkNotAttendedError = Err('HomeworkNotAttendedError', ForbiddenError, "You haven't claimed this homework yet.");
|
||||
export const RoleAlreadyExistError = Err('RoleAlreadyExistError', ForbiddenError, 'This role already exists.');
|
||||
export const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenError');
|
||||
|
||||
export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
|
||||
export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
|
||||
export const RecordNotFoundError = Err('RecordNotFoundError', NotFoundError, 'Record {0} not found.');
|
||||
export const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {0} not found.');
|
||||
export const MessageNotFoundError = Err('MessageNotFoundError', NotFoundError, 'Message {0} not found.');
|
||||
export const DocumentNotFoundError = Err('DocumentNotFoundError', NotFoundError, 'Document {2} not found.');
|
||||
|
||||
export const ProblemNotFoundError = Err('ProblemNotFountError', DocumentNotFoundError, 'Problem {0} not found.');
|
||||
export const SolutionNotFoundError = Err('SolutionNotFoundError', DocumentNotFoundError, 'Solution {0} not found.');
|
||||
export const TrainingNotFoundError = Err('TrainingNotFoundError', DocumentNotFoundError, 'Training {0} not found.');
|
||||
export const ContestNotFoundError = Err('ContestNotFoundError', DocumentNotFoundError, 'Contest {0} not found.');
|
||||
export const DiscussionNotFoundError = Err('DiscussionNotFoundError', DocumentNotFoundError, 'Discussion {0} not found.');
|
||||
export const DiscussionNodeNotFoundError = Err('DiscussionNodeNotFoundError', DocumentNotFoundError, 'Discussion node {1} not found.');
|
||||
|
||||
export const InvalidOperationError = Err('InvalidOperationError', MethodNotAllowedError);
|
||||
|
||||
global.Hydro.error = {
|
||||
HydroError,
|
||||
BadRequestError,
|
||||
BlacklistedError,
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
SendMailError,
|
||||
LoginError,
|
||||
CsrfTokenError,
|
||||
MethodNotAllowedError,
|
||||
InvalidOperationError,
|
||||
UserAlreadyExistError,
|
||||
InvalidTokenError,
|
||||
UserNotFoundError,
|
||||
VerifyPasswordError,
|
||||
ProblemDataNotFoundError,
|
||||
OpcountExceededError,
|
||||
PermissionError,
|
||||
PrivilegeError,
|
||||
NoProblemError,
|
||||
ValidationError,
|
||||
ProblemNotFoundError,
|
||||
TrainingNotFoundError,
|
||||
ContestNotFoundError,
|
||||
RecordNotFoundError,
|
||||
SolutionNotFoundError,
|
||||
AlreadyVotedError,
|
||||
ContestNotAttendedError,
|
||||
ContestNotLiveError,
|
||||
ContestScoreboardHiddenError,
|
||||
ContestAlreadyAttendedError,
|
||||
UserFacingError,
|
||||
SystemError,
|
||||
TrainingAlreadyEnrollError,
|
||||
HomeworkNotLiveError,
|
||||
HomeworkNotAttendedError,
|
||||
RemoteOnlineJudgeError,
|
||||
DiscussionNodeNotFoundError,
|
||||
DocumentNotFoundError,
|
||||
DiscussionNotFoundError,
|
||||
RoleAlreadyExistError,
|
||||
MessageNotFoundError,
|
||||
};
|
||||
|
||||
/*
|
||||
class FileTooLongError(ValidationError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'The uploaded file is too long.'
|
||||
|
||||
class FileTypeNotAllowedError(ValidationError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'This type of files are not allowed to be uploaded.'
|
||||
|
||||
class InvalidTokenDigestError(ForbiddenError):
|
||||
pass
|
||||
|
||||
class CurrentPasswordError(ForbiddenError):
|
||||
@property
|
||||
def message(self):
|
||||
return "Current password doesn't match."
|
||||
|
||||
class DiscussionCategoryAlreadyExistError(ForbiddenError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'Discussion category {1} already exists.'
|
||||
|
||||
class DiscussionCategoryNotFoundError(NotFoundError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'Discussion category {1} not found.'
|
||||
|
||||
class DiscussionNodeAlreadyExistError(ForbiddenError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'Discussion node {1} already exists.'
|
||||
|
||||
class TrainingRequirementNotSatisfiedError(ForbiddenError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'Training requirement is not satisfied.'
|
||||
|
||||
class UsageExceededError(ForbiddenError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'Usage exceeded.'
|
||||
|
||||
class InvalidArgumentError(BadRequestError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'Argument {0} is invalid.'
|
||||
*/
|
||||
@@ -0,0 +1,420 @@
|
||||
import moment from 'moment-timezone';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import AdmZip from 'adm-zip';
|
||||
import { isSafeInteger } from 'lodash';
|
||||
import {
|
||||
ContestNotLiveError, ValidationError, ProblemNotFoundError,
|
||||
ContestNotAttendedError, PermissionError,
|
||||
} from '../error';
|
||||
import { isContent, isTitle } from '../lib/validator';
|
||||
import paginate from '../lib/paginate';
|
||||
import { PERM, PRIV, CONSTANT } from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import * as document from '../model/document';
|
||||
import * as problem from '../model/problem';
|
||||
import * as record from '../model/record';
|
||||
import * as user from '../model/user';
|
||||
import * as message from '../model/message';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import * as bus from '../service/bus';
|
||||
|
||||
const ContestHandler = contest.ContestHandlerMixin(Handler);
|
||||
|
||||
class ContestListHandler extends ContestHandler {
|
||||
@param('rule', Types.String, true)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
async get(domainId: string, rule = '', page = 1) {
|
||||
this.response.template = 'contest_main.html';
|
||||
let tdocs;
|
||||
let qs;
|
||||
let tpcount;
|
||||
if (!rule) {
|
||||
tdocs = contest.getMulti(domainId).sort({ beginAt: -1 });
|
||||
qs = '';
|
||||
} else {
|
||||
if (!contest.RULES[rule]) throw new ValidationError('rule');
|
||||
tdocs = contest.getMulti(domainId, { rule }).sort({ beginAt: -1 });
|
||||
qs = `rule=${rule}`;
|
||||
}
|
||||
// eslint-disable-next-line prefer-const
|
||||
[tdocs, tpcount] = await paginate(tdocs, page, CONSTANT.CONTEST_PER_PAGE);
|
||||
const tids = [];
|
||||
for (const tdoc of tdocs) tids.push(tdoc.docId);
|
||||
const tsdict = await contest.getListStatus(domainId, this.user._id, tids);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', null],
|
||||
];
|
||||
this.response.body = {
|
||||
page, tpcount, qs, rule, tdocs, tsdict, path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ContestDetailHandler extends ContestHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
async get(domainId: string, tid: ObjectID, page = 1) {
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
this.response.template = 'contest_detail.html';
|
||||
const [tsdoc, pdict] = await Promise.all([
|
||||
contest.getStatus(domainId, tdoc.docId, this.user._id),
|
||||
problem.getList(domainId, tdoc.pids, true),
|
||||
]);
|
||||
const psdict = {};
|
||||
let rdict = {};
|
||||
let attended: boolean;
|
||||
if (tsdoc) {
|
||||
attended = tsdoc.attend === 1;
|
||||
for (const pdetail of tsdoc.journal || []) psdict[pdetail.pid] = pdetail;
|
||||
if (this.canShowRecord(tdoc)) {
|
||||
const q = [];
|
||||
for (const i in psdict) q.push(psdict[i].rid);
|
||||
rdict = await record.getList(domainId, q, true);
|
||||
} else {
|
||||
for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid };
|
||||
}
|
||||
} else attended = false;
|
||||
const udict = await user.getList(domainId, [tdoc.owner]);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', 'contest_main'],
|
||||
[tdoc.title, null, null, true],
|
||||
];
|
||||
this.response.body = {
|
||||
path, tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page,
|
||||
};
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async postAttend(domainId: string, tid: ObjectID) {
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
if (contest.isDone(tdoc)) throw new ContestNotLiveError(tid);
|
||||
await contest.attend(domainId, tid, this.user._id);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class ContestBoardcastHandler extends ContestHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
if (tdoc.owner !== this.user._id) throw new PermissionError('Boardcast Message');
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', 'contest_main'],
|
||||
[tdoc.title, 'contest_detail', { tid }, true],
|
||||
['contest_boardcast'],
|
||||
];
|
||||
this.response.template = 'contest_boardcast.html';
|
||||
this.response.body = path;
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('content', Types.String, isContent)
|
||||
async post(domainId: string, tid: ObjectID, content: string) {
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
if (tdoc.owner !== this.user._id) throw new PermissionError('Boardcast Message');
|
||||
const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).toArray();
|
||||
const uids = Array.from(new Set(tsdocs.map((tsdoc) => tsdoc.uid)));
|
||||
await Promise.all(
|
||||
uids.map((uid) => message.send(this.user._id, uid, content, message.FLAG_ALERT)),
|
||||
);
|
||||
this.response.redirect = this.url('contest_detail', { tid });
|
||||
}
|
||||
}
|
||||
|
||||
class ContestScoreboardHandler extends ContestHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
const [tdoc, rows, udict] = await this.getScoreboard(domainId, tid);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', 'contest_main'],
|
||||
[tdoc.title, 'contest_detail', { tid }, true],
|
||||
['contest_scoreboard', null],
|
||||
];
|
||||
this.response.template = 'contest_scoreboard.html';
|
||||
this.response.body = {
|
||||
tdoc, rows, path, udict,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ContestScoreboardDownloadHandler extends ContestHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('ext', Types.String)
|
||||
async get(domainId: string, tid: ObjectID, ext: string) {
|
||||
const getContent = {
|
||||
csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`,
|
||||
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
|
||||
};
|
||||
if (!getContent[ext]) throw new ValidationError('ext');
|
||||
const [, rows] = await this.getScoreboard(domainId, tid, true);
|
||||
this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
class ContestEditHandler extends ContestHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async prepare(domainId: string, tid: ObjectID) {
|
||||
this.tdoc = await contest.get(domainId, tid);
|
||||
if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||
else this.checkPerm(PERM.PERM_EDIT_CONTEST_SELF);
|
||||
}
|
||||
|
||||
async get() {
|
||||
this.response.template = 'contest_edit.html';
|
||||
const rules = {};
|
||||
for (const i in contest.RULES) {
|
||||
rules[i] = contest.RULES[i].TEXT;
|
||||
}
|
||||
const duration = (this.tdoc.endAt.getTime() - this.tdoc.beginAt.getTime()) / 3600 / 1000;
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', 'contest_main'],
|
||||
[this.tdoc.title, 'contest_detail', { tid: this.tdoc.docId }, true],
|
||||
['contest_edit', null],
|
||||
];
|
||||
const dt = this.tdoc.beginAt;
|
||||
this.response.body = {
|
||||
rules,
|
||||
tdoc: this.tdoc,
|
||||
duration,
|
||||
path,
|
||||
pids: this.tdoc.pids.join(','),
|
||||
date_text: dt.format('%Y-%m-%d'),
|
||||
time_text: dt.format('%H:%M'),
|
||||
page_name: 'contest_edit',
|
||||
};
|
||||
}
|
||||
|
||||
@param('beginAtDate', Types.Date)
|
||||
@param('beginAtTime', Types.Time)
|
||||
@param('duration', Types.Float)
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('content', Types.String, isContent)
|
||||
@param('rule', Types.String)
|
||||
@param('pids', Types.String)
|
||||
@param('rated', Types.Boolean)
|
||||
async post(
|
||||
domainId: string, beginAtDate: string, beginAtTime: string, duration: number,
|
||||
title: string, content: string, rule: string, _pids: string, rated = false,
|
||||
) {
|
||||
let pids = _pids.split(',').map((i) => {
|
||||
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
|
||||
return i;
|
||||
});
|
||||
const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
|
||||
if (!beginAtMoment.isValid()) {
|
||||
throw new ValidationError('beginAtDate', 'beginAtTime');
|
||||
}
|
||||
const endAt = beginAtMoment.clone().add(duration, 'hours').toDate();
|
||||
if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration');
|
||||
const beginAt = beginAtMoment.toDate();
|
||||
pids = await this.verifyProblems(domainId, pids);
|
||||
await contest.edit(domainId, this.tdoc.docId, {
|
||||
title, content, rule, beginAt, endAt, pids, rated,
|
||||
});
|
||||
if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt
|
||||
|| Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule) {
|
||||
await contest.recalcStatus(domainId, this.tdoc.docId);
|
||||
}
|
||||
this.response.body = { tid: this.tdoc.docId };
|
||||
this.response.redirect = this.url('contest_detail', { tid: this.tdoc.docId });
|
||||
}
|
||||
}
|
||||
|
||||
class ContestProblemHandler extends ContestHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('pid', Types.UnsignedInt)
|
||||
async _prepare(domainId: string, tid: ObjectID, pid: number) {
|
||||
[this.tdoc, this.pdoc] = await Promise.all([
|
||||
contest.get(domainId, tid),
|
||||
problem.get(domainId, pid, this.user._id),
|
||||
]);
|
||||
[this.tsdoc, this.udoc] = await Promise.all([
|
||||
contest.getStatus(domainId, this.tdoc.docId, this.user._id),
|
||||
user.getById(domainId, this.tdoc.owner),
|
||||
]);
|
||||
this.attended = this.tsdoc && this.tsdoc.attend === 1;
|
||||
this.response.template = 'problem_detail.html';
|
||||
if (contest.isDone(this.tdoc)) {
|
||||
if (!this.attended) throw new ContestNotAttendedError(this.tdoc.docId);
|
||||
if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc.docId);
|
||||
}
|
||||
if (!this.tdoc.pids.map((s) => s.toString()).includes(this.pdoc.docId.toString())) {
|
||||
throw new ProblemNotFoundError(pid, this.tdoc.docId);
|
||||
}
|
||||
}
|
||||
|
||||
async get() {
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', 'contest_main'],
|
||||
[this.tdoc.title, 'contest_detail', { tid: this.tdoc.docId }, true],
|
||||
[this.pdoc.title, null, null, true],
|
||||
];
|
||||
this.response.body = {
|
||||
tdoc: this.tdoc,
|
||||
pdoc: this.pdoc,
|
||||
tsdoc: this.tsdoc,
|
||||
udoc: this.udoc,
|
||||
attended: this.attended,
|
||||
path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ContestProblemSubmitHandler extends ContestProblemHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('pid', Types.UnsignedInt)
|
||||
async prepare(domainId: string, tid: ObjectID, pid: number) {
|
||||
let rdocs = [];
|
||||
if (this.canShowRecord(this.tdoc)) {
|
||||
rdocs = await record.getUserInProblemMulti(
|
||||
domainId, this.user._id,
|
||||
this.pdoc.docId, true,
|
||||
).sort({ _id: -1 }).limit(10).toArray();
|
||||
}
|
||||
this.response.template = 'problem_submit.html';
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', 'contest_main'],
|
||||
[this.tdoc.title, 'contest_detail', { tid }, true],
|
||||
[this.pdoc.title, 'contest_detail_problem', { tid, pid }, true],
|
||||
['contest_detail_problem_submit', null],
|
||||
];
|
||||
this.response.body = {
|
||||
tdoc: this.tdoc,
|
||||
pdoc: this.pdoc,
|
||||
tsdoc: this.tsdoc,
|
||||
udoc: this.udoc,
|
||||
attended: this.attend,
|
||||
path,
|
||||
rdocs,
|
||||
};
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('lang', Types.String)
|
||||
@param('code', Types.String)
|
||||
async post(domainId: string, tid: ObjectID, lang: string, code: string) {
|
||||
await this.limitRate('add_record', 60, 100);
|
||||
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true, {
|
||||
type: document.TYPE_CONTEST,
|
||||
tid,
|
||||
});
|
||||
const [rdoc] = await Promise.all([
|
||||
record.get(domainId, rid),
|
||||
contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId),
|
||||
]);
|
||||
bus.publish('record_change', rdoc);
|
||||
if (!this.canShowRecord(this.tdoc)) {
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('contest_detail', { tid });
|
||||
} else {
|
||||
this.response.body = { rid };
|
||||
this.response.redirect = this.url('record_detail', { rid });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ContestCodeHandler extends ContestHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
if (!this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
|
||||
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
|
||||
}
|
||||
this.limitRate('homework_code', 3600, 60);
|
||||
const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid);
|
||||
const rnames = {};
|
||||
for (const tsdoc of tsdocs) {
|
||||
for (const pdetail of tsdoc.detail || []) {
|
||||
rnames[pdetail.rid] = `U${tsdoc.uid}_P${pdetail.pid}_R${pdetail.rid}`;
|
||||
}
|
||||
}
|
||||
const zip = new AdmZip();
|
||||
const rdocs = await record.getMulti(domainId, {
|
||||
_id: {
|
||||
$in: Array.from(Object.keys(rnames)).map((id) => new ObjectID(id)),
|
||||
},
|
||||
}).toArray();
|
||||
for (const rdoc of rdocs) {
|
||||
zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code);
|
||||
}
|
||||
await this.binary(zip.toBuffer(), `${tdoc.title}.zip`);
|
||||
}
|
||||
}
|
||||
|
||||
class ContestCreateHandler extends ContestHandler {
|
||||
async get() {
|
||||
this.response.template = 'contest_edit.html';
|
||||
const rules = {};
|
||||
for (const i in contest.RULES) { rules[i] = contest.RULES[i].TEXT; }
|
||||
const now = new Date();
|
||||
let ts = now.getTime();
|
||||
ts = ts - (ts % (15 * 60 * 1000)) + 15 * 60 * 1000;
|
||||
const dt = new Date(ts);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', 'contest_main'],
|
||||
['contest_create', null],
|
||||
];
|
||||
this.response.body = {
|
||||
rules,
|
||||
path,
|
||||
page_name: 'contest_create',
|
||||
date_text: dt.format('%Y-%m-%d'),
|
||||
time_text: dt.format('%M:%S'),
|
||||
pids: '1000, 1001',
|
||||
};
|
||||
}
|
||||
|
||||
@param('beginAtDate', Types.Date)
|
||||
@param('beginAtTime', Types.Time)
|
||||
@param('duration', Types.Float)
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('content', Types.String, isContent)
|
||||
@param('rule', Types.String)
|
||||
@param('pids', Types.String)
|
||||
@param('rated', Types.Boolean)
|
||||
async post(
|
||||
domainId: string, beginAtDate: string, beginAtTime: string, duration: number,
|
||||
title: string, content: string, rule: string, _pids: string, rated = false,
|
||||
) {
|
||||
let pids = _pids.split(',').map((i) => {
|
||||
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
|
||||
return i;
|
||||
});
|
||||
const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
|
||||
if (!beginAt.isValid()) {
|
||||
throw new ValidationError('beginAtDate', 'beginAtTime');
|
||||
}
|
||||
const endAt = beginAt.clone().add(duration, 'hours');
|
||||
pids = await this.verifyProblems(domainId, pids);
|
||||
const tid = await contest.add(
|
||||
domainId, title, content,
|
||||
this.user._id, rule, beginAt.toDate(), endAt.toDate(), pids, rated,
|
||||
);
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('contest_detail', { tid });
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('contest_create', '/contest/create', ContestCreateHandler, PERM.PERM_CREATE_CONTEST);
|
||||
Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST);
|
||||
Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST);
|
||||
Route('contest_boardcast', '/contest/:tid/boardcast', ContestBoardcastHandler);
|
||||
Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
|
||||
Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST);
|
||||
Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler, PERM.PERM_VIEW_CONTEST);
|
||||
Route('contest_detail_problem', '/contest/:tid/p/:pid', ContestProblemHandler, PERM.PERM_VIEW_CONTEST);
|
||||
Route('contest_detail_problem_submit', '/contest/:tid/p/:pid/submit', ContestProblemSubmitHandler, PERM.PERM_VIEW_CONTEST);
|
||||
Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST);
|
||||
}
|
||||
|
||||
global.Hydro.handler.contest = apply;
|
||||
@@ -0,0 +1,372 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { isSafeInteger } from 'lodash';
|
||||
import { DiscussionNotFoundError, DocumentNotFoundError } from '../error';
|
||||
import {
|
||||
Mdoc, Drdoc, Ddoc, Drrdoc,
|
||||
} from '../interface';
|
||||
import paginate from '../lib/paginate';
|
||||
import * as user from '../model/user';
|
||||
import * as message from '../model/message';
|
||||
import * as discussion from '../model/discussion';
|
||||
import * as document from '../model/document';
|
||||
import { PERM, PRIV, CONSTANT } from '../model/builtin';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import { isTitle, isContent } from '../lib/validator';
|
||||
|
||||
export const typeMapper = {
|
||||
problem: document.TYPE_PROBLEM,
|
||||
contest: document.TYPE_CONTEST,
|
||||
node: document.TYPE_DISCUSSION_NODE,
|
||||
training: document.TYPE_TRAINING,
|
||||
homework: document.TYPE_HOMEWORK,
|
||||
};
|
||||
|
||||
class DiscussionHandler extends Handler {
|
||||
ddoc?: Ddoc;
|
||||
|
||||
drdoc?: Drdoc;
|
||||
|
||||
drrdoc?: Drrdoc;
|
||||
|
||||
vnode?: any;
|
||||
|
||||
@param('type', Types.String, true)
|
||||
@param('name', Types.String, true)
|
||||
@param('did', Types.ObjectID, true)
|
||||
@param('drid', Types.ObjectID, true)
|
||||
@param('drrid', Types.ObjectID, true)
|
||||
async _prepare(
|
||||
domainId: string, type: string, name: string,
|
||||
did: ObjectID, drid: ObjectID, drrid: ObjectID,
|
||||
) {
|
||||
this.checkPerm(PERM.PERM_VIEW_DISCUSSION);
|
||||
if (did) {
|
||||
this.ddoc = await discussion.get(domainId, did);
|
||||
if (!this.ddoc) throw new DiscussionNotFoundError(did);
|
||||
type = discussion.typeDisplay[this.ddoc.parentType];
|
||||
name = this.ddoc.parentId.toString();
|
||||
if (drrid) {
|
||||
[this.drdoc, this.drrdoc] = await discussion.getTailReply(domainId, drid, drrid);
|
||||
if (!this.drrdoc) throw new DiscussionNotFoundError(drrid);
|
||||
} else if (drid) {
|
||||
this.drdoc = await discussion.getReply(domainId, drid);
|
||||
if (!this.drdoc) throw new DiscussionNotFoundError(drid);
|
||||
if (!this.drdoc.parentId.equals(this.ddoc._id)) {
|
||||
throw new DocumentNotFoundError(drid);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO(twd2): do more visibility check eg. contest
|
||||
// TODO(twd2): exclude problem/contest discussions?
|
||||
// TODO(iceboy): continuation based pagination.
|
||||
this.vnode = await discussion.getVnode(domainId, typeMapper[type], name);
|
||||
if (this.vnode.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||
if (this.ddoc) {
|
||||
this.ddoc.parentType = this.ddoc.parentType || this.vnode.type;
|
||||
this.ddoc.parentId = this.ddoc.parentId || this.vnode.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DiscussionMainHandler extends Handler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
async get(domainId: string, page = 1) {
|
||||
const [ddocs, dpcount] = await paginate(
|
||||
discussion.getMulti(domainId),
|
||||
page,
|
||||
CONSTANT.DISCUSSION_PER_PAGE,
|
||||
);
|
||||
const udict = await user.getList(domainId, ddocs.map((ddoc) => ddoc.owner));
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['discussion_main', null],
|
||||
];
|
||||
const [vndict, vnodes] = await Promise.all([
|
||||
discussion.getListVnodes(
|
||||
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
|
||||
),
|
||||
discussion.getNodes(domainId),
|
||||
]);
|
||||
this.response.template = 'discussion_main_or_node.html';
|
||||
this.response.body = {
|
||||
ddocs, dpcount, udict, page, page_name: 'discussion_main', vndict, vnode: {}, path, vnodes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DiscussionNodeHandler extends DiscussionHandler {
|
||||
@param('type', Types.String)
|
||||
@param('name', Types.String)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
async get(domainId: string, type: string, _name: string, page = 1) {
|
||||
let name: ObjectID | string | number;
|
||||
if (ObjectID.isValid(_name)) name = new ObjectID(_name);
|
||||
else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10);
|
||||
else name = _name;
|
||||
const [ddocs, dpcount] = await paginate(
|
||||
discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name }),
|
||||
page,
|
||||
CONSTANT.DISCUSSION_PER_PAGE,
|
||||
);
|
||||
const uids = ddocs.map((ddoc) => ddoc.owner);
|
||||
uids.push(this.vnode.owner);
|
||||
const [udict, vnodes] = await Promise.all([
|
||||
user.getList(domainId, uids),
|
||||
discussion.getNodes(domainId),
|
||||
]);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['discussion_main', 'discussion_main'],
|
||||
[this.vnode.title, null, true],
|
||||
];
|
||||
const vndict = { [typeMapper[type]]: { [name.toString()]: this.vnode } };
|
||||
this.response.template = 'discussion_main_or_node.html';
|
||||
this.response.body = {
|
||||
ddocs,
|
||||
dpcount,
|
||||
udict,
|
||||
path,
|
||||
page,
|
||||
vndict,
|
||||
vnode: this.vnode,
|
||||
page_name: 'discussion_node',
|
||||
vnodes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DiscussionCreateHandler extends DiscussionHandler {
|
||||
async get({ type, name }) {
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['discussion_main', 'discussion_main'],
|
||||
[this.vnode.title, 'discussion_node', { type, name }, true],
|
||||
['discussion_create', null],
|
||||
];
|
||||
this.response.template = 'discussion_create.html';
|
||||
this.response.body = { path, vnode: this.vnode };
|
||||
}
|
||||
|
||||
@param('type', Types.String)
|
||||
@param('name', Types.String)
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('content', Types.String, isContent)
|
||||
@param('highlight', Types.Boolean)
|
||||
@param('pin', Types.Boolean)
|
||||
async post(
|
||||
domainId: string, type: string, _name: string,
|
||||
title: string, content: string, highlight = false, pin = false,
|
||||
) {
|
||||
this.limitRate('add_discussion', 3600, 30);
|
||||
let name: ObjectID | string | number;
|
||||
if (ObjectID.isValid(_name)) name = new ObjectID(_name);
|
||||
else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10);
|
||||
else name = _name;
|
||||
if (highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION);
|
||||
if (pin) this.checkPerm(PERM.PERM_PIN_DISCUSSION);
|
||||
const did = await discussion.add(
|
||||
domainId, typeMapper[type], name, this.user._id,
|
||||
title, content, this.request.ip, highlight, pin,
|
||||
);
|
||||
this.response.body = { did };
|
||||
this.response.redirect = this.url('discussion_detail', { did });
|
||||
}
|
||||
}
|
||||
|
||||
class DiscussionDetailHandler extends DiscussionHandler {
|
||||
@param('did', Types.ObjectID)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
async get(domainId: string, did: ObjectID, page = 1) {
|
||||
const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE)
|
||||
? await discussion.getStatus(domainId, did, this.user._id)
|
||||
: null;
|
||||
const [drdocs, pcount, drcount] = await paginate(
|
||||
discussion.getMultiReply(domainId, did),
|
||||
page,
|
||||
CONSTANT.REPLY_PER_PAGE,
|
||||
);
|
||||
const uids = drdocs.map((drdoc) => drdoc.owner);
|
||||
uids.push(this.ddoc.owner);
|
||||
for (const drdoc of drdocs) {
|
||||
if (drdoc.reply) {
|
||||
drdocs.forEach((drrdoc) => uids.push(drrdoc.owner));
|
||||
}
|
||||
}
|
||||
const udict = await user.getList(domainId, uids);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['discussion_main', 'discussion_main'],
|
||||
[this.vnode.title, 'discussion_node', { type: discussion.typeDisplay[this.ddoc.parentType], name: this.ddoc.parentId }, true],
|
||||
[this.ddoc.title, null, null, true],
|
||||
];
|
||||
this.response.template = 'discussion_detail.html';
|
||||
this.response.body = {
|
||||
path, ddoc: this.ddoc, dsdoc, drdocs, page, pcount, drcount, udict, vnode: this.vnode,
|
||||
};
|
||||
}
|
||||
|
||||
async post() {
|
||||
this.checkPriv(PRIV.PRIV_USER_PROFILE);
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectID)
|
||||
@param('content', Types.String, isContent)
|
||||
async postReply(domainId: string, did: ObjectID, content: string) {
|
||||
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
|
||||
this.limitRate('add_discussion', 3600, 30);
|
||||
// Notify related users
|
||||
const replies: Drdoc[] = await discussion.getMultiReply(domainId, did).toArray();
|
||||
const uids = Array.from(new Set(replies.map((drdoc) => drdoc.owner)));
|
||||
const tasks = uids.map((uid) => {
|
||||
if (uid !== this.user._id) {
|
||||
return message.send(1, uid, `Discussion update: ${domainId} ${did}`);
|
||||
}
|
||||
return Promise.resolve(null as Mdoc);
|
||||
});
|
||||
await Promise.all(tasks);
|
||||
await discussion.addReply(domainId, did, this.user._id, content, this.request.ip);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('drid', Types.ObjectID)
|
||||
@param('content', Types.String, isContent)
|
||||
async postTailReply(domainId: string, drid: ObjectID, content: string) {
|
||||
this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
|
||||
this.limitRate('add_discussion', 3600, 30);
|
||||
await discussion.addTailReply(domainId, drid, this.user._id, content, this.request.ip);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('drid', Types.ObjectID)
|
||||
@param('content', Types.String, isContent)
|
||||
async postEditReply(domainId: string, drid: ObjectID, content: string) {
|
||||
if (!(this.ddoc.owner === this.user._id
|
||||
&& this.user.hasPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION))) {
|
||||
if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY);
|
||||
else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY_SELF);
|
||||
}
|
||||
await discussion.editReply(domainId, drid, content);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('drid', Types.ObjectID)
|
||||
async postDeleteReply(domainId: string, drid: ObjectID) {
|
||||
if (!(this.ddoc.owner === this.user._id
|
||||
&& this.user.hasPerm(PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION))) {
|
||||
if (this.drdoc.owner !== this.user._id) {
|
||||
this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY);
|
||||
} else this.checkPerm(PERM.PERM_DELETE_DISCUSSION_SELF);
|
||||
}
|
||||
await discussion.delReply(domainId, drid);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('drid', Types.ObjectID)
|
||||
@param('drrid', Types.ObjectID)
|
||||
@param('content', Types.String, isContent)
|
||||
async postEditTailReply(domainId: string, drid: ObjectID, drrid: ObjectID, content: string) {
|
||||
if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION_REPLY);
|
||||
await discussion.editTailReply(domainId, drid, drrid, content);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('drid', Types.ObjectID)
|
||||
@param('drrid', Types.ObjectID)
|
||||
async postDeleteTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) {
|
||||
if (this.drrdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION_REPLY);
|
||||
await discussion.delTailReply(domainId, drid, drrid);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectID)
|
||||
async postStar(domainId: string, did: ObjectID) {
|
||||
await discussion.setStar(domainId, did, this.user._id, true);
|
||||
this.back({ star: true });
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectID)
|
||||
async postUnstar(domainId: string, did: ObjectID) {
|
||||
await discussion.setStar(domainId, did, this.user._id, false);
|
||||
this.back({ star: false });
|
||||
}
|
||||
}
|
||||
|
||||
class DiscussionDetailRawHandler extends DiscussionHandler {
|
||||
async get() {
|
||||
this.response.type = 'text/markdown';
|
||||
this.response.body = this.ddoc.content;
|
||||
}
|
||||
}
|
||||
|
||||
class DiscussionReplyRawHandler extends DiscussionHandler {
|
||||
async get() {
|
||||
this.response.type = 'text/markdown';
|
||||
this.response.body = this.drdoc.content;
|
||||
}
|
||||
}
|
||||
|
||||
class DiscussionTailReplyRawHandler extends DiscussionHandler {
|
||||
async get() {
|
||||
this.response.type = 'text/markdown';
|
||||
this.response.body = this.drrdoc.content;
|
||||
}
|
||||
}
|
||||
|
||||
class DiscussionEditHandler extends DiscussionHandler {
|
||||
async get() {
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['discussion_main', 'discussion_main'],
|
||||
[this.vnode.title, 'discussion_node', { type: discussion.typeDisplay[this.ddoc.parentType], name: this.ddoc.parentId }, true],
|
||||
[this.ddoc.title, 'discussion_detail', { did: this.ddoc.docId }, true],
|
||||
['discussion_edit', null],
|
||||
];
|
||||
this.response.template = 'discussion_edit.html';
|
||||
this.response.body = { ddoc: this.ddoc, path };
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectID)
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('content', Types.String, isContent)
|
||||
@param('highlight', Types.Boolean)
|
||||
@param('pin', Types.Boolean)
|
||||
async postUpdate(
|
||||
domainId: string, did: ObjectID, title: string, content: string,
|
||||
highlight = false, pin = false,
|
||||
) {
|
||||
if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_DISCUSSION);
|
||||
else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_SELF);
|
||||
if (highlight !== this.ddoc.highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION);
|
||||
if (pin !== this.ddoc.pin) this.checkPerm(PERM.PERM_PIN_DISCUSSION);
|
||||
await discussion.edit(domainId, did, title, content, highlight, pin);
|
||||
this.response.body = { did };
|
||||
this.response.redirect = this.url('discussion_detail', { did });
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectID)
|
||||
async postDelete(domainId: string, did: ObjectID) {
|
||||
if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_DISCUSSION);
|
||||
else this.checkPerm(PERM.PERM_DELETE_DISCUSSION_SELF);
|
||||
await discussion.del(domainId, did);
|
||||
this.response.body = { type: this.ddoc.parentType, parent: this.ddoc.parentId };
|
||||
this.response.redirect = this.url('discussion_node', {
|
||||
type: discussion.typeDisplay[this.ddoc.parentType],
|
||||
name: this.ddoc.parentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('discussion_main', '/discuss', DiscussionMainHandler);
|
||||
Route('discussion_detail', '/discuss/:did', DiscussionDetailHandler);
|
||||
Route('discussion_edit', '/discuss/:did/edit', DiscussionEditHandler);
|
||||
Route('discussion_detail', '/discuss/:did/raw', DiscussionDetailRawHandler);
|
||||
Route('discussion_reply_raw', '/discuss/:did/:drid/raw', DiscussionReplyRawHandler);
|
||||
Route('discussion_tail_reply_raw', '/discuss/:did/:drid/:drrid/raw', DiscussionTailReplyRawHandler);
|
||||
Route('discussion_node', '/discuss/:type/:name', DiscussionNodeHandler);
|
||||
Route('discussion_create', '/discuss/:type/:name/create', DiscussionCreateHandler, PRIV.PRIV_USER_PROFILE, PERM.PERM_CREATE_DISCUSSION);
|
||||
}
|
||||
|
||||
global.Hydro.handler.discussion = apply;
|
||||
@@ -0,0 +1,208 @@
|
||||
import { RoleAlreadyExistError, ValidationError } from '../error';
|
||||
import * as user from '../model/user';
|
||||
import * as domain from '../model/domain';
|
||||
import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
|
||||
import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin';
|
||||
import { gravatar } from '../lib/misc';
|
||||
import paginate from '../lib/paginate';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
|
||||
class DomainRankHandler extends Handler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
async get(domainId: string, page = 1) {
|
||||
const [dudocs, upcount, ucount] = await paginate(
|
||||
domain.getMultiInDomain(domainId).sort({ rp: -1 }),
|
||||
page,
|
||||
100,
|
||||
);
|
||||
let udocs = [];
|
||||
for (const dudoc of dudocs) {
|
||||
udocs.push(user.getById(domainId, dudoc.uid));
|
||||
}
|
||||
udocs = await Promise.all(udocs);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['ranking', null],
|
||||
];
|
||||
this.response.template = 'ranking.html';
|
||||
this.response.body = {
|
||||
udocs, upcount, ucount, page, path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ManageHandler extends Handler {
|
||||
domain: any;
|
||||
|
||||
async prepare({ domainId }) {
|
||||
this.checkPerm(PERM.PERM_EDIT_DOMAIN);
|
||||
this.domain = await domain.get(domainId);
|
||||
}
|
||||
}
|
||||
|
||||
class DomainEditHandler extends ManageHandler {
|
||||
async get() {
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['domain', null],
|
||||
['domain_edit', null],
|
||||
];
|
||||
this.response.template = 'domain_edit.html';
|
||||
this.response.body = { current: this.domain, settings: DOMAIN_SETTINGS, path };
|
||||
}
|
||||
|
||||
async post(args) {
|
||||
const $set = {};
|
||||
for (const key in args) {
|
||||
if (DOMAIN_SETTINGS_BY_KEY[key]) $set[key] = args[key];
|
||||
}
|
||||
await domain.edit(args.domainId, $set);
|
||||
this.response.redirect = this.url('domain_dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
class DomainDashboardHandler extends ManageHandler {
|
||||
async get() {
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['domain', null],
|
||||
['domain_dashboard', null],
|
||||
];
|
||||
this.response.template = 'domain_dashboard.html';
|
||||
this.response.body = { domain: this.domain, path };
|
||||
}
|
||||
}
|
||||
|
||||
class DomainUserHandler extends ManageHandler {
|
||||
async get({ domainId }) {
|
||||
const rudocs = {};
|
||||
const [dudocs, roles] = await Promise.all([
|
||||
domain.getMultiInDomain(domainId, {
|
||||
$and: [
|
||||
{ role: { $ne: 'default' } },
|
||||
{ role: { $ne: null } },
|
||||
],
|
||||
}).toArray(),
|
||||
domain.getRoles(domainId),
|
||||
]);
|
||||
const uids = dudocs.map((dudoc) => dudoc.uid);
|
||||
const udict = await user.getList(domainId, uids);
|
||||
for (const role of roles) rudocs[role._id] = [];
|
||||
for (const dudoc of dudocs) {
|
||||
const ud = udict[dudoc.uid];
|
||||
rudocs[ud.role].push(ud);
|
||||
}
|
||||
const rolesSelect = roles.map((role) => [role._id, role._id]);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['domain', null],
|
||||
['domain_user', null],
|
||||
];
|
||||
this.response.template = 'domain_user.html';
|
||||
this.response.body = {
|
||||
roles, rolesSelect, rudocs, udict, path, domain: this.domain,
|
||||
};
|
||||
}
|
||||
|
||||
@param('uid', Types.Int)
|
||||
@param('role', Types.String)
|
||||
async postSetUser(domainId: string, uid: number, role: string) {
|
||||
await domain.setUserRole(domainId, uid, role);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class DomainPermissionHandler extends ManageHandler {
|
||||
async get({ domainId }) {
|
||||
const roles = await domain.getRoles(domainId);
|
||||
const log2 = (val: bigint) => {
|
||||
// @ts-ignore
|
||||
for (let i = 0n; ; i++) {
|
||||
// @ts-ignore
|
||||
if ((val >> i) === 0n) return parseInt(i.toString(), 10) - 1;
|
||||
}
|
||||
};
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['domain', null],
|
||||
['domain_permission', null],
|
||||
];
|
||||
this.response.template = 'domain_permission.html';
|
||||
this.response.body = {
|
||||
roles, PERMS_BY_FAMILY, domain: this.domain, path, log2,
|
||||
};
|
||||
}
|
||||
|
||||
async post({ domainId }) {
|
||||
const roles = {};
|
||||
for (const role in this.request.body) {
|
||||
if (this.request.body[role] instanceof Array) {
|
||||
const perms = this.request.body[role];
|
||||
// @ts-ignore
|
||||
roles[role] = 0n;
|
||||
// @ts-ignore
|
||||
for (const r of perms) roles[role] += 1n << BigInt(r);
|
||||
}
|
||||
}
|
||||
await domain.setRoles(domainId, roles);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class DomainRoleHandler extends ManageHandler {
|
||||
async get({ domainId }) {
|
||||
const roles = await domain.getRoles(domainId);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['domain', null],
|
||||
['domain_role', null],
|
||||
];
|
||||
this.response.template = 'domain_role.html';
|
||||
this.response.body = { roles, domain: this.domain, path };
|
||||
}
|
||||
|
||||
@param('role', Types.String)
|
||||
async postAdd(domainId: string, role: string) {
|
||||
const roles = await domain.getRoles(this.domain);
|
||||
const rdict: any = {};
|
||||
for (const r of roles) rdict[r._id] = r.perm;
|
||||
if (rdict[role]) throw new RoleAlreadyExistError(role);
|
||||
await domain.addRole(domainId, role, rdict.default.perm);
|
||||
this.back();
|
||||
}
|
||||
|
||||
async postDelete({ domainId, roles }) {
|
||||
for (const role of roles) {
|
||||
if (['root', 'default', 'guest'].includes(role)) {
|
||||
throw new ValidationError('role');
|
||||
}
|
||||
}
|
||||
await domain.deleteRoles(domainId, roles);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class DomainSearchHandler extends Handler {
|
||||
@param('q', Types.String)
|
||||
async get(domainId: string, q: string) {
|
||||
const ddocs = await domain.getPrefixSearch(q, 20);
|
||||
for (let i = 0; i < ddocs.length; i++) {
|
||||
ddocs[i].gravatar = ddocs[i].gravatar ? gravatar(ddocs[i].gravatar) : '/img/team_avatar.png';
|
||||
}
|
||||
this.response.body = ddocs;
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('ranking', '/ranking', DomainRankHandler, PERM.PERM_VIEW_RANKING);
|
||||
Route('domain_dashboard', '/domain/dashboard', DomainDashboardHandler);
|
||||
Route('domain_edit', '/domain/edit', DomainEditHandler);
|
||||
Route('domain_user', '/domain/user', DomainUserHandler);
|
||||
Route('domain_permission', '/domain/permission', DomainPermissionHandler);
|
||||
Route('domain_role', '/domain/role', DomainRoleHandler);
|
||||
Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
|
||||
}
|
||||
|
||||
global.Hydro.handler.domain = apply;
|
||||
@@ -0,0 +1,410 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import {
|
||||
VerifyPasswordError, UserAlreadyExistError, InvalidTokenError,
|
||||
NotFoundError, UserNotFoundError, PermissionError,
|
||||
} from '../error';
|
||||
import * as bus from '../service/bus';
|
||||
import {
|
||||
Route, Connection, Handler, ConnectionHandler, param, Types,
|
||||
} from '../service/server';
|
||||
import * as misc from '../lib/misc';
|
||||
import md5 from '../lib/md5';
|
||||
import * as mail from '../lib/mail';
|
||||
import * as contest from '../model/contest';
|
||||
import * as message from '../model/message';
|
||||
import * as document from '../model/document';
|
||||
import * as system from '../model/system';
|
||||
import * as user from '../model/user';
|
||||
import * as file from '../model/file';
|
||||
import * as setting from '../model/setting';
|
||||
import * as domain from '../model/domain';
|
||||
import * as discussion from '../model/discussion';
|
||||
import * as token from '../model/token';
|
||||
import * as training from '../model/training';
|
||||
import { PERM, PRIV, CONSTANT } from '../model/builtin';
|
||||
import {
|
||||
isContent, isPassword, isEmail, isTitle,
|
||||
} from '../lib/validator';
|
||||
|
||||
const { geoip, useragent } = global.Hydro.lib;
|
||||
|
||||
class HomeHandler extends Handler {
|
||||
async homework(domainId: string) {
|
||||
if (this.user.hasPerm(PERM.PERM_VIEW_HOMEWORK)) {
|
||||
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK)
|
||||
.sort('beginAt', -1)
|
||||
.limit(CONSTANT.HOMEWORK_ON_MAIN)
|
||||
.toArray();
|
||||
const tsdict = await contest.getListStatus(
|
||||
domainId, this.user._id,
|
||||
tdocs.map((tdoc) => tdoc.docId), document.TYPE_HOMEWORK,
|
||||
);
|
||||
return [tdocs, tsdict];
|
||||
}
|
||||
return [[], {}];
|
||||
}
|
||||
|
||||
async contest(domainId: string) {
|
||||
if (this.user.hasPerm(PERM.PERM_VIEW_CONTEST)) {
|
||||
const tdocs = await contest.getMulti(domainId)
|
||||
.sort('beginAt', -1)
|
||||
.limit(CONSTANT.CONTEST_ON_MAIN)
|
||||
.toArray();
|
||||
const tsdict = await contest.getListStatus(
|
||||
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
|
||||
);
|
||||
return [tdocs, tsdict];
|
||||
}
|
||||
return [[], {}];
|
||||
}
|
||||
|
||||
async training(domainId: string) {
|
||||
if (this.user.hasPerm(PERM.PERM_VIEW_TRAINING)) {
|
||||
const tdocs = await training.getMulti(domainId)
|
||||
.sort('_id', 1)
|
||||
.limit(CONSTANT.TRAINING_ON_MAIN)
|
||||
.toArray();
|
||||
const tsdict = await training.getListStatus(
|
||||
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
|
||||
);
|
||||
return [tdocs, tsdict];
|
||||
}
|
||||
return [[], {}];
|
||||
}
|
||||
|
||||
async discussion(domainId: string): Promise<[any[], any]> {
|
||||
if (this.user.hasPerm(PERM.PERM_VIEW_DISCUSSION)) {
|
||||
const ddocs = await discussion.getMulti(domainId)
|
||||
.limit(CONSTANT.DISCUSSION_ON_MAIN)
|
||||
.toArray();
|
||||
const vndict = await discussion.getListVnodes(
|
||||
domainId, ddocs, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN),
|
||||
);
|
||||
return [ddocs, vndict];
|
||||
}
|
||||
return [[], {}];
|
||||
}
|
||||
|
||||
async get({ domainId }) {
|
||||
const [
|
||||
[htdocs, htsdict], [tdocs, tsdict],
|
||||
[trdocs, trsdict], [ddocs, vndict],
|
||||
] = await Promise.all([
|
||||
this.homework(domainId), this.contest(domainId),
|
||||
this.training(domainId), this.discussion(domainId),
|
||||
]);
|
||||
const [udict, dodoc, vnodes] = await Promise.all([
|
||||
user.getList(domainId, ddocs.map((ddoc) => ddoc.owner)),
|
||||
domain.get(domainId),
|
||||
discussion.getNodes(domainId),
|
||||
]);
|
||||
this.response.template = 'main.html';
|
||||
this.response.body = {
|
||||
htdocs,
|
||||
htsdict,
|
||||
tdocs,
|
||||
tsdict,
|
||||
trdocs,
|
||||
trsdict,
|
||||
ddocs,
|
||||
vndict,
|
||||
udict,
|
||||
domain: dodoc,
|
||||
vnodes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class HomeSecurityHandler extends Handler {
|
||||
async get() {
|
||||
// TODO(iceboy): pagination? or limit session count for uid?
|
||||
const sessions = await token.getSessionListByUid(this.user._id);
|
||||
for (const session of sessions) {
|
||||
session.isCurrent = session._id === this.session._id;
|
||||
session._id = md5(session._id);
|
||||
if (useragent) session.updateUa = useragent.parse(session.updateUa || session.createUa || '');
|
||||
if (geoip) {
|
||||
session.updateGeoip = geoip.lookup(
|
||||
session.updateIp || session.createIp,
|
||||
this.translate('geoip_locale'),
|
||||
);
|
||||
}
|
||||
}
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['home_security', null],
|
||||
];
|
||||
this.response.template = 'home_security.html';
|
||||
this.response.body = { sessions, geoipProvider: geoip?.provider, path };
|
||||
if (useragent) this.response.body.icon = useragent.icon;
|
||||
}
|
||||
|
||||
@param('current', Types.String)
|
||||
@param('password', Types.String, isPassword)
|
||||
@param('verifyPassword', Types.String)
|
||||
async postChangePassword(_: string, current: string, password: string, verify: string) {
|
||||
if (password !== verify) throw new VerifyPasswordError();
|
||||
this.user.checkPassword(current);
|
||||
await user.setPassword(this.user._id, password);
|
||||
await token.delByUid(this.user._id);
|
||||
this.response.redirect = this.url('user_login');
|
||||
}
|
||||
|
||||
@param('currentPassword', Types.String)
|
||||
@param('mail', Types.String, isEmail)
|
||||
async postChangeMail(domainId: string, current: string, email: string) {
|
||||
this.limitRate('send_mail', 3600, 30);
|
||||
this.user.checkPassword(current);
|
||||
const udoc = await user.getByEmail(domainId, email);
|
||||
if (udoc) throw new UserAlreadyExistError(email);
|
||||
const [code] = await token.add(
|
||||
token.TYPE_CHANGEMAIL,
|
||||
await system.get('changemail_token_expire_seconds'),
|
||||
{ uid: this.user._id, email },
|
||||
);
|
||||
const m = await this.renderHTML('user_changemail_mail.html', {
|
||||
path: `home/changeMail/${code}`,
|
||||
uname: this.user.uname,
|
||||
url_prefix: await system.get('server.url'),
|
||||
});
|
||||
await mail.sendMail(email, 'Change Email', 'user_changemail_mail', m);
|
||||
this.response.template = 'user_changemail_mail_sent.html';
|
||||
}
|
||||
|
||||
@param('tokenDigest', Types.String)
|
||||
async postDeleteToken(domainId: string, tokenDigest: string) {
|
||||
const sessions = await token.getSessionListByUid(this.user._id);
|
||||
for (const session of sessions) {
|
||||
if (tokenDigest === md5(session._id)) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await token.del(session._id, token.TYPE_SESSION);
|
||||
return this.back();
|
||||
}
|
||||
}
|
||||
throw new InvalidTokenError(tokenDigest);
|
||||
}
|
||||
|
||||
async postDeleteAllTokens() {
|
||||
await token.delByUid(this.user._id);
|
||||
this.response.redirect = this.url('user_login');
|
||||
}
|
||||
}
|
||||
|
||||
class HomeSettingsHandler extends Handler {
|
||||
@param('category', Types.String)
|
||||
async get(domainId: string, category: string) {
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
[`home_${category}`, null],
|
||||
];
|
||||
this.response.template = 'home_settings.html';
|
||||
this.response.body = {
|
||||
category,
|
||||
page_name: `home_${category}`,
|
||||
current: this.user,
|
||||
path,
|
||||
};
|
||||
if (category === 'preference') {
|
||||
this.response.body.settings = setting.PREFERENCE_SETTINGS;
|
||||
} else if (category === 'account') {
|
||||
this.response.body.settings = setting.ACCOUNT_SETTINGS;
|
||||
} else if (category === 'domain') {
|
||||
this.response.body.settings = setting.DOMAIN_USER_SETTINGS;
|
||||
} else throw new NotFoundError();
|
||||
}
|
||||
|
||||
async post(args: any) {
|
||||
const $set = {};
|
||||
if (args.category === 'domain') {
|
||||
for (const key in args) {
|
||||
if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key]
|
||||
&& !(setting.DOMAIN_USER_SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
|
||||
$set[key] = args[key];
|
||||
}
|
||||
}
|
||||
await domain.setUserInDomain(args.domainId, this.user._id, $set);
|
||||
} else {
|
||||
for (const key in args) {
|
||||
if (setting.SETTINGS_BY_KEY[key]
|
||||
&& !(setting.SETTINGS_BY_KEY[key].flag & setting.FLAG_DISABLED)) {
|
||||
$set[key] = args[key];
|
||||
}
|
||||
}
|
||||
await user.setById(this.user._id, $set);
|
||||
}
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class UserChangemailWithCodeHandler extends Handler {
|
||||
@param('code', Types.String)
|
||||
async get(domainId: string, code: string) {
|
||||
const tdoc = await token.get(code, token.TYPE_CHANGEMAIL);
|
||||
if (!tdoc || tdoc.uid !== this.user._id) {
|
||||
throw new InvalidTokenError(code);
|
||||
}
|
||||
const udoc = await user.getByEmail(domainId, tdoc.email);
|
||||
if (udoc) throw new UserAlreadyExistError(tdoc.email);
|
||||
await Promise.all([
|
||||
user.setEmail(this.user._id, tdoc.email),
|
||||
token.del(code, token.TYPE_CHANGEMAIL),
|
||||
]);
|
||||
this.response.redirect = this.url('home_security');
|
||||
}
|
||||
}
|
||||
|
||||
class HomeDomainHandler extends Handler {
|
||||
async get() {
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['home_domain', null],
|
||||
];
|
||||
const dudict = await domain.getDictUserByDomainId(this.user._id);
|
||||
const dids = Object.keys(dudict);
|
||||
const ddocs = await domain.getMulti({ _id: { $in: dids } }).toArray();
|
||||
const canManage = {};
|
||||
for (const ddoc of ddocs) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const udoc = await user.getById(ddoc._id, this.user._id);
|
||||
canManage[ddoc._id] = udoc.hasPerm(PERM.PERM_EDIT_DOMAIN)
|
||||
|| udoc.hasPriv(PRIV.PRIV_MANAGE_ALL_DOMAIN);
|
||||
}
|
||||
this.response.template = 'home_domain.html';
|
||||
this.response.body = {
|
||||
ddocs, dudict, canManage, path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class HomeDomainCreateHandler extends Handler {
|
||||
async get() {
|
||||
this.response.body = {
|
||||
path: [
|
||||
['Hydro', 'homepage'],
|
||||
['domain_create', null],
|
||||
],
|
||||
};
|
||||
this.response.template = 'domain_create.html';
|
||||
}
|
||||
|
||||
@param('id', Types.String)
|
||||
@param('name', Types.String, isTitle)
|
||||
@param('bulletin', Types.String, isContent)
|
||||
@param('gravatar', Types.String, true, isEmail)
|
||||
async post(_: string, id: string, name: string, bulletin: string, gravatar: string) {
|
||||
gravatar = gravatar || this.user.gravatar || this.user.mail || 'guest@hydro.local';
|
||||
const domainId = await domain.add(id, this.user._id, name, bulletin);
|
||||
await domain.edit(domainId, { gravatar });
|
||||
this.response.redirect = this.url('domain_manage', { domainId });
|
||||
this.response.body = { domainId };
|
||||
}
|
||||
}
|
||||
|
||||
class HomeMessagesHandler extends Handler {
|
||||
async get() {
|
||||
// TODO(iceboy): projection, pagination.
|
||||
const messages = await message.getByUser(this.user._id);
|
||||
const udict = await user.getList('system', [
|
||||
...messages.map((mdoc) => mdoc.from),
|
||||
...messages.map((mdoc) => mdoc.to),
|
||||
]);
|
||||
// TODO(twd2): improve here:
|
||||
const parsed = {};
|
||||
for (const m of messages) {
|
||||
const target = m.from === this.user._id ? m.to : m.from;
|
||||
if (!parsed[target]) {
|
||||
parsed[target] = {
|
||||
_id: target,
|
||||
udoc: { ...udict[target], gravatar: misc.gravatar(udict[target].gravatar) },
|
||||
messages: [],
|
||||
};
|
||||
}
|
||||
parsed[target].messages.push(m);
|
||||
}
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['home_messages', null],
|
||||
];
|
||||
this.response.body = { messages: parsed, path };
|
||||
this.response.template = 'home_messages.html';
|
||||
}
|
||||
|
||||
@param('uid', Types.Int)
|
||||
@param('content', Types.String, isContent)
|
||||
async postSend(domainId: string, uid: number, content: string) {
|
||||
const udoc = await user.getById('system', uid);
|
||||
if (!udoc) throw new UserNotFoundError(uid);
|
||||
if (udoc.gravatar) udoc.gravatar = misc.gravatar(udoc.gravatar);
|
||||
const mdoc = await message.send(this.user._id, uid, content, message.FLAG_UNREAD);
|
||||
this.back({ mdoc, udoc });
|
||||
}
|
||||
|
||||
@param('messageId', Types.ObjectID)
|
||||
async postDeleteMessage(domainId: string, messageId: ObjectID) {
|
||||
const msg = await message.get(messageId);
|
||||
if ([msg.from, msg.to].includes(this.user._id)) await message.del(messageId);
|
||||
else throw new PermissionError();
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('messageId', Types.ObjectID)
|
||||
async postRead(domainId: string, messageId: ObjectID) {
|
||||
const msg = await message.get(messageId);
|
||||
if ([msg.from, msg.to].includes(this.user._id)) {
|
||||
await message.setFlag(messageId, message.FLAG_UNREAD);
|
||||
} else throw new PermissionError();
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class HomeMessagesConnectionHandler extends ConnectionHandler {
|
||||
id: string;
|
||||
|
||||
async prepare() {
|
||||
bus.subscribe([`user_message-${this.user._id}`], this.onMessageReceived.bind(this));
|
||||
}
|
||||
|
||||
async onMessageReceived(e: any) {
|
||||
this.send(e.value);
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
bus.unsubscribe([`user_message-${this.user._id}`], this.id);
|
||||
}
|
||||
}
|
||||
|
||||
class HomeFileHandler extends Handler {
|
||||
async get() {
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['domain_file', null],
|
||||
];
|
||||
const ufdocs = await file.getMulti({ owner: this.user._id }).toArray();
|
||||
this.response.template = 'home_file.html';
|
||||
this.response.body = { ufdocs, path };
|
||||
}
|
||||
|
||||
@param('ufid', Types.ObjectID)
|
||||
async postDelete(domainId: string, ufid: ObjectID) {
|
||||
const ufdoc = await file.getMeta(ufid);
|
||||
if (ufdoc.owner !== this.user._id) this.checkPriv(PRIV.PRIV_DELETE_FILE);
|
||||
else this.checkPriv(PRIV.PRIV_DELETE_FILE_SELF);
|
||||
const result = await file.del(ufdoc._id);
|
||||
if (result) await user.inc(this.user._id, 'usage', -ufdoc.size);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
async function apply() {
|
||||
Route('homepage', '/', HomeHandler);
|
||||
Route('home_security', '/home/security', HomeSecurityHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Route('user_changemail_with_code', '/home/changeMail/:code', UserChangemailWithCodeHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Route('home_settings', '/home/settings/:category', HomeSettingsHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Route('home_domain', '/home/domain', HomeDomainHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Route('home_domain_create', '/home/domain/create', HomeDomainCreateHandler, PRIV.PRIV_CREATE_DOMAIN);
|
||||
Route('home_messages', '/home/messages', HomeMessagesHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Route('home_file', '/home/file', HomeFileHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Connection('home_messages_conn', '/home/messages-conn', HomeMessagesConnectionHandler, PRIV.PRIV_USER_PROFILE);
|
||||
}
|
||||
|
||||
global.Hydro.handler.home = module.exports = apply;
|
||||
@@ -0,0 +1,416 @@
|
||||
import yaml from 'js-yaml';
|
||||
import moment from 'moment-timezone';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import AdmZip from 'adm-zip';
|
||||
import { isSafeInteger } from 'lodash';
|
||||
import {
|
||||
ValidationError, HomeworkNotLiveError, ProblemNotFoundError,
|
||||
HomeworkNotAttendedError,
|
||||
} from '../error';
|
||||
import { PenaltyRules } from '../interface';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import * as bus from '../service/bus';
|
||||
import { PERM, CONSTANT } from '../model/builtin';
|
||||
import * as user from '../model/user';
|
||||
import * as contest from '../model/contest';
|
||||
import * as discussion from '../model/discussion';
|
||||
import * as problem from '../model/problem';
|
||||
import * as record from '../model/record';
|
||||
import * as document from '../model/document';
|
||||
import paginate from '../lib/paginate';
|
||||
import { isTitle, isContent } from '../lib/validator';
|
||||
|
||||
const validatePenaltyRules = (input: string) => {
|
||||
const s = yaml.safeLoad(input);
|
||||
return s;
|
||||
};
|
||||
const convertPenaltyRules = validatePenaltyRules;
|
||||
|
||||
const HomeworkHandler = contest.ContestHandlerMixin(Handler);
|
||||
|
||||
class HomeworkMainHandler extends HomeworkHandler {
|
||||
async get({ domainId }) {
|
||||
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK).toArray();
|
||||
const calendar = [];
|
||||
for (const tdoc of tdocs) {
|
||||
const cal = { ...tdoc, url: this.url('homework_detail', { tid: tdoc.docId }) };
|
||||
if (contest.isExtended(tdoc) || contest.isDone(tdoc)) {
|
||||
cal.endAt = tdoc.endAt;
|
||||
cal.penaltySince = tdoc.penaltySince;
|
||||
} else {
|
||||
cal.endAt = tdoc.penaltySince;
|
||||
}
|
||||
calendar.push(cal);
|
||||
}
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', null],
|
||||
];
|
||||
this.response.body = { tdocs, calendar, path };
|
||||
this.response.template = 'homework_main.html';
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkDetailHandler extends HomeworkHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
async get(domainId: string, tid: ObjectID, page = 1) {
|
||||
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
|
||||
const [tsdoc, pdict] = await Promise.all([
|
||||
contest.getStatus(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK),
|
||||
problem.getList(domainId, tdoc.pids, true),
|
||||
]);
|
||||
const psdict = {};
|
||||
let rdict = {};
|
||||
let attended = false;
|
||||
if (tsdoc) {
|
||||
attended = tsdoc.attend === 1;
|
||||
for (const pdetail of tsdoc.journal || []) {
|
||||
psdict[pdetail.pid] = pdetail;
|
||||
rdict[pdetail.rid] = { _id: pdetail.rid };
|
||||
}
|
||||
if (this.canShowRecord(tdoc) && tsdoc.journal) {
|
||||
rdict = await record.getList(
|
||||
domainId,
|
||||
tsdoc.journal.map((pdetail) => pdetail.rid),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
// discussion
|
||||
const [ddocs, dpcount, dcount] = await paginate(
|
||||
discussion.getMulti(domainId, { parentType: tdoc.docType, parentId: tdoc.docId }),
|
||||
page,
|
||||
CONSTANT.DISCUSSION_PER_PAGE,
|
||||
);
|
||||
const uids = ddocs.map((ddoc) => ddoc.owner);
|
||||
uids.push(tdoc.owner);
|
||||
const udict = await user.getList(domainId, uids);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', 'homework_main'],
|
||||
[tdoc.title, null, null, true],
|
||||
];
|
||||
this.response.template = 'homework_detail.html';
|
||||
this.response.body = {
|
||||
tdoc, tsdoc, attended, udict, pdict, psdict, rdict, ddocs, page, dpcount, dcount, path,
|
||||
};
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async postAttend(domainId: string, tid: ObjectID) {
|
||||
this.checkPerm(PERM.PERM_ATTEND_HOMEWORK);
|
||||
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
|
||||
if (contest.isDone(tdoc)) throw new HomeworkNotLiveError(tdoc.docId);
|
||||
await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkDetailProblemHandler extends HomeworkHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('pid', Types.UnsignedInt)
|
||||
async prepare(domainId: string, tid: ObjectID, pid: number) {
|
||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM);
|
||||
[this.tdoc, this.pdoc, this.tsdoc] = await Promise.all([
|
||||
contest.get(domainId, tid, document.TYPE_HOMEWORK),
|
||||
problem.get(domainId, pid, this.user._id),
|
||||
contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK),
|
||||
]);
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('pid', Types.UnsignedInt)
|
||||
async get(domainId: string, tid: ObjectID, pid: number) {
|
||||
const udoc = await user.getById(domainId, this.tdoc.owner);
|
||||
const attended = this.tsdoc && this.tsdoc.attend === 1;
|
||||
if (!contest.isDone(this.tdoc)) {
|
||||
if (!attended) throw new HomeworkNotAttendedError(tid);
|
||||
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
|
||||
}
|
||||
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', 'homework_main'],
|
||||
[this.tdoc.title, 'homework_detail', { tid }, true],
|
||||
[this.pdoc.title, null, null, true],
|
||||
];
|
||||
this.response.template = 'problem_detail.html';
|
||||
this.response.body = {
|
||||
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('pid', Types.UnsignedInt)
|
||||
async get(domainId: string, tid: ObjectID, pid: number) {
|
||||
const udoc = await user.getById(domainId, this.tdoc.owner);
|
||||
const attended = this.tsdoc && this.tsdoc.attend === 1;
|
||||
if (!attended) throw new HomeworkNotAttendedError(tid);
|
||||
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
|
||||
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid);
|
||||
const rdocs = this.canShowRecord(this.tdoc)
|
||||
? await record.getUserInProblemMulti(domainId, this.user._id, pid, true)
|
||||
.sort('_id', -1).limit(10).toArray()
|
||||
: [];
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', 'homework_main'],
|
||||
[this.tdoc.title, 'homework_detail', { tid }, true],
|
||||
[this.pdoc.title, 'homework_detail_problem', { tid, pid }, true],
|
||||
['homework_detail_problem_submit', null],
|
||||
];
|
||||
this.response.template = 'problem_submit.html';
|
||||
this.response.body = {
|
||||
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path, rdocs,
|
||||
};
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('pid', Types.UnsignedInt)
|
||||
@param('code', Types.String)
|
||||
@param('lang', Types.String)
|
||||
async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) {
|
||||
this.limitRate('add_record', 3600, 100);
|
||||
const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK);
|
||||
if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid);
|
||||
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
|
||||
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid);
|
||||
const rid = await record.add(domainId, pid, this.user._id, lang, code, true, {
|
||||
type: document.TYPE_HOMEWORK,
|
||||
tid,
|
||||
});
|
||||
const [rdoc] = await Promise.all([
|
||||
record.get(domainId, rid),
|
||||
contest.updateStatus(domainId, tid, this.user._id,
|
||||
rid, pid, false, 0, document.TYPE_HOMEWORK),
|
||||
]);
|
||||
bus.publish('record_change', rdoc);
|
||||
this.response.body = { tid, rid };
|
||||
if (this.canShowRecord(this.tdoc)) this.response.redirect = this.url('record_detail', { rid });
|
||||
else this.response.redirect = this.url('homework_detail', { tid });
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkCreateHandler extends HomeworkHandler {
|
||||
async get() {
|
||||
const beginAt = moment().add(1, 'day');
|
||||
console.log(beginAt, beginAt.tz(this.user.timeZone));
|
||||
const penaltySince = beginAt.clone().add(7, 'days');
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', 'homework_main'],
|
||||
['homework_create', null],
|
||||
];
|
||||
this.response.template = 'homework_edit.html';
|
||||
this.response.body = {
|
||||
path,
|
||||
dateBeginText: beginAt.tz(this.user.timeZone).format('YYYY-M-D'),
|
||||
timeBeginText: '00:00',
|
||||
datePenaltyText: penaltySince.tz(this.user.timeZone).format('YYYY-M-D'),
|
||||
timePenaltyText: '23:59',
|
||||
pids: '1000, 1001',
|
||||
extensionDays: 1,
|
||||
page_name: 'homework_create',
|
||||
};
|
||||
}
|
||||
|
||||
@param('beginAtDate', Types.Date)
|
||||
@param('beginAtTime', Types.Time)
|
||||
@param('penaltySinceDate', Types.Date)
|
||||
@param('penaltySinceTime', Types.Time)
|
||||
@param('extensionDays', Types.Float)
|
||||
@param('penaltyRules', Types.String, validatePenaltyRules, convertPenaltyRules)
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('content', Types.String, isContent)
|
||||
@param('pids', Types.String)
|
||||
@param('rated', Types.Boolean)
|
||||
async post(
|
||||
domainId: string, beginAtDate: string, beginAtTime: string,
|
||||
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
|
||||
penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false,
|
||||
) {
|
||||
const pids = _pids.split(',').map((i) => {
|
||||
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
|
||||
return i;
|
||||
});
|
||||
const beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
|
||||
if (!beginAt.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime');
|
||||
const penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone);
|
||||
if (!penaltySince.isValid()) throw new ValidationError('endAtDate', 'endAtTime');
|
||||
const endAt = penaltySince.clone().add(extensionDays, 'days');
|
||||
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
|
||||
if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays');
|
||||
await this.verifyProblems(domainId, pids);
|
||||
const tid = await contest.add(domainId, title, content, this.user._id,
|
||||
'homework', beginAt.toDate(), endAt.toDate(), pids, rated,
|
||||
{ penaltySince: penaltySince.toDate(), penaltyRules }, document.TYPE_HOMEWORK);
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('homework_detail', { tid });
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkEditHandler extends HomeworkHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
|
||||
if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
|
||||
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
|
||||
const extensionDays = Math.round(
|
||||
(tdoc.endAt.getTime() - tdoc.penaltySince.getTime()) / 36000 / 24,
|
||||
) / 100;
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', 'homework_main'],
|
||||
[tdoc.title, 'homework_detail', { tid }, true],
|
||||
['homework_edit', null],
|
||||
];
|
||||
const beginAt = moment(tdoc.beginAt).tz(this.user.timeZone);
|
||||
const penaltySince = moment(tdoc.penaltySince).tz(this.user.timeZone);
|
||||
this.response.template = 'homework_edit.html';
|
||||
this.response.body = {
|
||||
tdoc,
|
||||
dateBeginText: beginAt.format('YYYY-M-D'),
|
||||
timeBeginText: beginAt.format('hh:mm'),
|
||||
datePenaltyText: penaltySince.format('YYYY-M-D'),
|
||||
timePenaltyText: penaltySince.format('hh:mm'),
|
||||
extensionDays,
|
||||
penaltyRules: yaml.safeDump(tdoc.penaltyRules),
|
||||
pids: tdoc.pids.join(','),
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('beginAtDate', Types.Date)
|
||||
@param('beginAtTime', Types.Time)
|
||||
@param('penaltySinceDate', Types.Date)
|
||||
@param('penaltySinceTime', Types.Time)
|
||||
@param('extensionDays', Types.Float)
|
||||
@param('penaltyRules', Types.String, validatePenaltyRules, convertPenaltyRules)
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('content', Types.String, isContent)
|
||||
@param('pids', Types.String)
|
||||
@param('rated', Types.Boolean)
|
||||
async post(
|
||||
domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string,
|
||||
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
|
||||
penaltyRules: string, title: string, content: string, _pids: string, rated = false,
|
||||
) {
|
||||
const pids = _pids.split(',').map((i) => {
|
||||
if (isSafeInteger(parseInt(i, 10))) return parseInt(i, 10);
|
||||
return i;
|
||||
});
|
||||
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
|
||||
if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
|
||||
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
|
||||
let beginAt;
|
||||
let penaltySince;
|
||||
try {
|
||||
beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
|
||||
} catch (e) {
|
||||
throw new ValidationError('beginAtDate', 'beginAtTime');
|
||||
}
|
||||
try {
|
||||
penaltySince = moment.tz(`${penaltySinceDate} ${penaltySinceTime}`, this.user.timeZone);
|
||||
} catch (e) {
|
||||
throw new ValidationError('endAtDate', 'endAtTime');
|
||||
}
|
||||
let endAt = penaltySince.clone().add(extensionDays, 'days');
|
||||
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
|
||||
await this.verifyProblems(domainId, pids);
|
||||
beginAt = beginAt.toDate();
|
||||
endAt = endAt.toDate();
|
||||
penaltySince = penaltySince.toDate();
|
||||
await contest.edit(domainId, tid, {
|
||||
title, content, beginAt, endAt, pids, penaltySince, penaltyRules, rated,
|
||||
}, document.TYPE_HOMEWORK);
|
||||
if (tdoc.beginAt !== beginAt
|
||||
|| tdoc.endAt !== endAt
|
||||
|| tdoc.penaltySince !== penaltySince
|
||||
|| new Set(tdoc.pids) !== new Set(pids)) {
|
||||
await contest.recalcStatus(domainId, tdoc.docId, document.TYPE_HOMEWORK);
|
||||
}
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('homework_detail', { tid });
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkScoreboardHandler extends HomeworkHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
console.log(123);
|
||||
const [tdoc, rows, udict] = await this.getScoreboard(
|
||||
domainId, tid, false, document.TYPE_HOMEWORK,
|
||||
);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', 'homework_main'],
|
||||
[tdoc.title, 'homework_detail', { tid }, true],
|
||||
['homework_scoreboard', null],
|
||||
];
|
||||
this.response.template = 'contest_scoreboard.html';
|
||||
this.response.body = {
|
||||
tdoc, rows, path, udict,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkScoreboardDownloadHandler extends HomeworkHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('ext', Types.String)
|
||||
async get(domainId: string, tid: ObjectID, ext: string) {
|
||||
const getContent = {
|
||||
csv: (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`,
|
||||
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
|
||||
};
|
||||
if (!getContent[ext]) throw new ValidationError('ext');
|
||||
const [, rows] = await this.getScoreboard(domainId, tid, true, document.TYPE_CONTEST);
|
||||
this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkCodeHandler extends HomeworkHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
|
||||
this.limitRate('homework_code', 3600, 60);
|
||||
const [tdoc, tsdocs] = await contest.getAndListStatus(
|
||||
domainId, tid, document.TYPE_HOMEWORK,
|
||||
);
|
||||
const rnames = {};
|
||||
for (const tsdoc of tsdocs) {
|
||||
for (const pdetail of tsdoc.detail || []) {
|
||||
rnames[pdetail.rid] = `U${tsdoc.uid}_P${pdetail.pid}_R${pdetail.rid}`;
|
||||
}
|
||||
}
|
||||
const zip = new AdmZip();
|
||||
const rdocs = await record.getMulti(domainId, {
|
||||
_id: {
|
||||
$in: Array.from(Object.keys(rnames)).map((id) => new ObjectID(id)),
|
||||
},
|
||||
}).toArray();
|
||||
for (const rdoc of rdocs) {
|
||||
zip.addFile(`${rnames[rdoc._id]}.${rdoc.lang}`, rdoc.code);
|
||||
}
|
||||
await this.binary(zip.toBuffer(), `${tdoc.title}.zip`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('homework_main', '/homework', HomeworkMainHandler, PERM.PERM_VIEW_HOMEWORK);
|
||||
Route('homework_create', '/homework/create', HomeworkCreateHandler, PERM.PERM_CREATE_HOMEWORK);
|
||||
Route('homework_detail', '/homework/:tid', HomeworkDetailHandler, PERM.PERM_VIEW_HOMEWORK);
|
||||
Route('homework_scoreboard', '/homework/:tid/scoreboard', HomeworkScoreboardHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD);
|
||||
Route('homework_scoreboard', '/homework/:tid/scoreboard/download/:ext', HomeworkScoreboardDownloadHandler, PERM.PERM_VIEW_HOMEWORK_SCOREBOARD);
|
||||
Route('homework_detail_problem', '/homework/:tid/p/:pid', HomeworkDetailProblemHandler, PERM.PERM_VIEW_HOMEWORK);
|
||||
Route('homework_detail_problem_submit', '/homework/:tid/p/:pid/submit', HomeworkDetailProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
||||
Route('homework_code', '/homework/:tid/code', HomeworkCodeHandler, PERM.PERM_VIEW_HOMEWORK);
|
||||
Route('homework_edit', '/homework/:tid/edit', HomeworkEditHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.homework = apply;
|
||||
@@ -0,0 +1,61 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { ValidationError } from '../error';
|
||||
import * as problem from '../model/problem';
|
||||
import { PERM } from '../model/builtin';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import { isPid } from '../lib/validator';
|
||||
|
||||
class ProblemImportHandler extends Handler {
|
||||
async prepare() {
|
||||
this.checkPerm(PERM.PERM_CREATE_PROBLEM);
|
||||
}
|
||||
|
||||
async get() {
|
||||
this.response.template = 'problem_import.html';
|
||||
this.response.body = {
|
||||
path: [
|
||||
['Hydro', 'homepage'],
|
||||
['problem_main', 'problem_main'],
|
||||
['problem_import', null],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@param('url', Types.String)
|
||||
@param('pid', Types.String, isPid)
|
||||
@param('hidden', Types.Boolean)
|
||||
@param('remoteType', Types.String)
|
||||
async post(domainId: string, url: string, pid: string, hidden = false, remoteType: string) {
|
||||
if (typeof global.Hydro.lib[`import.${remoteType}`] !== 'function') {
|
||||
throw new ValidationError('remoteType');
|
||||
}
|
||||
const [pdoc, testdata] = await global.Hydro.lib[`import.${remoteType}`](url, this);
|
||||
if (hidden) pdoc.hidden = true;
|
||||
const docId = await problem.add(
|
||||
domainId, pid || pdoc.pid, pdoc.title, pdoc.content, this.user._id,
|
||||
pdoc.tag || [], pdoc.category || [], null, pdoc.hidden,
|
||||
);
|
||||
if (testdata) {
|
||||
const file = path.resolve(os.tmpdir(), 'hydro', `import_${domainId}_${pid || pdoc.pid || docId}.zip`);
|
||||
const w = fs.createWriteStream(file);
|
||||
await new Promise((resolve, reject) => {
|
||||
w.on('finish', resolve);
|
||||
w.on('error', reject);
|
||||
testdata.pipe(w);
|
||||
});
|
||||
await problem.setTestdata(domainId, docId, file);
|
||||
}
|
||||
this.response.body = { pid: pid || docId };
|
||||
this.response.redirect = this.url('problem_settings', { pid: pid || docId });
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('problem_import', '/problem/import', ProblemImportHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.import = apply;
|
||||
@@ -0,0 +1,146 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { JudgeResultBody, Rdoc } from '../interface';
|
||||
import * as record from '../model/record';
|
||||
import * as problem from '../model/problem';
|
||||
import * as builtin from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import * as domain from '../model/domain';
|
||||
import * as task from '../model/task';
|
||||
import * as bus from '../service/bus';
|
||||
import {
|
||||
Route, Handler, Connection, ConnectionHandler,
|
||||
} from '../service/server';
|
||||
|
||||
async function _postJudge(rdoc: Rdoc) {
|
||||
const accept = rdoc.status === builtin.STATUS.STATUS_ACCEPTED;
|
||||
const tasks = [];
|
||||
if (rdoc.contest) {
|
||||
tasks.push(
|
||||
contest.updateStatus(
|
||||
rdoc.domainId, rdoc.contest.tid, rdoc.uid,
|
||||
rdoc._id, rdoc.pid, accept, rdoc.score, rdoc.contest.type,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (await problem.updateStatus(rdoc.domainId, rdoc.pid, rdoc.uid, rdoc._id, rdoc.status)) {
|
||||
if (accept && !rdoc.rejudged) {
|
||||
tasks.push(
|
||||
problem.inc(rdoc.domainId, rdoc.pid, 'nAccept', 1),
|
||||
domain.incUserInDomain(rdoc.domainId, rdoc.uid, 'nAccept', 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
export async function next(body: JudgeResultBody) {
|
||||
if (body.rid) body.rid = new ObjectID(body.rid);
|
||||
let rdoc = await record.get(body.domainId, body.rid);
|
||||
const $set: any = {};
|
||||
const $push: any = {};
|
||||
if (body.case) {
|
||||
const c: any = {};
|
||||
c.memory = body.case.memory;
|
||||
c.time = body.case.time;
|
||||
c.message = body.case.message;
|
||||
c.status = body.case.status;
|
||||
rdoc.testCases.push(c);
|
||||
$push.testCases = c;
|
||||
}
|
||||
if (body.message) {
|
||||
rdoc.judgeTexts.push(body.message);
|
||||
$push.judgeTexts = body.message;
|
||||
}
|
||||
if (body.compilerText) {
|
||||
rdoc.compilerTexts.push(body.compilerText);
|
||||
$push.compilerTexts = body.compilerText;
|
||||
}
|
||||
if (body.status) $set.status = body.status;
|
||||
if (body.score) $set.score = body.score;
|
||||
if (body.time) $set.time = body.time;
|
||||
if (body.memory) $set.memory = body.memory;
|
||||
if (body.progress) $set.progress = body.progress;
|
||||
rdoc = await record.update(body.domainId, body.rid, $set, $push);
|
||||
bus.publish('record_change', { rdoc, $set, $push });
|
||||
}
|
||||
|
||||
export async function end(body: JudgeResultBody) {
|
||||
if (body.rid) body.rid = new ObjectID(body.rid);
|
||||
let rdoc = await record.get(body.domainId, body.rid);
|
||||
const $set: any = {};
|
||||
const $push: any = {};
|
||||
const $unset = { progress: '' };
|
||||
if (body.message) {
|
||||
rdoc.judgeTexts.push(body.message);
|
||||
$push.judgeTexts = body.message;
|
||||
}
|
||||
if (body.compilerText) {
|
||||
rdoc.compilerTexts.push(body.compilerText);
|
||||
$push.compilerTexts = body.compilerText;
|
||||
}
|
||||
if (body.status) $set.status = body.status;
|
||||
if (body.score) $set.score = body.score;
|
||||
if (body.stdout) $set.stdout = body.stdout;
|
||||
if (body.stderr) $set.stderr = body.stderr;
|
||||
if (body.time) $set.time = body.time;
|
||||
if (body.memory) $set.memory = body.memory;
|
||||
$set.judgeAt = new Date();
|
||||
$set.judger = body.judger;
|
||||
rdoc = await record.update(body.domainId, body.rid, $set, $push, $unset);
|
||||
await _postJudge(rdoc);
|
||||
bus.publish('record_change', { rdoc }); // trigger a full update
|
||||
}
|
||||
|
||||
class JudgeHandler extends Handler {
|
||||
async get({ check = false }) {
|
||||
if (check) return;
|
||||
const tasks = [];
|
||||
let t = await task.getFirst({ type: 'judge' });
|
||||
while (t) {
|
||||
tasks.push(t);
|
||||
t = await task.getFirst({ type: 'judge' }); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
this.response.body = { tasks };
|
||||
}
|
||||
|
||||
async postNext() {
|
||||
await next(this.request.body);
|
||||
}
|
||||
|
||||
async postEnd() {
|
||||
this.request.body.judger = this.user._id;
|
||||
await end(this.request.body);
|
||||
}
|
||||
}
|
||||
|
||||
class JudgeConnectionHandler extends ConnectionHandler {
|
||||
processing: any;
|
||||
|
||||
async message(msg) {
|
||||
if (msg.key === 'next') await next(msg);
|
||||
else if (msg.key === 'end') {
|
||||
await end({ judger: this.user._id, ...msg });
|
||||
this.processing = null;
|
||||
const t = await task.getFirst({ type: 'judge' });
|
||||
this.send({ task: t });
|
||||
this.processing = t;
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.processing) {
|
||||
await record.reset(this.processing.domainId, this.processing.rid, false);
|
||||
task.add(this.processing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('judge', '/judge', JudgeHandler, builtin.PRIV.PRIV_JUDGE);
|
||||
Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE);
|
||||
}
|
||||
|
||||
apply.next = next;
|
||||
apply.end = end;
|
||||
|
||||
global.Hydro.handler.judge = apply;
|
||||
@@ -0,0 +1,170 @@
|
||||
import * as judge from './judge';
|
||||
import { ValidationError } from '../error';
|
||||
import * as check from '../check';
|
||||
import * as setting from '../model/setting';
|
||||
import * as system from '../model/system';
|
||||
import { STATUS, PRIV } from '../model/builtin';
|
||||
import * as record from '../model/record';
|
||||
import {
|
||||
Route, Connection, Handler, ConnectionHandler, param, Types,
|
||||
} from '../service/server';
|
||||
import { validate } from '../lib/validator';
|
||||
|
||||
function set(key, value) {
|
||||
if (setting.SYSTEM_SETTINGS_BY_KEY[key]) {
|
||||
const s = setting.SYSTEM_SETTINGS_BY_KEY[key];
|
||||
if (s.flag & setting.FLAG_DISABLED) return undefined;
|
||||
if (s.flag & setting.FLAG_SECRET && !value) return undefined;
|
||||
if (s.type === 'boolean') {
|
||||
if (value === 'on') return true;
|
||||
return false;
|
||||
}
|
||||
if (s.type === 'number') {
|
||||
if (!Number.isSafeInteger(parseInt(value, 10))) {
|
||||
throw new ValidationError(key);
|
||||
}
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
class SystemHandler extends Handler {
|
||||
async prepare() {
|
||||
this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
|
||||
this.response.body = {
|
||||
path: [
|
||||
['Hydro', 'homepage'],
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SystemMainHandler extends SystemHandler {
|
||||
async get() {
|
||||
this.response.redirect = '/manage/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCheckConnHandler extends ConnectionHandler {
|
||||
id: any;
|
||||
|
||||
async prepare() {
|
||||
this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
|
||||
await this.check();
|
||||
}
|
||||
|
||||
async check() {
|
||||
const log = (payload: any) => this.send({ type: 'log', payload });
|
||||
const warn = (payload: any) => this.send({ type: 'warn', payload });
|
||||
const error = (payload: any) => this.send({ type: 'error', payload });
|
||||
await check.start(this, log, warn, error, (id) => { this.id = id; });
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
check.cancel(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemDashboardHandler extends SystemHandler {
|
||||
async get() {
|
||||
this.response.template = 'manage_dashboard.html';
|
||||
this.response.body.path.push(['manage_dashboard', null]);
|
||||
}
|
||||
|
||||
async postRestart() {
|
||||
const count = await system.get('server.worker');
|
||||
process.send({ event: 'restart', count });
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class SystemScriptHandler extends SystemHandler {
|
||||
async get() {
|
||||
this.response.template = 'manage_script.html';
|
||||
this.response.body.scripts = global.Hydro.script;
|
||||
this.response.body.path.push(['manage_script', null]);
|
||||
}
|
||||
|
||||
@param('id', Types.String)
|
||||
@param('args', Types.String, true)
|
||||
async post(domainId: string, id: string, raw = '{}') {
|
||||
if (!global.Hydro.script[id]) throw new ValidationError('id');
|
||||
const args = JSON.parse(raw);
|
||||
validate(global.Hydro.script[id].validate, args);
|
||||
const rid = await record.add(domainId, -1, this.user._id, '-', id, false, { input: 'raw' });
|
||||
const report = (data) => judge.next({ domainId, rid, ...data });
|
||||
report({ message: `Running script: ${id}`, status: STATUS.STATUS_JUDGING });
|
||||
const start = new Date().getTime();
|
||||
// Maybe async?
|
||||
global.Hydro.script[id].run(args, report)
|
||||
.then((ret: any) => {
|
||||
const time = new Date().getTime() - start;
|
||||
judge.end({
|
||||
domainId,
|
||||
rid,
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
message: ret.toString(),
|
||||
judger: 1,
|
||||
time,
|
||||
memory: 0,
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
const time = new Date().getTime() - start;
|
||||
judge.end({
|
||||
domainId,
|
||||
rid,
|
||||
status: STATUS.STATUS_SYSTEM_ERROR,
|
||||
message: `${err}\n${err.stack}`,
|
||||
judger: 1,
|
||||
time,
|
||||
memory: 0,
|
||||
});
|
||||
});
|
||||
this.response.body = { rid };
|
||||
this.response.redirect = this.url('record_detail', { rid });
|
||||
}
|
||||
}
|
||||
|
||||
class SystemSettingHandler extends SystemHandler {
|
||||
async get() {
|
||||
this.response.template = 'manage_setting.html';
|
||||
this.response.body.path.push(['manage_setting', null]);
|
||||
this.response.body.current = {};
|
||||
this.response.body.settings = setting.SYSTEM_SETTINGS;
|
||||
for (const s of this.response.body.settings) {
|
||||
// FIXME no-await-in-loop
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
this.response.body.current[s.key] = await system.get(s.key);
|
||||
}
|
||||
}
|
||||
|
||||
async post(args: any) {
|
||||
const tasks = [];
|
||||
for (const key in args) {
|
||||
if (typeof args[key] === 'object') {
|
||||
for (const subkey in args[key]) {
|
||||
if (typeof set(`${key}.${subkey}`, args[key][subkey]) !== 'undefined') {
|
||||
tasks.push(system.set(`${key}.${subkey}`, set(`${key}.${subkey}`, args[key][subkey])));
|
||||
}
|
||||
}
|
||||
} else if (typeof set(key, args[key]) !== 'undefined') {
|
||||
tasks.push(system.set(key, set(key, args[key])));
|
||||
}
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
async function apply() {
|
||||
Route('manage', '/manage', SystemMainHandler);
|
||||
Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
|
||||
Route('manage_script', '/manage/script', SystemScriptHandler);
|
||||
Route('manage_setting', '/manage/setting', SystemSettingHandler);
|
||||
Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.manage = apply;
|
||||
@@ -0,0 +1,114 @@
|
||||
/* eslint-disable camelcase */
|
||||
import fs from 'fs';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { PRIV } from '../model/builtin';
|
||||
import * as system from '../model/system';
|
||||
import * as file from '../model/file';
|
||||
import * as user from '../model/user';
|
||||
import * as db from '../service/db';
|
||||
import {
|
||||
Route, Handler, Types, param, multipart,
|
||||
} from '../service/server';
|
||||
import { BadRequestError } from '../error';
|
||||
|
||||
const coll = db.collection('status');
|
||||
|
||||
class FileDownloadHandler extends Handler {
|
||||
@param('fileId', Types.ObjectID)
|
||||
@param('secret', Types.String)
|
||||
@param('name', Types.String, true)
|
||||
async get(domainId: string, fileId: ObjectID, secret: string, name: string) {
|
||||
if (name) name = Buffer.from(name, 'base64').toString();
|
||||
this.response.attachment(name || fileId.toHexString());
|
||||
this.response.body = await file.getWithSecret(fileId, secret);
|
||||
}
|
||||
}
|
||||
|
||||
class FileUploadHandler extends Handler {
|
||||
async getQuota() {
|
||||
let quota = await system.get('user.quota');
|
||||
if (this.user.hasPriv(PRIV.PRIV_UNLIMITED_QUOTA)) {
|
||||
quota = 2 ** 63 - 1;
|
||||
}
|
||||
return quota;
|
||||
}
|
||||
|
||||
async get() {
|
||||
this.response.template = 'fs_upload.html';
|
||||
this.response.body = { fdoc: null, usage: this.user.usage, quota: await this.getQuota() };
|
||||
}
|
||||
|
||||
@multipart(256 * 1024)
|
||||
@param('title', Types.String)
|
||||
async post(domainId: string, title: string) {
|
||||
if (!this.request.files.file) throw new BadRequestError();
|
||||
const quota = await this.getQuota();
|
||||
const lfdoc = await fs.promises.stat(this.request.files.file.path);
|
||||
let ufid: ObjectID;
|
||||
const udoc = await user.inc(this.user._id, 'usage', lfdoc.size);
|
||||
try {
|
||||
ufid = await file.add(this.request.files.file.path, title, this.user._id);
|
||||
} catch (e) {
|
||||
await user.inc(this.user._id, 'usage', -lfdoc.size);
|
||||
throw e;
|
||||
}
|
||||
const fdoc = await file.getMeta(ufid);
|
||||
this.response.template = 'fs_upload.html';
|
||||
this.response.body = {
|
||||
fdoc, ufid, usage: udoc.usage, quota,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class StatusHandler extends Handler {
|
||||
async get() {
|
||||
const stats = await coll.find().sort({ type: 1, updateAt: -1 }).toArray();
|
||||
for (const i in stats) {
|
||||
let desc = '';
|
||||
const online = new Date(stats[i].updateAt).getTime() > new Date().getTime() - 300000;
|
||||
if (!online) desc = 'Offline';
|
||||
desc = desc || 'Online';
|
||||
stats[i].isOnline = online;
|
||||
stats[i].status = desc;
|
||||
}
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['status', null],
|
||||
];
|
||||
this.response.body = { stats, path };
|
||||
this.response.template = 'status.html';
|
||||
}
|
||||
}
|
||||
|
||||
class StatusUpdateHandler extends Handler {
|
||||
async post(args) {
|
||||
this.checkPriv(PRIV.PRIV_JUDGE);
|
||||
args.type = 'judger';
|
||||
return coll.updateOne(
|
||||
{ mid: args.mid, type: 'judger' },
|
||||
{ $set: args },
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SwitchLanguageHandler extends Handler {
|
||||
@param('lang', Types.String)
|
||||
async get(domainId: string, lang: string) {
|
||||
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
|
||||
await user.setById(this.user._id, { viewLang: lang });
|
||||
} else this.session.viewLang = lang;
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('file_download', '/fs/:fileId/:secret', FileDownloadHandler);
|
||||
Route('file_download_with_name', '/fs/:fileId/:name/:secret', FileDownloadHandler);
|
||||
Route('file_upload', '/fs/upload', FileUploadHandler, PRIV.PRIV_CREATE_FILE);
|
||||
Route('status', '/status', StatusHandler);
|
||||
Route('status_update', '/status/update', StatusUpdateHandler);
|
||||
Route('switch_language', '/language/:lang', SwitchLanguageHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.misc = apply;
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable no-empty-function */
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import { NotFoundError } from '../error';
|
||||
import { Route, Handler } from '../service/server';
|
||||
|
||||
class NotFoundHandler extends Handler {
|
||||
async prepare() {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
all() { }
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('notfound', '*', NotFoundHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.notfound = apply;
|
||||
@@ -0,0 +1,635 @@
|
||||
import { isSafeInteger, flatten } from 'lodash';
|
||||
import yaml from 'js-yaml';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import {
|
||||
NoProblemError, ProblemDataNotFoundError, BadRequestError,
|
||||
SolutionNotFoundError, ProblemNotFoundError, ValidationError,
|
||||
PermissionError,
|
||||
} from '../error';
|
||||
import {
|
||||
Pdoc, User, Rdoc, PathComponent,
|
||||
} from '../interface';
|
||||
import paginate from '../lib/paginate';
|
||||
import {
|
||||
checkPid, isTitle, isContent, isPid,
|
||||
} from '../lib/validator';
|
||||
import * as file from '../model/file';
|
||||
import * as problem from '../model/problem';
|
||||
import * as record from '../model/record';
|
||||
import * as domain from '../model/domain';
|
||||
import * as user from '../model/user';
|
||||
import * as solution from '../model/solution';
|
||||
import { PERM, PRIV, CONSTANT } from '../model/builtin';
|
||||
import * as bus from '../service/bus';
|
||||
import {
|
||||
Route, Connection, Handler, ConnectionHandler, Types, param, multipart,
|
||||
} from '../service/server';
|
||||
|
||||
const parseCategory = (value: string) => flatten(value.split('+').map((e) => e.split(','))).map((e) => e.trim());
|
||||
const parsePid = (value: string) => (isSafeInteger(value) ? parseInt(value, 10) : value);
|
||||
|
||||
class ProblemHandler extends Handler {
|
||||
async __prepare() {
|
||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM);
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.response.template === 'problem_main.html' && this.request.json) {
|
||||
const {
|
||||
path, page, pcount, ppcount, pdocs, psdict, category,
|
||||
} = this.response.body;
|
||||
this.response.body = {
|
||||
title: this.renderTitle(category),
|
||||
fragments: (await Promise.all([
|
||||
this.renderHTML('partials/problem_list.html', {
|
||||
page, ppcount, pcount, pdocs, psdict,
|
||||
}),
|
||||
this.renderHTML('partials/problem_stat.html', { pcount }),
|
||||
this.renderHTML('partials/problem_lucky.html', { category }),
|
||||
this.renderHTML('partials/path.html', { path }),
|
||||
])).map((i) => ({ html: i })),
|
||||
raw: {
|
||||
path, page, pcount, ppcount, pdocs, psdict, category,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemMainHandler extends ProblemHandler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('q', Types.String, true)
|
||||
async get(domainId: string, page = 1, q = '') {
|
||||
this.response.template = 'problem_main.html';
|
||||
const query: any = {};
|
||||
let psdict = {};
|
||||
const path: PathComponent[] = [
|
||||
['Hydro', 'homepage'],
|
||||
['problem_main', null],
|
||||
];
|
||||
if (q) {
|
||||
q = q.toLowerCase();
|
||||
const $regex = new RegExp(`\\A\\Q${q}\\E`, 'gmi');
|
||||
query.title = { $regex };
|
||||
path.push([q, null, null, true]);
|
||||
}
|
||||
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) query.hidden = false;
|
||||
const [pdocs, ppcount, pcount] = await paginate(
|
||||
problem.getMulti(domainId, query).sort({ pid: 1 }),
|
||||
page,
|
||||
CONSTANT.PROBLEM_PER_PAGE,
|
||||
);
|
||||
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
|
||||
psdict = await problem.getListStatus(
|
||||
domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId),
|
||||
);
|
||||
}
|
||||
this.response.body = {
|
||||
path, page, pcount, ppcount, pdocs, psdict, category: q,
|
||||
};
|
||||
}
|
||||
|
||||
@param('pid', Types.UnsignedInt)
|
||||
async postStar(domainId: string, pid: number) {
|
||||
await problem.setStar(domainId, pid, this.user._id, true);
|
||||
this.back({ star: true });
|
||||
}
|
||||
|
||||
@param('pid', Types.UnsignedInt)
|
||||
async postUnstar(domainId: string, pid: number) {
|
||||
await problem.setStar(domainId, pid, this.user._id, false);
|
||||
this.back({ star: false });
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemCategoryHandler extends ProblemHandler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('category', Types.String, null, parseCategory)
|
||||
async get(domainId: string, page = 1, category: string[]) {
|
||||
this.response.template = 'problem_main.html';
|
||||
const q: any = { $and: [] };
|
||||
for (const name of category) {
|
||||
q.$and.push({
|
||||
$or: [
|
||||
{ category: { $elemMatch: { $eq: name } } },
|
||||
{ tag: { $elemMatch: { $eq: name } } },
|
||||
],
|
||||
});
|
||||
}
|
||||
let psdict = {};
|
||||
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
|
||||
const [pdocs, ppcount, pcount] = await paginate(
|
||||
problem.getMulti(domainId, q).sort({ pid: 1 }),
|
||||
page,
|
||||
CONSTANT.PROBLEM_PER_PAGE,
|
||||
);
|
||||
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
|
||||
psdict = await problem.getListStatus(
|
||||
domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId),
|
||||
);
|
||||
}
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['problem_main', 'problem_main'],
|
||||
[category, null, null, true],
|
||||
];
|
||||
this.response.body = {
|
||||
path, page, pcount, ppcount, pdocs, psdict, category: category.join('+'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemRandomHandler extends ProblemHandler {
|
||||
@param('category', Types.String, true, null, parseCategory)
|
||||
async get(domainId: string, category: string[] = []) {
|
||||
const q: any = category.length ? { $and: [] } : {};
|
||||
for (const name of category) {
|
||||
if (name) {
|
||||
q.$and.push({
|
||||
$or: [
|
||||
{ category: { $elemMatch: { $eq: name } } },
|
||||
{ tag: { $elemMatch: { $eq: name } } },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
|
||||
const pid = await problem.random(domainId, q);
|
||||
if (!pid) throw new NoProblemError();
|
||||
this.response.body = { pid };
|
||||
this.response.redirect = this.url('problem_detail', { pid });
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemDetailHandler extends ProblemHandler {
|
||||
pdoc: Pdoc;
|
||||
|
||||
udoc: User;
|
||||
|
||||
@param('pid', Types.String, null, parsePid)
|
||||
async _prepare(domainId: string, pid: number | string) {
|
||||
this.response.template = 'problem_detail.html';
|
||||
this.pdoc = await problem.get(domainId, pid, this.user._id);
|
||||
if (!this.pdoc) throw new ProblemNotFoundError(domainId, pid);
|
||||
if (this.pdoc.hidden && this.pdoc.owner !== this.user._id) {
|
||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||
}
|
||||
this.udoc = await user.getById(domainId, this.pdoc.owner);
|
||||
this.response.body = {
|
||||
pdoc: this.pdoc,
|
||||
udoc: this.udoc,
|
||||
title: this.pdoc.title,
|
||||
path: [
|
||||
['Hydro', 'homepage'],
|
||||
['problem_main', 'problem_main'],
|
||||
[this.pdoc.title, null, true],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
async get(...args: any[]) { }
|
||||
|
||||
@param('pid', Types.ObjectID)
|
||||
@param('dest', Types.String)
|
||||
@param('hidden', Types.Boolean)
|
||||
async postCopy(domainId: string, pid: number, destDomainId: string, hidden = false) {
|
||||
const udoc = await user.getById(destDomainId, this.user._id);
|
||||
if (!udoc.hasPerm(PERM.PERM_CREATE_PROBLEM)) {
|
||||
throw new PermissionError(PERM.PERM_CREATE_PROBLEM);
|
||||
}
|
||||
if (!this.pdoc.data) {
|
||||
// Copy Without Data
|
||||
pid = await problem.add(
|
||||
destDomainId, this.pdoc.pid, this.pdoc.title,
|
||||
this.pdoc.content, this.user._id, this.pdoc.tag,
|
||||
this.pdoc.category, null, hidden,
|
||||
);
|
||||
} else if (this.pdoc.data instanceof ObjectID) {
|
||||
// With data
|
||||
pid = await problem.add(
|
||||
destDomainId, this.pdoc.pid, this.pdoc.title,
|
||||
this.pdoc.content, this.user._id, this.pdoc.tag,
|
||||
this.pdoc.category, { domainId, pid }, hidden,
|
||||
);
|
||||
} else {
|
||||
// TODO better message
|
||||
// Data should only be copied once.
|
||||
throw new BadRequestError('Cannot copy this problem.');
|
||||
}
|
||||
this.response.redirect = this.url('problem_settings', { domainId: destDomainId, pid });
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemSubmitHandler extends ProblemDetailHandler {
|
||||
@param('pid', Types.String, null, parsePid)
|
||||
async get(domainId: string, pid: string | number) {
|
||||
this.response.template = 'problem_submit.html';
|
||||
const rdocs = await record
|
||||
.getUserInProblemMulti(domainId, this.user._id, this.pdoc.docId)
|
||||
.sort({ _id: -1 })
|
||||
.limit(10)
|
||||
.toArray();
|
||||
this.response.body = {
|
||||
path: [
|
||||
['Hydro', 'homepage'],
|
||||
['problem_main', 'problem_main'],
|
||||
[this.pdoc.title, 'problem_detail', { pid }, true],
|
||||
['problem_submit', null],
|
||||
],
|
||||
pdoc: this.pdoc,
|
||||
udoc: this.udoc,
|
||||
rdocs,
|
||||
title: this.pdoc.title,
|
||||
};
|
||||
}
|
||||
|
||||
@param('lang', Types.String)
|
||||
@param('code', Types.String)
|
||||
async post(domainId: string, lang: string, code: string) {
|
||||
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true);
|
||||
const [rdoc] = await Promise.all([
|
||||
record.get(domainId, rid),
|
||||
domain.incUserInDomain(domainId, this.user._id, 'nSubmit'),
|
||||
]);
|
||||
bus.publish('record_change', { rdoc });
|
||||
this.response.body = { rid };
|
||||
this.response.redirect = this.url('record_detail', { rid });
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemPretestHandler extends ProblemDetailHandler {
|
||||
@param('lang', Types.String)
|
||||
@param('code', Types.String)
|
||||
@param('input', Types.String, true)
|
||||
async post(domainId: string, lang: string, code: string, input: string = '') {
|
||||
this.limitRate('add_record', 3600, 100);
|
||||
// TODO parseConfig
|
||||
const rid = await record.add(
|
||||
domainId, this.pdoc.docId, this.user._id,
|
||||
lang, code, true,
|
||||
{
|
||||
input,
|
||||
time: 1000,
|
||||
memory: 256,
|
||||
},
|
||||
);
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
bus.publish('record_change', { rdoc });
|
||||
this.response.body = { rid };
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemPretestConnectionHandler extends ConnectionHandler {
|
||||
pid: string;
|
||||
|
||||
domainId: string;
|
||||
|
||||
id: string;
|
||||
|
||||
@param('pid', Types.String)
|
||||
async prepare(domainId: string, pid: string) {
|
||||
const pdoc = await problem.get(domainId, pid);
|
||||
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||
if (!pdoc) throw new ProblemNotFoundError(domainId, pid);
|
||||
this.pid = pdoc.docId.toString();
|
||||
this.domainId = domainId;
|
||||
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
|
||||
}
|
||||
|
||||
async onRecordChange(data) {
|
||||
const rdoc: Rdoc = data.value.rdoc;
|
||||
if (
|
||||
rdoc.uid !== this.user._id
|
||||
|| rdoc.pid.toString() !== this.pid
|
||||
|| rdoc.domainId !== this.domainId
|
||||
) return;
|
||||
rdoc.compilerTexts = [];
|
||||
rdoc.judgeTexts = [];
|
||||
// @ts-ignore
|
||||
rdoc.testCases = rdoc.testCases.map((c) => ({
|
||||
status: c.status,
|
||||
}));
|
||||
// TODO handle update
|
||||
if (rdoc.contest) return;
|
||||
this.send({ rdoc });
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
bus.unsubscribe(['record_change'], this.id);
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemStatisticsHandler extends ProblemDetailHandler {
|
||||
async get(domainId: string) {
|
||||
const udoc = await user.getById(domainId, this.pdoc.owner);
|
||||
const path = [
|
||||
['problem_main', 'problem_main'],
|
||||
[this.pdoc.title, 'problem_detail', { pid: this.pdoc.pid }, true],
|
||||
['problem_statistics', null],
|
||||
];
|
||||
this.response.template = 'problem_statistics.html';
|
||||
this.response.body = { pdoc: this.pdoc, udoc, path };
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemManageHandler extends ProblemDetailHandler {
|
||||
async prepare() {
|
||||
if (this.pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SELF);
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemSettingsHandler extends ProblemManageHandler {
|
||||
@param('pid', Types.String)
|
||||
async get(domainId: string, pid: string) {
|
||||
this.response.template = 'problem_settings.html';
|
||||
this.response.body.path = [
|
||||
['Hydro', 'homepage'],
|
||||
['problem_main', 'problem_main'],
|
||||
[this.pdoc.title, 'problem_detail', { pid }, true],
|
||||
['problem_settings', null],
|
||||
];
|
||||
if (this.response.body.config) {
|
||||
this.response.body.config = yaml.safeDump(this.response.body.config);
|
||||
}
|
||||
}
|
||||
|
||||
@param('pid', Types.String, null, parsePid)
|
||||
@param('yaml', Types.String)
|
||||
async postConfig(domainId: string, pid: string | number, cfg: string) {
|
||||
const pdoc = await problem.get(domainId, pid);
|
||||
// TODO validate
|
||||
const config = yaml.safeLoad(cfg);
|
||||
await problem.edit(domainId, pdoc.docId, { config });
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('pid', Types.String, null, parsePid)
|
||||
@param('hidden', Types.Boolean)
|
||||
@param('category', Types.String, true, null, parseCategory)
|
||||
@param('tag', Types.String, true, null, parseCategory)
|
||||
async postSetting(
|
||||
domainId: string, pid: string | number, hidden = false,
|
||||
category: string[] = [], tag: string[] = [],
|
||||
) {
|
||||
const pdoc = await problem.get(domainId, pid);
|
||||
await problem.edit(domainId, pdoc.docId, { hidden, category, tag });
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemEditHandler extends ProblemManageHandler {
|
||||
async get({ pid }) {
|
||||
this.response.template = 'problem_edit.html';
|
||||
this.response.body.path = [
|
||||
['Hydro', 'homepage'],
|
||||
['problem_main', 'problem_main'],
|
||||
[this.pdoc.title, 'problem_detail', { pid }, true],
|
||||
['problem_edit', null],
|
||||
];
|
||||
}
|
||||
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('content', Types.String, isContent)
|
||||
async post(domainId: string, title: string, content: string) {
|
||||
const pid = checkPid(this.request.body.pid);
|
||||
const pdoc = await problem.get(domainId, this.request.params.pid);
|
||||
await problem.edit(domainId, pdoc.docId, { title, content, pid });
|
||||
this.response.redirect = this.url('problem_detail', { pid: this.request.params.pid });
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemDataUploadHandler extends ProblemManageHandler {
|
||||
async get() {
|
||||
if (this.pdoc.data instanceof ObjectID) {
|
||||
const f = await file.getMeta(this.pdoc.data);
|
||||
this.response.body.md5 = f.md5;
|
||||
} else if (this.pdoc.data) {
|
||||
this.response.body.from = this.pdoc.data;
|
||||
}
|
||||
this.response.template = 'problem_upload.html';
|
||||
}
|
||||
|
||||
@multipart(256 * 1024)
|
||||
async post({ domainId }) {
|
||||
if (!this.request.files.file) throw new ValidationError('file');
|
||||
this.pdoc = await problem.setTestdata(
|
||||
domainId, this.pdoc.docId, this.request.files.file.path,
|
||||
);
|
||||
if (this.pdoc.data instanceof ObjectID) {
|
||||
const f = await file.getMeta(this.pdoc.data);
|
||||
this.response.body.md5 = f.md5;
|
||||
}
|
||||
this.response.template = 'problem_upload.html';
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemDataDownloadHandler extends ProblemDetailHandler {
|
||||
async get({ pid }) {
|
||||
if (!this.user.hasPriv(PRIV.PRIV_JUDGE)) {
|
||||
if (this.user._id !== this.pdoc.owner) {
|
||||
this.checkPerm(PERM.PERM_READ_PROBLEM_DATA);
|
||||
} else this.checkPerm(PERM.PERM_READ_PROBLEM_DATA_SELF);
|
||||
}
|
||||
if (this.pdoc.data instanceof ObjectID) {
|
||||
this.response.redirect = await file.url(this.pdoc.data, `${this.pdoc.title}.zip`);
|
||||
} else if (this.pdoc.data) {
|
||||
if (!this.pdoc.data.host) {
|
||||
this.response.redirect = this.url('problem_data', {
|
||||
domainId: this.pdoc.data.domainId,
|
||||
pid: this.pdoc.data.pid,
|
||||
});
|
||||
} else {
|
||||
const [scheme, raw] = this.pdoc.data.host.split('//');
|
||||
const args = JSON.parse(Buffer.from(raw, 'base64').toString());
|
||||
if (scheme === 'hydro') {
|
||||
const [secure, host, port, domainId, id] = args;
|
||||
this.response.redirect = `http${secure ? 's' : ''}://${host}:${port}/d/${domainId}/p/${id}/data`;
|
||||
} else if (scheme === 'syzoj') {
|
||||
const [secure, host, port, id] = args;
|
||||
this.response.redirect = `http${secure ? 's' : ''}://${host}:${port}/problem/${id}/testdata/download`;
|
||||
} else {
|
||||
throw new ProblemDataNotFoundError(pid);
|
||||
}
|
||||
}
|
||||
} else throw new ProblemDataNotFoundError(pid);
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemSolutionHandler extends ProblemDetailHandler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
async get(domainId: string, page = 1) {
|
||||
this.response.template = 'problem_solution.html';
|
||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
|
||||
const [psdocs, pcount, pscount] = await paginate(
|
||||
solution.getMulti(domainId, this.pdoc.docId),
|
||||
page,
|
||||
CONSTANT.SOLUTION_PER_PAGE,
|
||||
);
|
||||
const uids = [this.pdoc.owner];
|
||||
const docids = [];
|
||||
for (const psdoc of psdocs) {
|
||||
docids.push(psdoc.docId);
|
||||
uids.push(psdoc.owner);
|
||||
if (psdoc.reply.length) {
|
||||
for (const psrdoc of psdoc.reply) uids.push(psrdoc.owner);
|
||||
}
|
||||
}
|
||||
const udict = await user.getList(domainId, uids);
|
||||
const pssdict = solution.getListStatus(domainId, docids, this.user._id);
|
||||
const path = [
|
||||
['problem_main', 'problem_main'],
|
||||
[this.pdoc.title, 'problem_detail', { pid: this.pdoc.pid }, true],
|
||||
['problem_solution', null],
|
||||
];
|
||||
this.response.body = {
|
||||
path, psdocs, page, pcount, pscount, udict, pssdict, pdoc: this.pdoc,
|
||||
};
|
||||
}
|
||||
|
||||
@param('content', Types.String, isContent)
|
||||
async postSubmit(domainId: string, content: string) {
|
||||
this.checkPerm(PERM.PERM_CREATE_PROBLEM_SOLUTION);
|
||||
await solution.add(domainId, this.pdoc.docId, this.user._id, content);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('content', Types.String, isContent)
|
||||
@param('psid', Types.ObjectID)
|
||||
async postEditSolution(domainId: string, content: string, psid: ObjectID) {
|
||||
let psdoc = await solution.get(domainId, psid);
|
||||
if (psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION);
|
||||
else this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF);
|
||||
psdoc = await solution.edit(domainId, psdoc.docId, content);
|
||||
this.back({ psdoc });
|
||||
}
|
||||
|
||||
@param('psid', Types.ObjectID)
|
||||
async postDeleteSolution(domainId: string, psid: ObjectID) {
|
||||
const psdoc = await solution.get(domainId, psid);
|
||||
if (psdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION);
|
||||
else this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF);
|
||||
await solution.del(domainId, psdoc.docId);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('psid', Types.ObjectID)
|
||||
@param('content', Types.String, isContent)
|
||||
async postReply(domainId: string, psid: ObjectID, content: string) {
|
||||
this.checkPerm(PERM.PERM_REPLY_PROBLEM_SOLUTION);
|
||||
const psdoc = await solution.get(domainId, psid);
|
||||
await solution.reply(domainId, psdoc.docId, this.user._id, content);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('psid', Types.ObjectID)
|
||||
@param('psrid', Types.ObjectID)
|
||||
@param('content', Types.String, isContent)
|
||||
async postEditReply(domainId: string, psid: ObjectID, psrid: ObjectID, content: string) {
|
||||
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
|
||||
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
|
||||
if (!(psrdoc.owner === this.user._id
|
||||
&& this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF))) {
|
||||
this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY);
|
||||
}
|
||||
await solution.editReply(domainId, psid, psrid, content);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('psid', Types.ObjectID)
|
||||
@param('psrid', Types.ObjectID)
|
||||
async postDeleteReply(domainId: string, psid: ObjectID, psrid: ObjectID) {
|
||||
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
|
||||
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
|
||||
if (!(psrdoc.owner === this.user._id
|
||||
&& this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF))) {
|
||||
this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY);
|
||||
}
|
||||
await solution.delReply(domainId, psid, psrid);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('psid', Types.ObjectID)
|
||||
async postUpvote(domainId: string, psid: ObjectID) {
|
||||
const [psdoc, pssdoc] = await solution.vote(domainId, psid, this.user._id, 1);
|
||||
this.back({ vote: psdoc.vote, user_vote: pssdoc.vote });
|
||||
}
|
||||
|
||||
@param('psid', Types.ObjectID)
|
||||
async postDownvote(domainId: string, psid: ObjectID) {
|
||||
const [psdoc, pssdoc] = await solution.vote(domainId, psid, this.user._id, -1);
|
||||
this.back({ vote: psdoc.vote, user_vote: pssdoc.vote });
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemSolutionRawHandler extends ProblemDetailHandler {
|
||||
@param('psid', Types.ObjectID)
|
||||
async get(domainId: string, psid: ObjectID) {
|
||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
|
||||
const psdoc = await solution.get(domainId, psid);
|
||||
this.response.type = 'text/markdown';
|
||||
this.response.body = psdoc.content;
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemSolutionReplyRawHandler extends ProblemDetailHandler {
|
||||
@param('psid', Types.ObjectID)
|
||||
@param('psrid', Types.ObjectID)
|
||||
async get(domainId: string, psid: ObjectID, psrid: ObjectID) {
|
||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
|
||||
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
|
||||
if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid, psrid);
|
||||
this.response.type = 'text/markdown';
|
||||
this.response.body = psrdoc.content;
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemCreateHandler extends Handler {
|
||||
async get() {
|
||||
this.response.template = 'problem_edit.html';
|
||||
this.checkPerm(PERM.PERM_CREATE_PROBLEM);
|
||||
this.response.body = {
|
||||
path: [
|
||||
['Hydro', 'homepage'],
|
||||
['problem_main', 'problem_main'],
|
||||
['problem_create', null],
|
||||
],
|
||||
page_name: 'problem_create',
|
||||
};
|
||||
}
|
||||
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('pid', Types.String, isPid, true)
|
||||
@param('content', Types.String, isContent)
|
||||
@param('hidden', Types.Boolean)
|
||||
async post(domainId: string, title: string, pid: string, content: string, hidden = false) {
|
||||
const docId = await problem.add(
|
||||
domainId, pid, title, content,
|
||||
this.user._id, [], [], null, hidden,
|
||||
);
|
||||
this.response.body = { pid: docId };
|
||||
this.response.redirect = this.url('problem_settings', { pid: docId });
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('problem_main', '/p', ProblemMainHandler);
|
||||
Route('problem_category', '/p/category/:category', ProblemCategoryHandler);
|
||||
Route('problem_random', '/problem/random', ProblemRandomHandler);
|
||||
Route('problem_detail', '/p/:pid', ProblemDetailHandler);
|
||||
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
|
||||
Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler);
|
||||
Route('problem_settings', '/p/:pid/settings', ProblemSettingsHandler);
|
||||
Route('problem_statistics', '/p/:pid/statistics', ProblemStatisticsHandler);
|
||||
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
|
||||
Route('problem_upload', '/p/:pid/upload', ProblemDataUploadHandler);
|
||||
Route('problem_data', '/p/:pid/data', ProblemDataDownloadHandler);
|
||||
Route('problem_solution', '/p/:pid/solution', ProblemSolutionHandler);
|
||||
Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
|
||||
Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
|
||||
Route('problem_create', '/problem/create', ProblemCreateHandler);
|
||||
Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.problem = apply;
|
||||
@@ -0,0 +1,190 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { PermissionError, RecordNotFoundError } from '../error';
|
||||
import { PERM, CONSTANT } from '../model/builtin';
|
||||
import * as problem from '../model/problem';
|
||||
import * as record from '../model/record';
|
||||
import * as contest from '../model/contest';
|
||||
import * as user from '../model/user';
|
||||
import paginate from '../lib/paginate';
|
||||
import * as bus from '../service/bus';
|
||||
import {
|
||||
Route, Handler, Connection, ConnectionHandler, Types, param,
|
||||
} from '../service/server';
|
||||
import { Rdoc } from '../interface';
|
||||
|
||||
const RecordHandler = contest.ContestHandlerMixin(Handler);
|
||||
|
||||
class RecordListHandler extends RecordHandler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('pid', Types.String, true)
|
||||
@param('tid', Types.ObjectID, true)
|
||||
@param('uidOrName', Types.String, true)
|
||||
async get(domainId: string, page = 1, pid?: string, tid?: ObjectID, uidOrName?: string) {
|
||||
this.response.template = 'record_main.html';
|
||||
if (tid) tid = new ObjectID(tid);
|
||||
const q: any = { tid, hidden: false };
|
||||
if (uidOrName) {
|
||||
q.$or = [
|
||||
{ unameLower: uidOrName.toLowerCase() },
|
||||
{ _id: parseInt(uidOrName, 10) },
|
||||
];
|
||||
}
|
||||
if (pid) q.pid = pid;
|
||||
const [rdocs] = await paginate(
|
||||
record.getMulti(domainId, q).sort('_id', -1),
|
||||
page,
|
||||
CONSTANT.RECORD_PER_PAGE,
|
||||
);
|
||||
const canViewProblemHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||
const [udict, pdict] = await Promise.all([
|
||||
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
|
||||
problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden, false),
|
||||
]);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['record_main', null],
|
||||
];
|
||||
this.response.body = {
|
||||
path,
|
||||
page,
|
||||
rdocs,
|
||||
pdict,
|
||||
udict,
|
||||
filterPid: pid,
|
||||
filterTid: tid,
|
||||
filterUidOrName: uidOrName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RecordDetailHandler extends RecordHandler {
|
||||
@param('rid', Types.ObjectID)
|
||||
async get(domainId: string, rid: ObjectID) {
|
||||
this.response.template = 'record_detail.html';
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
if (!rdoc) throw new RecordNotFoundError(rid);
|
||||
if (rdoc.contest) {
|
||||
const tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type);
|
||||
if (!this.canShowRecord(tdoc, true)) throw new PermissionError(rid);
|
||||
}
|
||||
if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) {
|
||||
rdoc.code = null;
|
||||
}
|
||||
const [pdoc, udoc] = await Promise.all([
|
||||
problem.get(domainId, rdoc.pid),
|
||||
user.getById(domainId, rdoc.uid),
|
||||
]);
|
||||
this.response.body = {
|
||||
path: [
|
||||
['Hydro', 'homepage'],
|
||||
['record_detail', null],
|
||||
],
|
||||
udoc,
|
||||
rdoc,
|
||||
pdoc,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RecordRejudgeHandler extends Handler {
|
||||
@param('rid', Types.ObjectID)
|
||||
async post(domainId: string, rid: ObjectID) {
|
||||
this.checkPerm(PERM.PERM_REJUDGE);
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
if (rdoc) {
|
||||
await record.reset(domainId, rid, true);
|
||||
await record.judge(domainId, rid);
|
||||
}
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
const RecordConnectionHandler = contest.ContestHandlerMixin(ConnectionHandler);
|
||||
|
||||
class RecordMainConnectionHandler extends RecordConnectionHandler {
|
||||
@param('tid', Types.ObjectID, true)
|
||||
async prepare(domainId: string, tid: ObjectID) {
|
||||
this.domainId = domainId;
|
||||
if (tid) {
|
||||
const tdoc = await contest.get(domainId, new ObjectID(tid), -1);
|
||||
if (this.canShowRecord(tdoc)) this.tid = tid;
|
||||
else {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
|
||||
}
|
||||
|
||||
async message(msg) {
|
||||
if (msg.rids instanceof Array) {
|
||||
const rids = msg.rids.map((id: string) => new ObjectID(id));
|
||||
const rdocs = await record.getMulti(
|
||||
this.domainId, { _id: { $in: rids } },
|
||||
).toArray();
|
||||
for (const rdoc of rdocs) {
|
||||
this.onRecordChange({ value: { rdoc } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
bus.unsubscribe(['record_change'], this.id);
|
||||
}
|
||||
|
||||
async onRecordChange(data) {
|
||||
const rdoc: Rdoc = data.value.rdoc;
|
||||
if (rdoc.contest && rdoc.contest.tid.toString() !== this.tid) return;
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [udoc, pdoc] = await Promise.all([
|
||||
user.getById(this.domainId, rdoc.uid),
|
||||
problem.get(this.domainId, rdoc.pid, null),
|
||||
]);
|
||||
if (pdoc && pdoc.hidden && !this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null;
|
||||
else this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) });
|
||||
}
|
||||
}
|
||||
|
||||
class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(ConnectionHandler) {
|
||||
@param('rid', Types.ObjectID)
|
||||
async prepare(domainId: string, rid: ObjectID) {
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
if (rdoc.contest) {
|
||||
const tdoc = await contest.get(domainId, rdoc.contest.tid, -1);
|
||||
if (!this.canShowRecord(tdoc)) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.rid = rid.toString();
|
||||
this.id = bus.subscribe(['record_change'], this.onRecordChange.bind(this));
|
||||
this.onRecordChange({ value: { rdoc } });
|
||||
}
|
||||
|
||||
async onRecordChange(data) {
|
||||
const rdoc: Rdoc = data.value.rdoc;
|
||||
const { $set, $push } = data.value;
|
||||
if (rdoc._id.toString() !== this.rid) return;
|
||||
if ($set) this.send({ $set, $push });
|
||||
else {
|
||||
this.send({
|
||||
status_html: await this.renderHTML('record_detail_status.html', { rdoc }),
|
||||
summary_html: await this.renderHTML('record_detail_summary.html', { rdoc }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
bus.unsubscribe(['record_change'], this.id);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('record_main', '/record', RecordListHandler);
|
||||
Route('record_detail', '/record/:rid', RecordDetailHandler);
|
||||
Route('record_rejudge', '/record/:rid/rejudge', RecordRejudgeHandler);
|
||||
Connection('record_conn', '/record-conn', RecordMainConnectionHandler);
|
||||
Connection('record_detail_conn', '/record-detail-conn', RecordDetailConnectionHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.record = apply;
|
||||
@@ -0,0 +1,272 @@
|
||||
import assert from 'assert';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { ValidationError, ProblemNotFoundError } from '../error';
|
||||
import { isTitle, isContent, isDescription } from '../lib/validator';
|
||||
import paginate from '../lib/paginate';
|
||||
import { PERM, PRIV, CONSTANT } from '../model/builtin';
|
||||
import * as problem from '../model/problem';
|
||||
import * as builtin from '../model/builtin';
|
||||
import * as training from '../model/training';
|
||||
import * as user from '../model/user';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import { Tdoc } from '../interface';
|
||||
|
||||
async function _parseDagJson(domainId, dag) {
|
||||
const parsed = [];
|
||||
try {
|
||||
dag = JSON.parse(dag);
|
||||
assert(dag instanceof Array, 'dag must be an array');
|
||||
const ids = new Set(dag.map((s) => s._id));
|
||||
assert(dag.length === ids.size, '_id must be unique');
|
||||
for (const node of dag) {
|
||||
assert(node._id, 'each node should have a _id');
|
||||
assert(node.title, 'each node shoule have a title');
|
||||
assert(node.requireNids instanceof Array);
|
||||
assert(node.pids instanceof Array);
|
||||
assert(node.pids.length);
|
||||
for (const nid of node.requireNids) {
|
||||
assert(ids.has(nid), `required nid ${nid} not found`);
|
||||
}
|
||||
const tasks = [];
|
||||
for (const i in node.pids) {
|
||||
tasks.push(problem.get(domainId, node.pids[i]).then((pdoc) => {
|
||||
if (!pdoc) throw new ProblemNotFoundError(domainId, node.pids[i]);
|
||||
node.pids[i] = pdoc.docId;
|
||||
}));
|
||||
}
|
||||
// FIXME no-await-in-loop
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(tasks);
|
||||
const newNode = {
|
||||
_id: parseInt(node._id, 10),
|
||||
title: node.title,
|
||||
requireNids: Array.from(new Set(node.requireNids)),
|
||||
pids: Array.from(new Set(node.pids)),
|
||||
};
|
||||
parsed.push(newNode);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new ValidationError('dag', [e.message]);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
class TrainingMainHandler extends Handler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
async get(domainId: string, page = 1) {
|
||||
const [tdocs, tpcount] = await paginate(
|
||||
training.getMulti(domainId),
|
||||
page,
|
||||
CONSTANT.TRAINING_PER_PAGE,
|
||||
);
|
||||
const tids: Set<ObjectID> = new Set();
|
||||
for (const tdoc of tdocs) tids.add(tdoc.docId);
|
||||
const tsdict = {};
|
||||
let tdict = {};
|
||||
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
|
||||
const enrolledTids: Set<ObjectID> = new Set();
|
||||
const tsdocs = await training.getMultiStatus(domainId, {
|
||||
uid: this.user._id,
|
||||
$or: [{ docId: { $in: Array.from(tids) } }, { enroll: 1 }],
|
||||
}).toArray();
|
||||
for (const tsdoc of tsdocs) {
|
||||
tsdict[tsdoc.docId] = tsdoc;
|
||||
enrolledTids.add(tsdoc.docId);
|
||||
}
|
||||
for (const tid of tids) enrolledTids.delete(tid);
|
||||
if (enrolledTids.size) {
|
||||
tdict = await training.getList(domainId, Array.from(enrolledTids));
|
||||
}
|
||||
}
|
||||
for (const tdoc of tdocs) tdict[tdoc.docId] = tdoc;
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['training_main', null],
|
||||
];
|
||||
this.response.template = 'training_main.html';
|
||||
this.response.body = {
|
||||
tdocs, page, tpcount, tsdict, tdict, path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TrainingDetailHandler extends Handler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
const tdoc = await training.get(domainId, tid);
|
||||
const pids = training.getPids(tdoc);
|
||||
const [owner, pdict] = await Promise.all([
|
||||
user.getById(domainId, tdoc.owner),
|
||||
problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)),
|
||||
]);
|
||||
const psdict = await problem.getListStatus(domainId, this.user._id, pids);
|
||||
const donePids = new Set();
|
||||
const progPids = new Set();
|
||||
for (const pid in psdict) {
|
||||
const psdoc = psdict[pid];
|
||||
if (psdoc.status) {
|
||||
if (psdoc.status === builtin.STATUS.STATUS_ACCEPTED) {
|
||||
donePids.add(parseInt(pid, 10));
|
||||
} else progPids.add(parseInt(pid, 10));
|
||||
}
|
||||
}
|
||||
const nsdict = {};
|
||||
const ndict = {};
|
||||
const doneNids = new Set();
|
||||
for (const node of tdoc.dag) {
|
||||
ndict[node._id] = node;
|
||||
const totalCount = node.pids.length;
|
||||
const doneCount = Set.union(new Set(node.pids), donePids).size;
|
||||
const nsdoc = {
|
||||
progress: totalCount ? Math.floor(100 * (doneCount / totalCount)) : 100,
|
||||
isDone: training.isDone(node, doneNids, donePids),
|
||||
isProgress: training.isProgress(node, doneNids, donePids, progPids),
|
||||
isOpen: training.isOpen(node, doneNids, donePids, progPids),
|
||||
isInvalid: training.isInvalid(node, doneNids),
|
||||
};
|
||||
if (nsdoc.isDone) doneNids.add(node._id);
|
||||
nsdict[node._id] = nsdoc;
|
||||
}
|
||||
const tsdoc = await training.setStatus(domainId, tdoc.docId, this.user._id, {
|
||||
doneNids: Array.from(doneNids),
|
||||
donePids: Array.from(donePids),
|
||||
done: doneNids.size === tdoc.dag.length,
|
||||
});
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['training_main', 'training_main'],
|
||||
[tdoc.title, null, null, true],
|
||||
];
|
||||
this.response.template = 'training_detail.html';
|
||||
this.response.body = {
|
||||
path, tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, owner,
|
||||
};
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async postEnroll(domainId: string, tid: ObjectID) {
|
||||
this.checkPriv(PRIV.PRIV_USER_PROFILE);
|
||||
const tdoc = await training.get(domainId, tid);
|
||||
await training.enroll(domainId, tdoc.docId, this.user._id);
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class TrainingCreateHandler extends Handler {
|
||||
async get() {
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['training_main', 'training_main'],
|
||||
['training_create', null],
|
||||
];
|
||||
this.response.template = 'training_edit.html';
|
||||
this.response.body = { page_name: 'training_create', path };
|
||||
}
|
||||
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('content', Types.String, isContent)
|
||||
@param('dag', Types.String, isContent)
|
||||
@param('description', Types.String, isDescription)
|
||||
async post(
|
||||
domainId: string, title: string, content: string,
|
||||
_dag: string, description: string,
|
||||
) {
|
||||
const dag = await _parseDagJson(domainId, _dag);
|
||||
const pids = training.getPids({ dag });
|
||||
assert(pids.length, new ValidationError('dag'));
|
||||
const pdocs = await problem.getMulti(domainId, {
|
||||
$or: [{ docId: { $in: pids } }, { pid: { $in: pids } }],
|
||||
}).sort('_id', 1).toArray();
|
||||
const existPids = pdocs.map((pdoc) => pdoc.docId);
|
||||
const existPnames = pdocs.map((pdoc) => pdoc.pid);
|
||||
if (pids.length !== existPids.length) {
|
||||
for (const pid of pids) {
|
||||
assert(
|
||||
existPids.includes(pid) || existPnames.includes(pid),
|
||||
new ProblemNotFoundError(pid),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const pdoc of pdocs) {
|
||||
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||
}
|
||||
const tid = await training.add(domainId, title, content, this.user._id, dag, description);
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('training_detail', { tid });
|
||||
}
|
||||
}
|
||||
|
||||
class TrainingEditHandler extends Handler {
|
||||
tdoc: Tdoc;
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async prepare(domainId: string, tid: ObjectID) {
|
||||
this.tdoc = await training.get(domainId, tid);
|
||||
if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_TRAINING);
|
||||
else this.checkPerm(PERM.PERM_EDIT_TRAINING_SELF);
|
||||
}
|
||||
|
||||
async get({ tid }) {
|
||||
const dag = JSON.stringify(this.tdoc.dag, null, 2);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['training_main', 'training_main'],
|
||||
[this.tdoc.title, 'training_detail', { tid }, true],
|
||||
['training_edit', null],
|
||||
];
|
||||
this.response.template = 'training_edit.html';
|
||||
this.response.body = {
|
||||
tdoc: this.tdoc, dag, path, page_name: 'training_edit',
|
||||
};
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('title', Types.String, isTitle)
|
||||
@param('content', Types.String, isContent)
|
||||
@param('dag', Types.String, isContent)
|
||||
@param('description', Types.String, isDescription)
|
||||
async post(
|
||||
domainId: string, tid: ObjectID,
|
||||
title: string, content: string,
|
||||
_dag: string, description: string,
|
||||
) {
|
||||
const dag = await _parseDagJson(domainId, _dag);
|
||||
const pids = training.getPids({ dag });
|
||||
assert(pids.length, new ValidationError('dag'));
|
||||
const pdocs = await problem.getMulti(domainId, {
|
||||
$or: [
|
||||
{ docId: { $in: pids } },
|
||||
{ pid: { $in: pids } },
|
||||
],
|
||||
}).sort('_id', 1).toArray();
|
||||
const existPids = pdocs.map((pdoc) => pdoc.docId);
|
||||
const existPnames = pdocs.map((pdoc) => pdoc.pid);
|
||||
if (pids.length !== existPids.length) {
|
||||
for (const pid in pids) {
|
||||
assert(
|
||||
existPids.includes(pid) || existPnames.includes(pid),
|
||||
new ProblemNotFoundError(pid),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const pdoc of pdocs) {
|
||||
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||
}
|
||||
await training.edit(domainId, tid, {
|
||||
title, content, dag, description,
|
||||
});
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('training_detail', { tid });
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('training_main', '/training', TrainingMainHandler, PERM.PERM_VIEW_TRAINING);
|
||||
Route('training_create', '/training/create', TrainingCreateHandler, PERM.PERM_CREATE_TRAINING);
|
||||
Route('training_detail', '/training/:tid', TrainingDetailHandler, PERM.PERM_VIEW_TRAINING);
|
||||
Route('training_edit', '/training/:tid/edit', TrainingEditHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.training = apply;
|
||||
@@ -0,0 +1,274 @@
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
UserAlreadyExistError, InvalidTokenError, VerifyPasswordError,
|
||||
UserNotFoundError, SystemError, BlacklistedError,
|
||||
UserFacingError,
|
||||
} from '../error';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import * as user from '../model/user';
|
||||
import * as token from '../model/token';
|
||||
import * as record from '../model/record';
|
||||
import * as problem from '../model/problem';
|
||||
import * as task from '../model/task';
|
||||
import * as system from '../model/system';
|
||||
import { PERM, PRIV } from '../model/builtin';
|
||||
import { isEmail, isPassword, isUname } from '../lib/validator';
|
||||
import { sendMail } from '../lib/mail';
|
||||
import * as misc from '../lib/misc';
|
||||
import paginate from '../lib/paginate';
|
||||
|
||||
class UserLoginHandler extends Handler {
|
||||
async get() {
|
||||
this.response.template = 'user_login.html';
|
||||
}
|
||||
|
||||
@param('uname', Types.String)
|
||||
@param('password', Types.String)
|
||||
@param('rememberme', Types.Boolean)
|
||||
async post(domainId: string, uname: string, password: string, rememberme = false) {
|
||||
const udoc = await user.getByUname(domainId, uname);
|
||||
if (!udoc) throw new UserNotFoundError(uname);
|
||||
udoc.checkPassword(password);
|
||||
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
|
||||
if (udoc.priv === PRIV.PRIV_NONE) throw new BlacklistedError(uname);
|
||||
this.session.uid = udoc._id;
|
||||
this.session.save = rememberme;
|
||||
this.response.redirect = this.request.referer.endsWith('/login') ? '/' : this.request.referer;
|
||||
}
|
||||
}
|
||||
|
||||
class UserLogoutHandler extends Handler {
|
||||
async get() {
|
||||
this.response.template = 'user_logout.html';
|
||||
}
|
||||
|
||||
async post() {
|
||||
this.session.uid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class UserRegisterHandler extends Handler {
|
||||
async get() {
|
||||
this.response.template = 'user_register.html';
|
||||
}
|
||||
|
||||
@param('mail', Types.String, isEmail)
|
||||
async post(domainId: string, mail: string) {
|
||||
if (await user.getByEmail('system', mail)) throw new UserAlreadyExistError(mail);
|
||||
this.limitRate('send_mail', 3600, 30);
|
||||
const t = await token.add(
|
||||
token.TYPE_REGISTRATION,
|
||||
await system.get('registration_token_expire_seconds'),
|
||||
{ mail },
|
||||
);
|
||||
if (await system.get('smtp.user')) {
|
||||
const m = await this.renderHTML('user_register_mail.html', {
|
||||
path: `register/${t[0]}`,
|
||||
url_prefix: await system.get('server.url'),
|
||||
});
|
||||
await sendMail(mail, 'Sign Up', 'user_register_mail', m);
|
||||
this.response.template = 'user_register_mail_sent.html';
|
||||
} else {
|
||||
this.response.redirect = this.url('user_register_with_code', { code: t[0] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UserRegisterWithCodeHandler extends Handler {
|
||||
@param('code', Types.String)
|
||||
async get(domainId: string, code: string) {
|
||||
this.response.template = 'user_register_with_code.html';
|
||||
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
|
||||
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
|
||||
this.response.body = { mail };
|
||||
}
|
||||
|
||||
@param('password', Types.String, isPassword)
|
||||
@param('verifyPassword', Types.String)
|
||||
@param('uname', Types.String, isUname)
|
||||
@param('code', Types.String)
|
||||
async post(
|
||||
domainId: string, password: string, verify: string,
|
||||
uname: string, code: string,
|
||||
) {
|
||||
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
|
||||
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
|
||||
if (password !== verify) throw new VerifyPasswordError();
|
||||
const uid = await user.create(mail, uname, password, undefined, this.request.ip);
|
||||
await token.del(code, token.TYPE_REGISTRATION);
|
||||
this.session.uid = uid;
|
||||
this.response.redirect = this.url('homepage');
|
||||
}
|
||||
}
|
||||
|
||||
class UserLostPassHandler extends Handler {
|
||||
async get() {
|
||||
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
|
||||
this.response.template = 'user_lostpass.html';
|
||||
}
|
||||
|
||||
@param('mail', Types.String, isEmail)
|
||||
async post(domainId: string, mail: string) {
|
||||
if (!await system.get('smtp.user')) throw new SystemError('Cannot send mail');
|
||||
const udoc = await user.getByEmail('system', mail);
|
||||
if (!udoc) throw new UserNotFoundError(mail);
|
||||
const [tid] = await token.add(
|
||||
token.TYPE_LOSTPASS,
|
||||
await system.get('lostpass_token_expire_seconds'),
|
||||
{ uid: udoc._id },
|
||||
);
|
||||
const m = await this.renderHTML('user_lostpass_mail', { url: `lostpass/${tid}`, uname: udoc.uname });
|
||||
await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m);
|
||||
this.response.template = 'user_lostpass_mail_sent.html';
|
||||
}
|
||||
}
|
||||
|
||||
class UserLostPassWithCodeHandler extends Handler {
|
||||
async get({ domainId, code }) {
|
||||
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
|
||||
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
|
||||
const udoc = await user.getById(domainId, tdoc.uid);
|
||||
this.response.body = { uname: udoc.uname };
|
||||
}
|
||||
|
||||
@param('code', Types.String)
|
||||
@param('password', Types.String, isPassword)
|
||||
@param('verifyPassword', Types.String)
|
||||
async post(domainId: string, code: string, password: string, verifyPassword: string) {
|
||||
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
|
||||
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
|
||||
if (password !== verifyPassword) throw new VerifyPasswordError();
|
||||
await user.setPassword(tdoc.uid, password);
|
||||
await token.del(code, token.TYPE_LOSTPASS);
|
||||
this.response.redirect = this.url('homepage');
|
||||
}
|
||||
}
|
||||
|
||||
class UserDetailHandler extends Handler {
|
||||
@param('uid', Types.Int)
|
||||
async get(domainId: string, uid: number) {
|
||||
const isSelfProfile = this.user._id === uid;
|
||||
const udoc = await user.getById(domainId, uid);
|
||||
if (!udoc) throw new UserNotFoundError(uid);
|
||||
const [sdoc, rdocs, [pdocs, pcount]] = await Promise.all([
|
||||
token.getMostRecentSessionByUid(uid),
|
||||
record.getByUid(domainId, uid, 30),
|
||||
paginate(
|
||||
problem.getMulti(domainId, { owner: this.user._id }),
|
||||
1,
|
||||
100,
|
||||
),
|
||||
]);
|
||||
const pdict = await problem.getList(
|
||||
domainId, rdocs.map((rdoc) => rdoc.pid),
|
||||
this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN), false,
|
||||
);
|
||||
// Remove sensitive data
|
||||
if (!isSelfProfile && sdoc) {
|
||||
sdoc.createIp = '';
|
||||
sdoc.updateIp = '';
|
||||
sdoc._id = '';
|
||||
}
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['user_detail', 'user_detail', { uid }],
|
||||
];
|
||||
this.response.template = 'user_detail.html';
|
||||
this.response.body = {
|
||||
isSelfProfile, udoc, sdoc, rdocs, pdocs, pcount, pdict, path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UserDeleteHandler extends Handler {
|
||||
async post({ password }) {
|
||||
this.user.checkPassword(password);
|
||||
const tid = await task.add({
|
||||
executeAfter: moment().add(7, 'days').toDate(),
|
||||
type: 'script',
|
||||
id: 'deleteUser',
|
||||
args: { uid: this.user._id },
|
||||
});
|
||||
await user.setById(this.user._id, { del: tid });
|
||||
this.response.template = 'user_delete_pending.html';
|
||||
}
|
||||
}
|
||||
|
||||
class UserSearchHandler extends Handler {
|
||||
@param('q', Types.String)
|
||||
@param('exectMatch', Types.Boolean)
|
||||
async get(domainId: string, q: string, exactMatch = false) {
|
||||
let udoc = await user.getById(domainId, parseInt(q, 10));
|
||||
const udocs = udoc ? [udoc] : [];
|
||||
udoc = await user.getByUname(domainId, q);
|
||||
if (udoc) udocs.push(udoc);
|
||||
udoc = await user.getByEmail(domainId, q);
|
||||
if (udoc) udocs.push(udoc);
|
||||
if (!exactMatch) udocs.push(...await user.getPrefixList(q, 20));
|
||||
for (const i in udocs) {
|
||||
udocs[i].gravatar = misc.gravatar(udocs[i].gravatar || '');
|
||||
}
|
||||
this.response.body = udocs;
|
||||
}
|
||||
}
|
||||
|
||||
class OauthHandler extends Handler {
|
||||
@param('type', Types.String)
|
||||
async get(domainId: string, type: string) {
|
||||
if (global.Hydro.lib[`oauth_${type}`]) await global.Hydro.lib[`oauth_${type}`].get.call(this);
|
||||
}
|
||||
}
|
||||
|
||||
class OauthCallbackHandler extends Handler {
|
||||
async get(args: any) {
|
||||
let r;
|
||||
if (global.Hydro.lib[`oauth_${args.type}`]) r = await global.Hydro.lib[`oauth_${args.type}`].callback(args);
|
||||
else throw new UserFacingError('Oauth type');
|
||||
const udoc = await user.getByEmail('system', r.email);
|
||||
if (udoc) {
|
||||
this.session.uid = udoc._id;
|
||||
} else {
|
||||
this.checkPriv(PRIV.PRIV_REGISTER_USER);
|
||||
let username = '';
|
||||
r.uname = r.uname || [];
|
||||
r.uname.push(String.random(16));
|
||||
for (const uname of r.uname) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const nudoc = await user.getByUname('system', uname);
|
||||
if (!nudoc) {
|
||||
username = uname;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const uid = await user.create(
|
||||
r.email, username, String.random(32),
|
||||
undefined, this.request.ip,
|
||||
);
|
||||
const $set: any = {
|
||||
oauth: args.type,
|
||||
};
|
||||
if (r.bio) $set.bio = r.bio;
|
||||
if (r.viewLang) $set.viewLang = r.viewLang;
|
||||
await user.setById(uid, $set);
|
||||
this.session.uid = uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('user_login', '/login', UserLoginHandler);
|
||||
Route('user_oauth', '/oauth/:type', OauthHandler);
|
||||
Route('user_oauth_callback', '/oauth/:type/callback', OauthCallbackHandler);
|
||||
Route('user_register', '/register', UserRegisterHandler, PRIV.PRIV_REGISTER_USER);
|
||||
Route('user_register_with_code', '/register/:code', UserRegisterWithCodeHandler, PRIV.PRIV_REGISTER_USER);
|
||||
Route('user_logout', '/logout', UserLogoutHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Route('user_lostpass', '/lostpass', UserLostPassHandler);
|
||||
Route('user_lostpass_with_code', '/lostpass/:code', UserLostPassWithCodeHandler);
|
||||
Route('user_search', '/user/search', UserSearchHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Route('user_delete', '/user/delete', UserDeleteHandler, PRIV.PRIV_USER_PROFILE);
|
||||
Route('user_detail', '/user/:uid', UserDetailHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.user = apply;
|
||||
@@ -0,0 +1,435 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { ObjectID, GridFSBucket } from 'mongodb';
|
||||
import fs from 'fs';
|
||||
import { Dictionary, NumericDictionary } from 'lodash';
|
||||
|
||||
export interface Setting {
|
||||
family: string,
|
||||
key: string,
|
||||
range: Array<[string, string]> | Dictionary<string>,
|
||||
value: any,
|
||||
type: string,
|
||||
name: string,
|
||||
desc: string,
|
||||
flag: number,
|
||||
}
|
||||
|
||||
export interface Udoc extends Dictionary<any> {
|
||||
_id: number,
|
||||
mail: string,
|
||||
mailLower: string,
|
||||
uname: string,
|
||||
unameLower: string,
|
||||
salt: string,
|
||||
hash: string,
|
||||
hashType: string,
|
||||
priv: number,
|
||||
regat: Date,
|
||||
loginat: Date,
|
||||
regip: string,
|
||||
loginip: string,
|
||||
}
|
||||
|
||||
export interface User extends Dictionary<any> {
|
||||
udoc: () => any,
|
||||
dudoc: () => any,
|
||||
_id: number,
|
||||
mail: string,
|
||||
uname: string,
|
||||
salt: () => string,
|
||||
hash: () => string,
|
||||
hashType: string,
|
||||
priv: number,
|
||||
regat: Date,
|
||||
loginat: Date,
|
||||
perm: () => bigint,
|
||||
role: string,
|
||||
regip: () => string,
|
||||
loginip: () => string,
|
||||
hasPerm: (perm: bigint) => boolean,
|
||||
hasPriv: (priv: number) => boolean,
|
||||
checkPassword: (password: string) => void,
|
||||
}
|
||||
|
||||
export type Udict = NumericDictionary<User>;
|
||||
|
||||
export interface ProblemDataSource {
|
||||
host?: string, // Empty for local
|
||||
domainId: string,
|
||||
pid: number,
|
||||
}
|
||||
|
||||
// ObjectID for built-in files, ProblemDataSource for other source (RemoteJudge, etc.)
|
||||
export type ProblemData = ObjectID | ProblemDataSource;
|
||||
|
||||
export interface TestCaseConfig {
|
||||
input: string,
|
||||
output: string,
|
||||
time?: number,
|
||||
memory?: number,
|
||||
}
|
||||
|
||||
export enum LocalProblemType {
|
||||
Default = 'default',
|
||||
SubmitAnswer = 'submit_answer',
|
||||
Interactive = 'interactive',
|
||||
}
|
||||
|
||||
export enum RemoteProblemType {
|
||||
RemoteJudge = 'remotejudge',
|
||||
}
|
||||
|
||||
export enum RemoteServerType {
|
||||
vj4 = 'vj4',
|
||||
syzoj = 'syzoj',
|
||||
}
|
||||
|
||||
export enum SubtaskType {
|
||||
min = 'min',
|
||||
max = 'max',
|
||||
sum = 'sum',
|
||||
}
|
||||
|
||||
export interface SubtaskConfig {
|
||||
time?: number,
|
||||
memory?: number,
|
||||
score?: number,
|
||||
type?: SubtaskType,
|
||||
cases?: TestCaseConfig[],
|
||||
}
|
||||
|
||||
export interface LocalProblemConfig {
|
||||
type?: LocalProblemType,
|
||||
score?: number,
|
||||
time?: number,
|
||||
memory?: number,
|
||||
checker_type?: string,
|
||||
checker?: string,
|
||||
interactor?: string,
|
||||
user_extra_files?: string[],
|
||||
judge_extra_files?: string[],
|
||||
cases?: TestCaseConfig[],
|
||||
subtasks?: SubtaskConfig[],
|
||||
}
|
||||
|
||||
export interface RemoteProblemConfig {
|
||||
type?: RemoteProblemType,
|
||||
server_type?: RemoteServerType,
|
||||
url?: string,
|
||||
// TODO handle username&password storage
|
||||
pid?: string,
|
||||
}
|
||||
|
||||
export type ProblemConfig = LocalProblemConfig | RemoteProblemConfig;
|
||||
|
||||
export interface Pdoc {
|
||||
_id: ObjectID,
|
||||
domainId: string,
|
||||
docType: number,
|
||||
docId: number,
|
||||
pid: string,
|
||||
owner: number,
|
||||
title: string,
|
||||
content: string,
|
||||
nSubmit: number,
|
||||
nAccept: number,
|
||||
tag: string[],
|
||||
category: string[],
|
||||
difficulty: number,
|
||||
data?: ProblemData,
|
||||
hidden: boolean,
|
||||
config: ProblemConfig,
|
||||
acMsg?: string,
|
||||
}
|
||||
|
||||
export type Pdict = NumericDictionary<Pdoc>;
|
||||
|
||||
export interface ProblemStatusDoc {
|
||||
_id: ObjectID,
|
||||
docId: number,
|
||||
docType: number,
|
||||
domainId: string,
|
||||
uid: number,
|
||||
rid?: ObjectID,
|
||||
status?: number,
|
||||
star?: boolean,
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
time: number,
|
||||
memory: number,
|
||||
status: number,
|
||||
message: string,
|
||||
}
|
||||
|
||||
export interface PretestConfig {
|
||||
time?: number,
|
||||
memory?: number,
|
||||
input: string,
|
||||
}
|
||||
|
||||
export interface ContestInfo {
|
||||
type: number,
|
||||
tid: ObjectID,
|
||||
}
|
||||
|
||||
export interface Rdoc {
|
||||
_id: ObjectID,
|
||||
domainId: string,
|
||||
pid: number
|
||||
uid: number,
|
||||
lang: string,
|
||||
code: string,
|
||||
score: number,
|
||||
memory: number,
|
||||
time: number,
|
||||
judgeTexts: string[],
|
||||
compilerTexts: string[],
|
||||
testCases: TestCase[],
|
||||
rejudged: boolean,
|
||||
judger: string,
|
||||
judgeAt: Date,
|
||||
status: number,
|
||||
hidden: boolean,
|
||||
config?: PretestConfig,
|
||||
contest?: ContestInfo,
|
||||
}
|
||||
|
||||
export interface ScoreboardNode {
|
||||
type: string,
|
||||
value: string,
|
||||
raw?: any,
|
||||
}
|
||||
|
||||
export type PenaltyRules = Dictionary<number>;
|
||||
|
||||
export interface Tdoc {
|
||||
_id: ObjectID,
|
||||
domainId: string,
|
||||
docId: ObjectID,
|
||||
docType: number,
|
||||
owner: number,
|
||||
beginAt: Date,
|
||||
endAt: Date,
|
||||
attend: number,
|
||||
title: string,
|
||||
content: string,
|
||||
rule: string,
|
||||
pids: number[],
|
||||
|
||||
// For homework
|
||||
penaltySince?: Date,
|
||||
penaltyRules?: PenaltyRules,
|
||||
|
||||
// For training
|
||||
dag: any
|
||||
}
|
||||
|
||||
export interface DomainDoc extends Dictionary<any> {
|
||||
_id: string,
|
||||
owner: number,
|
||||
roles: Dictionary<string>,
|
||||
gravatar: string,
|
||||
bulletin: string,
|
||||
pidCounter: number,
|
||||
}
|
||||
|
||||
// Message
|
||||
export interface Mdoc {
|
||||
_id: ObjectID,
|
||||
from: number,
|
||||
to: number,
|
||||
content: string,
|
||||
flag: number,
|
||||
}
|
||||
|
||||
// Userfile
|
||||
export interface Ufdoc {
|
||||
_id: ObjectID,
|
||||
secret: string,
|
||||
md5: string,
|
||||
owner: number,
|
||||
size: number,
|
||||
}
|
||||
|
||||
// Blacklist
|
||||
export interface Bdoc {
|
||||
_id: string, // ip
|
||||
expireAt: Date,
|
||||
}
|
||||
|
||||
// Discussion
|
||||
export interface Ddoc {
|
||||
_id: ObjectID,
|
||||
docType: number,
|
||||
docId: ObjectID,
|
||||
parentType: number,
|
||||
parentId: ObjectID | number,
|
||||
owner: number,
|
||||
title: string,
|
||||
content: string,
|
||||
ip: string,
|
||||
pin: boolean,
|
||||
highlight: boolean,
|
||||
}
|
||||
|
||||
// Discussion reply
|
||||
export interface Drdoc {
|
||||
_id: ObjectID,
|
||||
docType: number,
|
||||
docId: ObjectID,
|
||||
parentType: number,
|
||||
parentId: ObjectID,
|
||||
owner: number,
|
||||
ip: string,
|
||||
content: string,
|
||||
reply: Drrdoc[],
|
||||
}
|
||||
|
||||
// Discussion Tail Reply
|
||||
export interface Drrdoc {
|
||||
_id: ObjectID,
|
||||
owner: number,
|
||||
content: string,
|
||||
ip: string,
|
||||
}
|
||||
|
||||
export interface TokenDoc {
|
||||
_id: string,
|
||||
tokenType: number,
|
||||
createAt: Date,
|
||||
updateAt: Date,
|
||||
expireAt: Date,
|
||||
[key: string]: any,
|
||||
}
|
||||
|
||||
export interface ContestStat extends Dictionary<any> {
|
||||
detail: any,
|
||||
}
|
||||
|
||||
export interface ContestRule {
|
||||
TEXT: string,
|
||||
check: (args: any) => any,
|
||||
statusSort: any,
|
||||
showScoreboard: (tdoc: Tdoc, now: Date) => boolean,
|
||||
showRecord: (tdoc: Tdoc, now: Date) => boolean,
|
||||
stat: (tdoc: Tdoc, journal: any[]) => ContestStat,
|
||||
scoreboard: (
|
||||
isExport: boolean, _: (s: string) => string,
|
||||
tdoc: Tdoc, rankedTsdocs: any[] | Generator<any>, udict: Udict, pdict: Pdict
|
||||
) => ScoreboardNode[][],
|
||||
rank: (tsdocs: any[]) => any[] | Generator<any>,
|
||||
}
|
||||
|
||||
export type ContestRules = Dictionary<ContestRule>;
|
||||
|
||||
export type ProblemImporter = (url: string, handler: any) =>
|
||||
Promise<[Pdoc, fs.ReadStream?]> | [Pdoc, fs.ReadStream?];
|
||||
|
||||
export interface Script {
|
||||
run: (args: any, report: Function) => any,
|
||||
description: string,
|
||||
validate: any,
|
||||
}
|
||||
|
||||
export interface JudgeResultBody {
|
||||
domainId: string,
|
||||
rid: ObjectID,
|
||||
judger?: number,
|
||||
progress?: number
|
||||
case?: {
|
||||
status: number,
|
||||
time: number,
|
||||
memory: number,
|
||||
message?: string,
|
||||
},
|
||||
status?: number,
|
||||
score?: number,
|
||||
time?: number,
|
||||
memory?: number,
|
||||
message?: string,
|
||||
compilerText?: string,
|
||||
|
||||
// For pretest
|
||||
stdout?: string,
|
||||
stderr?: string,
|
||||
}
|
||||
|
||||
export type PathComponent = [string, string, any?, boolean?];
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
Hydro: {
|
||||
model: {
|
||||
blacklist: typeof import('./model/blacklist'),
|
||||
builtin: typeof import('./model/builtin'),
|
||||
contest: typeof import('./model/contest'),
|
||||
discussion: typeof import('./model/discussion'),
|
||||
document: typeof import('./model/document'),
|
||||
domain: typeof import('./model/domain'),
|
||||
file: typeof import('./model/file'),
|
||||
message: typeof import('./model/message'),
|
||||
opcount: typeof import('./model/opcount'),
|
||||
problem: typeof import('./model/problem'),
|
||||
record: typeof import('./model/record'),
|
||||
setting: typeof import('./model/setting'),
|
||||
solution: typeof import('./model/solution'),
|
||||
system: typeof import('./model/system'),
|
||||
task: typeof import('./model/task'),
|
||||
token: typeof import('./model/token'),
|
||||
training: typeof import('./model/training'),
|
||||
user: typeof import('./model/user'),
|
||||
[key: string]: any,
|
||||
},
|
||||
handler: { [key: string]: Function },
|
||||
script: { [key: string]: Script },
|
||||
service: {
|
||||
bus: typeof import('./service/bus'),
|
||||
db: typeof import('./service/db'),
|
||||
gridfs: GridFSBucket,
|
||||
monitor: typeof import('./service/monitor'),
|
||||
server: typeof import('./service/server'),
|
||||
[key: string]: any,
|
||||
},
|
||||
lib: {
|
||||
download: typeof import('./lib/download').default,
|
||||
'hash.hydro': typeof import('./lib/hash.hydro').default,
|
||||
i18n: typeof import('./lib/i18n').default,
|
||||
'import.syzoj': typeof import('./lib/import.syzoj').syzoj,
|
||||
jwt: typeof import('./lib/jwt'),
|
||||
logger: typeof import('./lib/logger').default,
|
||||
mail: typeof import('./lib/mail'),
|
||||
md5: typeof import('./lib/md5').default,
|
||||
misc: typeof import('./lib/misc'),
|
||||
nav: typeof import('./lib/nav').default,
|
||||
paginate: typeof import('./lib/paginate').default,
|
||||
rank: typeof import('./lib/rank').default,
|
||||
rating: typeof import('./lib/rating').default,
|
||||
testdataConfig: typeof import('./lib/testdataConfig'),
|
||||
sha1: typeof import('./lib/sha1').default,
|
||||
sysinfo: typeof import('./lib/sysinfo'),
|
||||
'testdata.convert.ini': typeof import('./lib/testdata.convert.ini').default,
|
||||
useragent: typeof import('./lib/useragent'),
|
||||
validator: typeof import('./lib/validator'),
|
||||
[key: string]: any
|
||||
},
|
||||
stat: any,
|
||||
ui: {
|
||||
manifest: Dict<string>,
|
||||
nav: any,
|
||||
template: { [key: string]: string },
|
||||
},
|
||||
error: typeof import('./error'),
|
||||
locales: Dict<Dict<string>>,
|
||||
postInit: Array<() => Promise<any>>,
|
||||
},
|
||||
onDestory: Array<() => void | Promise<void>>,
|
||||
addons: string[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'cluster' {
|
||||
let isFirstWorker: boolean;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import superagent from 'superagent';
|
||||
import fs from 'fs';
|
||||
|
||||
async function _download(url: string, path: string, retry: number) {
|
||||
const w = fs.createWriteStream(path);
|
||||
superagent.get(url).retry(retry).pipe(w);
|
||||
await new Promise((resolve, reject) => {
|
||||
w.on('finish', resolve);
|
||||
w.on('error', reject);
|
||||
});
|
||||
return path;
|
||||
}
|
||||
|
||||
export default function download(url: string, path?: string, retry = 3) {
|
||||
if (path) return _download(url, path, retry);
|
||||
return superagent.get(url).retry(retry);
|
||||
}
|
||||
|
||||
global.Hydro.lib.download = download;
|
||||
@@ -0,0 +1,7 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export default function hash(password: string, salt: string) {
|
||||
return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256').toString('hex').substr(0, 64);
|
||||
}
|
||||
|
||||
global.Hydro.lib['hash.hydro'] = hash;
|
||||
@@ -0,0 +1,56 @@
|
||||
const locales = {};
|
||||
|
||||
declare global {
|
||||
interface String {
|
||||
format: (...args: Array<any>) => string;
|
||||
rawformat: (object: any) => string;
|
||||
translate: (...languages: string[]) => string;
|
||||
}
|
||||
}
|
||||
|
||||
String.prototype.format = function formatStr(...args) {
|
||||
let result = this;
|
||||
if (args.length > 0) {
|
||||
if (args.length === 1 && typeof (args[0]) === 'object') {
|
||||
for (const key in args) {
|
||||
if (args[key] !== undefined) {
|
||||
const reg = new RegExp(`(\\{${key}\\})`, 'g');
|
||||
result = result.replace(reg, args[key]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] !== undefined) {
|
||||
const reg = new RegExp(`(\\{)${i}(\\})`, 'g');
|
||||
result = result.replace(reg, args[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
String.prototype.rawformat = function rawFormat(object) {
|
||||
const res = this.split('{@}');
|
||||
return [res[0], object, res[1]].join();
|
||||
};
|
||||
|
||||
String.prototype.translate = function translate(...languages: string[]) {
|
||||
if (languages.length === 0) languages = ['zh_CN'];
|
||||
for (const language of languages) {
|
||||
if (locales[language]) {
|
||||
if (locales[language][this] !== undefined) return locales[language][this];
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
export default 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 = load;
|
||||
@@ -0,0 +1,71 @@
|
||||
import assert from 'assert';
|
||||
import superagent from 'superagent';
|
||||
import download from './download';
|
||||
import { ValidationError, RemoteOnlineJudgeError } from '../error';
|
||||
|
||||
const RE_SYZOJ = /https?:\/\/([a-zA-Z0-9.]+)\/problem\/([0-9]+)\/?/i;
|
||||
|
||||
export async function syzoj(url: string, handler: any) {
|
||||
assert(url.match(RE_SYZOJ), new ValidationError('url'));
|
||||
if (!url.endsWith('/')) url += '/';
|
||||
const [, host, pid] = RE_SYZOJ.exec(url);
|
||||
const res = await superagent.get(`${url}export`);
|
||||
assert(res.status === 200, new RemoteOnlineJudgeError('Cannot connect to target server'));
|
||||
assert(res.body.success, new RemoteOnlineJudgeError((res.body.error || {}).message));
|
||||
const p = res.body.obj;
|
||||
const content = [];
|
||||
if (p.description) {
|
||||
content.push(
|
||||
handler.translate('problem.import.problem_description'),
|
||||
p.description,
|
||||
);
|
||||
}
|
||||
if (p.input_format) {
|
||||
content.push(
|
||||
handler.translate('problem.import.input_format'),
|
||||
p.input_format,
|
||||
);
|
||||
}
|
||||
if (p.output_format) {
|
||||
content.push(
|
||||
handler.translate('problem.import.output_format'),
|
||||
p.output_format,
|
||||
);
|
||||
}
|
||||
if (p.hint) {
|
||||
content.push(
|
||||
handler.translate('problem.import.hint'),
|
||||
p.hint,
|
||||
);
|
||||
}
|
||||
if (p.limit_and_hint) {
|
||||
content.push(
|
||||
handler.translate('problem.import.limit_and_hint'),
|
||||
p.limit_and_hint,
|
||||
);
|
||||
}
|
||||
if (p.have_additional_file) {
|
||||
content.push(
|
||||
handler.translate('problem.import.additional_file'),
|
||||
`${url}download/additional_file`,
|
||||
);
|
||||
}
|
||||
const pdoc = {
|
||||
title: p.title,
|
||||
content: content.join(' \n'),
|
||||
owner: handler.user._id,
|
||||
from: url,
|
||||
pid: `${host}_${pid}`,
|
||||
tags: p.tags,
|
||||
config: {
|
||||
time: p.time_limit,
|
||||
memory: p.memory_limit * 1024,
|
||||
filename: p.file_io_input_name,
|
||||
type: p.type,
|
||||
},
|
||||
};
|
||||
const r = download(`${url}testdata/download`);
|
||||
return [pdoc, r];
|
||||
}
|
||||
|
||||
global.Hydro.lib['import.syzoj'] = syzoj;
|
||||
@@ -0,0 +1,35 @@
|
||||
function decodeBase64WithUriEncoding(encodedText: string) {
|
||||
return Buffer.from(encodedText, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
function unescapedString(escapedString: string) {
|
||||
escapedString += new Array(5 - (escapedString.length % 4)).join('=');
|
||||
return escapedString.replace(/-/g, '+').replace(/_/g, '/');
|
||||
}
|
||||
|
||||
function decodeJWT(idToken: string) {
|
||||
const token = idToken.split('.');
|
||||
if (token.length !== 3) {
|
||||
throw new Error('Invalid idToken');
|
||||
}
|
||||
try {
|
||||
const headerSegment = JSON.parse(decodeBase64WithUriEncoding(token[0]));
|
||||
const payloadSegment = JSON.parse(decodeBase64WithUriEncoding(token[1]));
|
||||
const signature = unescapedString(token[2]);
|
||||
return {
|
||||
dataToSign: [token[0], token[1]].join('.'),
|
||||
header: headerSegment,
|
||||
payload: payloadSegment,
|
||||
signature,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
}
|
||||
|
||||
export function decode(idToken: string) {
|
||||
const decodedJWT = decodeJWT(idToken);
|
||||
return decodedJWT.payload;
|
||||
}
|
||||
|
||||
global.Hydro.lib.jwt = { decode };
|
||||
@@ -0,0 +1,41 @@
|
||||
import serialize from 'serialize-javascript';
|
||||
|
||||
function wrap(func: Function, type: string) {
|
||||
return (...args: any) => {
|
||||
const time = new Date();
|
||||
func(...args);
|
||||
if (global.Hydro.model.message) {
|
||||
try {
|
||||
global.Hydro.model.message.send(1, 1, serialize({ time, type, args }), 0);
|
||||
} catch (e) {
|
||||
func(e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class Logger {
|
||||
log: (...args: any[]) => void;
|
||||
|
||||
error: (...args: any[]) => void;
|
||||
|
||||
info: (...args: any[]) => void;
|
||||
|
||||
warn: (...args: any[]) => void;
|
||||
|
||||
debug: (...args: any[]) => void;
|
||||
|
||||
constructor() {
|
||||
this.log = wrap(console.log, 'log');
|
||||
this.error = wrap(console.error, 'error');
|
||||
this.info = wrap(console.info, 'info');
|
||||
this.warn = wrap(console.warn, 'warn');
|
||||
this.debug = wrap(console.debug, 'debug');
|
||||
}
|
||||
}
|
||||
|
||||
const exp = new Logger();
|
||||
|
||||
global.Hydro.lib.logger = exp;
|
||||
|
||||
export default exp;
|
||||
@@ -0,0 +1,39 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { SendMailError } from '../error';
|
||||
|
||||
export async function sendMail(to: string, subject: string, text: string, html: string) {
|
||||
let t: unknown;
|
||||
try {
|
||||
const { system } = global.Hydro.model;
|
||||
const [host, port, secure, user, pass, from] = await Promise.all([
|
||||
system.get('smtp.host'),
|
||||
system.get('smtp.port'),
|
||||
system.get('smtp.secure'),
|
||||
system.get('smtp.user'),
|
||||
system.get('smtp.pass'),
|
||||
system.get('smtp.from'),
|
||||
]);
|
||||
const transporter = nodemailer.createTransport({
|
||||
host, port, secure, auth: { user, pass },
|
||||
});
|
||||
t = await new Promise((resolve, reject) => {
|
||||
transporter.sendMail({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
}, (err, info) => {
|
||||
if (err) reject(err);
|
||||
else resolve(info);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
throw new SendMailError(to);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
global.Hydro.lib.mail = {
|
||||
sendMail,
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
// From https://github.com/pvorb/node-md5/blob/master/md5.js
|
||||
// BSD 3-Clause "New" or "Revised" License
|
||||
|
||||
/* eslint-disable max-len */
|
||||
function safeAdd(x, y) {
|
||||
const lsw = (x & 0xffff) + (y & 0xffff);
|
||||
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||
return (msw << 16) | (lsw & 0xffff);
|
||||
}
|
||||
const bitRotateLeft = (num, cnt) => (num << cnt) | (num >>> (32 - cnt));
|
||||
const md5cmn = (q, a, b, x, s, t) => safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
|
||||
const md5ff = (a, b, c, d, x, s, t) => md5cmn((b & c) | (~b & d), a, b, x, s, t);
|
||||
const md5gg = (a, b, c, d, x, s, t) => md5cmn((b & d) | (c & ~d), a, b, x, s, t);
|
||||
const md5hh = (a, b, c, d, x, s, t) => md5cmn(b ^ c ^ d, a, b, x, s, t);
|
||||
const md5ii = (a, b, c, d, x, s, t) => md5cmn(c ^ (b | ~d), a, b, x, s, t);
|
||||
function binlMD5(x, len) {
|
||||
x[len >> 5] |= 0x80 << len % 32;
|
||||
x[(((len + 64) >>> 9) << 4) + 14] = len;
|
||||
let i;
|
||||
let olda;
|
||||
let oldb;
|
||||
let oldc;
|
||||
let oldd;
|
||||
let a = 1732584193;
|
||||
let b = -271733879;
|
||||
let c = -1732584194;
|
||||
let d = 271733878;
|
||||
for (i = 0; i < x.length; i += 16) {
|
||||
olda = a;
|
||||
oldb = b;
|
||||
oldc = c;
|
||||
oldd = d;
|
||||
a = md5ff(a, b, c, d, x[i], 7, -680876936);
|
||||
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
|
||||
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
|
||||
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
|
||||
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
|
||||
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
|
||||
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
|
||||
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
|
||||
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
|
||||
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
|
||||
c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
|
||||
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
|
||||
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
|
||||
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
|
||||
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
|
||||
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
|
||||
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
|
||||
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
|
||||
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
|
||||
b = md5gg(b, c, d, a, x[i], 20, -373897302);
|
||||
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
|
||||
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
|
||||
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
|
||||
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
|
||||
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
|
||||
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
|
||||
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
|
||||
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
|
||||
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
|
||||
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
|
||||
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
|
||||
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
|
||||
a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
|
||||
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
|
||||
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
|
||||
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
|
||||
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
|
||||
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
|
||||
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
|
||||
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
|
||||
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
|
||||
d = md5hh(d, a, b, c, x[i], 11, -358537222);
|
||||
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
|
||||
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
|
||||
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
|
||||
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
|
||||
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
|
||||
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
|
||||
a = md5ii(a, b, c, d, x[i], 6, -198630844);
|
||||
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
|
||||
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
|
||||
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
|
||||
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
|
||||
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
|
||||
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
|
||||
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
|
||||
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
|
||||
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
|
||||
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
|
||||
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
|
||||
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
|
||||
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
|
||||
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
|
||||
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
|
||||
a = safeAdd(a, olda);
|
||||
b = safeAdd(b, oldb);
|
||||
c = safeAdd(c, oldc);
|
||||
d = safeAdd(d, oldd);
|
||||
}
|
||||
return [a, b, c, d];
|
||||
}
|
||||
function binl2rstr(input) {
|
||||
let i;
|
||||
let output = '';
|
||||
const length32 = input.length * 32;
|
||||
for (i = 0; i < length32; i += 8) { output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff); }
|
||||
return output;
|
||||
}
|
||||
function rstr2binl(input) {
|
||||
let i;
|
||||
const output = [];
|
||||
output[(input.length >> 2) - 1] = undefined;
|
||||
for (i = 0; i < output.length; i += 1) { output[i] = 0; }
|
||||
const length8 = input.length * 8;
|
||||
for (i = 0; i < length8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
|
||||
return output;
|
||||
}
|
||||
const rstrMD5 = (s) => binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
|
||||
function rstr2hex(input) {
|
||||
const hexTab = '0123456789abcdef';
|
||||
let output = '';
|
||||
let x;
|
||||
let i;
|
||||
for (i = 0; i < input.length; i += 1) {
|
||||
x = input.charCodeAt(i);
|
||||
output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export default function md5(s: string) {
|
||||
return rstr2hex(rstrMD5(unescape(encodeURIComponent(s))));
|
||||
}
|
||||
|
||||
global.Hydro.lib.md5 = md5;
|
||||
@@ -0,0 +1,78 @@
|
||||
import AnsiUp from 'ansi_up';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import md5 from './md5';
|
||||
|
||||
const AU = new AnsiUp();
|
||||
|
||||
export function ansiToHtml(str: string) {
|
||||
return AU.ansi_to_html(str);
|
||||
}
|
||||
|
||||
export function gravatar(email: string, s = 32) {
|
||||
return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${s}`;
|
||||
}
|
||||
|
||||
export function datetimeSpan(dt: Date | ObjectID, relative = true, format = '%Y-%m-%d %H:%M:%S') {
|
||||
if (!dt) return 'DATETIME_SPAN_ERROR';
|
||||
if (dt instanceof ObjectID) dt = new Date(dt.generationTime * 1000);
|
||||
else if (typeof dt === 'number' || typeof dt === 'string') dt = new Date(dt);
|
||||
return '<span class="time{0}" data-timestamp="{1}">{2}</span>'.format(
|
||||
relative ? ' relative' : '',
|
||||
dt.getTime() / 1000,
|
||||
dt.format(format),
|
||||
);
|
||||
}
|
||||
|
||||
export function* paginate(page: number, numPages: number) {
|
||||
const radius = 2;
|
||||
let first: number;
|
||||
let last: number;
|
||||
if (page > 1) {
|
||||
if (page > 2) yield ['first', 1];
|
||||
yield ['previous', page - 1];
|
||||
}
|
||||
if (page <= radius) [first, last] = [1, Math.min(1 + radius * 2, numPages)];
|
||||
else if (page >= numPages - radius) {
|
||||
[first, last] = [Math.max(1, numPages - radius * 2), numPages];
|
||||
} else {
|
||||
[first, last] = [page - radius, page + radius];
|
||||
}
|
||||
if (first > 1) yield ['ellipsis', 0];
|
||||
for (let page0 = first; page0 < last + 1; page0++) {
|
||||
if (page0 !== page) yield ['page', page0];
|
||||
else yield ['current', page];
|
||||
}
|
||||
if (last < numPages) yield ['ellipsis', 0];
|
||||
if (page < numPages) {
|
||||
yield ['next', page + 1];
|
||||
if (page < numPages - 1) yield ['last', numPages];
|
||||
}
|
||||
}
|
||||
|
||||
export function size(s: number, base = 1) {
|
||||
s *= base;
|
||||
const unit = 1024;
|
||||
const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
for (const unitName of unitNames) {
|
||||
if (s < unit) return '{0} {1}'.format(Math.round(s * 10) / 10, unitName);
|
||||
s /= unit;
|
||||
}
|
||||
return `${Math.round(s * unit)} ${unitNames[unitNames.length - 1]}`;
|
||||
}
|
||||
|
||||
function _digit2(number: number) {
|
||||
return number < 10 ? `0${number}` : number.toString();
|
||||
}
|
||||
|
||||
export function formatSeconds(_seconds = '0') {
|
||||
const seconds = parseInt(_seconds, 10);
|
||||
return '{0}:{1}:{2}'.format(
|
||||
_digit2(Math.floor(seconds / 3600)),
|
||||
_digit2(Math.floor((seconds % 3600) / 60)),
|
||||
_digit2(seconds % 60),
|
||||
);
|
||||
}
|
||||
|
||||
global.Hydro.lib.misc = {
|
||||
gravatar, datetimeSpan, paginate, size, formatSeconds, ansiToHtml,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { isNull } from 'lodash';
|
||||
import { PERM, PRIV } from '../model/builtin';
|
||||
|
||||
global.Hydro.ui.nav = [];
|
||||
|
||||
const trueChecker = () => true;
|
||||
const Checker = (perm: bigint, priv: number, checker: Function = trueChecker) => (handler) => (
|
||||
checker(handler)
|
||||
&& (perm ? handler.user.hasPerm(perm) : true)
|
||||
&& (priv ? handler.user.hasPriv(priv) : true)
|
||||
);
|
||||
|
||||
const Item = (
|
||||
name: string, args: any, prefix: string,
|
||||
...permPrivChecker: Array<number | bigint | Function>
|
||||
) => {
|
||||
let _priv: number;
|
||||
let _perm: bigint;
|
||||
let checker: Function = trueChecker;
|
||||
for (const item of permPrivChecker) {
|
||||
if (item instanceof Object && !isNull(item)) {
|
||||
if (item instanceof Array) {
|
||||
if (typeof item[0] === 'number') {
|
||||
// @ts-ignore
|
||||
_priv = item;
|
||||
} else if (typeof item[0] === 'bigint') {
|
||||
// @ts-ignore
|
||||
_perm = item;
|
||||
}
|
||||
} else if (typeof item.call !== 'undefined') {
|
||||
checker = item;
|
||||
}
|
||||
} else if (typeof item === 'number') {
|
||||
_priv = item;
|
||||
} else if (typeof item === 'bigint') {
|
||||
_perm = item;
|
||||
}
|
||||
}
|
||||
checker = Checker(_perm, _priv, checker);
|
||||
global.Hydro.ui.nav.push({
|
||||
name, args: args || {}, prefix, checker,
|
||||
});
|
||||
};
|
||||
|
||||
Item('homepage', null, 'homepage');
|
||||
Item('problem_main', null, 'problem', PERM.PERM_VIEW_PROBLEM);
|
||||
Item('training_main', null, 'training', PERM.PERM_VIEW_TRAINING);
|
||||
Item('homework_main', null, 'homework', PERM.PERM_VIEW_HOMEWORK);
|
||||
Item('discussion_main', null, 'discussion', PERM.PERM_VIEW_DISCUSSION);
|
||||
Item('contest_main', null, 'contest', PERM.PERM_VIEW_CONTEST);
|
||||
Item('record_main', null, 'record');
|
||||
Item('ranking', null, 'ranking', PERM.PERM_VIEW_RANKING);
|
||||
Item('domain_dashboard', null, 'domain', PERM.PERM_EDIT_DOMAIN);
|
||||
Item('manage_dashboard', null, 'manage', PRIV.PRIV_EDIT_SYSTEM);
|
||||
|
||||
global.Hydro.lib.nav = Item;
|
||||
export default Item;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Cursor } from 'mongodb';
|
||||
import { ValidationError } from '../error';
|
||||
|
||||
export default async function paginate(
|
||||
cursor: Cursor, page: number, pageSize: number,
|
||||
): Promise<[any[]/* docs */, number/* numPages */, number/* count */]> {
|
||||
if (page <= 0) throw new ValidationError('page');
|
||||
const [count, pageDocs] = await Promise.all([
|
||||
cursor.count(),
|
||||
cursor.skip((page - 1) * pageSize).limit(pageSize).toArray(),
|
||||
]);
|
||||
const numPages = Math.floor((count + pageSize - 1) / pageSize);
|
||||
return [pageDocs, numPages, count];
|
||||
}
|
||||
|
||||
global.Hydro.lib.paginate = paginate;
|
||||
@@ -0,0 +1,14 @@
|
||||
function* ranked<T>(diter: T[], equ = (a: T, b: T) => a === b): Generator<[number, T], void> {
|
||||
let last = null;
|
||||
let r = 0;
|
||||
let count = 0;
|
||||
for (const doc of diter) {
|
||||
count++;
|
||||
if (count === 1 || !equ(last, doc)) r = count;
|
||||
last = doc;
|
||||
yield [r, doc];
|
||||
}
|
||||
}
|
||||
|
||||
global.Hydro.lib.rank = ranked;
|
||||
export default ranked;
|
||||
@@ -0,0 +1,127 @@
|
||||
// Based on https://github.com/QAQrz/Codeforces-Rating-System/blob/master/rating.py
|
||||
|
||||
class User {
|
||||
uid: number;
|
||||
|
||||
rank: number;
|
||||
|
||||
old: number;
|
||||
|
||||
seed?: number;
|
||||
|
||||
delta?: number;
|
||||
|
||||
new?: number;
|
||||
|
||||
constructor(rank: number, old: number, uid = 0) {
|
||||
this.rank = rank;
|
||||
this.old = old;
|
||||
this.seed = 1;
|
||||
this.uid = uid;
|
||||
}
|
||||
}
|
||||
|
||||
class RatingCalculator {
|
||||
users: User[];
|
||||
|
||||
constructor(users: User[]) {
|
||||
this.users = [];
|
||||
for (const user of users) {
|
||||
this.users.push(new User(user.rank, user.old, user.uid));
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
calP(a: User, b: User) {
|
||||
return 1 / (1 + 10 ** ((b.old - a.old) / 400));
|
||||
}
|
||||
|
||||
getExSeed(rating: number, ownUser: User) {
|
||||
const exUser = new User(0.0, rating);
|
||||
let result = 1;
|
||||
for (const user of this.users) {
|
||||
if (user !== ownUser) result += this.calP(user, exUser);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
calRating(rank: number, user: User) {
|
||||
let left = 1;
|
||||
let right = 8000;
|
||||
while (right - left > 1) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
if (this.getExSeed(mid, user) < rank) right = mid;
|
||||
else left = mid;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
calculate() {
|
||||
// Calculate seed
|
||||
for (let i = 0; i < this.users.length; i++) {
|
||||
this.users[i].seed = 1;
|
||||
for (let j = 0; j < this.users.length; j++) {
|
||||
if (i !== j) {
|
||||
this.users[i].seed += this.calP(this.users[j], this.users[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Calculate initial delta and sum_delta
|
||||
let sumDelta = 0;
|
||||
for (const user of this.users) {
|
||||
user.delta = (this.calRating(Math.sqrt(user.rank * user.seed), user) - user.old) / 2;
|
||||
sumDelta += user.delta;
|
||||
}
|
||||
// Calculate first inc
|
||||
let inc = Math.floor(-sumDelta / this.users.length) - 1;
|
||||
for (const user of this.users) user.delta += inc;
|
||||
// Calculate second inc
|
||||
this.users = this.users.sort((a, b) => b.old - a.old);
|
||||
const s = Math.min(
|
||||
this.users.length,
|
||||
Math.floor(4 * Math.round(Math.sqrt(this.users.length))),
|
||||
);
|
||||
let sumS = 0;
|
||||
for (let i = 0; i < s; i++) {
|
||||
sumS += this.users[i].delta;
|
||||
}
|
||||
inc = Math.min(Math.max(Math.floor(-sumS / s), -10), 0);
|
||||
// Calculate new rating
|
||||
for (const user of this.users) {
|
||||
user.delta += inc;
|
||||
user.new = user.old + user.delta;
|
||||
}
|
||||
this.users = this.users.sort((a, b) => a.rank - b.rank);
|
||||
return this.users;
|
||||
}
|
||||
}
|
||||
|
||||
interface RatingInputUser {
|
||||
old: number,
|
||||
uid: number,
|
||||
rank: number,
|
||||
}
|
||||
|
||||
interface RatingOutputUser {
|
||||
new: number,
|
||||
uid: number,
|
||||
}
|
||||
|
||||
export default function calculate(users: RatingInputUser[]): RatingOutputUser[] {
|
||||
let last_idx = 0;
|
||||
let last_rank = 1;
|
||||
for (let i = 1; i < users.length; i++) {
|
||||
if (users[i].rank > last_rank) {
|
||||
for (let j = last_idx; j < i; j++) users[j].rank = i;
|
||||
last_idx = i;
|
||||
last_rank = users[i].rank;
|
||||
}
|
||||
}
|
||||
for (let i = last_idx; i < users.length; i++) {
|
||||
users[i].rank = users.length;
|
||||
}
|
||||
const calculator = new RatingCalculator(users);
|
||||
return calculator.calculate() as RatingOutputUser[];
|
||||
}
|
||||
|
||||
global.Hydro.lib.rating = calculate;
|
||||
@@ -0,0 +1,10 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
function sha1(content: string) {
|
||||
const hash = crypto.createHash('sha1');
|
||||
hash.update(content);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
global.Hydro.lib.sha1 = sha1;
|
||||
export default sha1;
|
||||
@@ -0,0 +1,62 @@
|
||||
import systeminformation from 'systeminformation';
|
||||
import { size } from './misc';
|
||||
|
||||
const cache = {
|
||||
mid: '', cpu: '', osinfo: '', flags: '',
|
||||
};
|
||||
|
||||
export async function get() {
|
||||
const [Cpu, Memory, OsInfo, CurrentLoad, CpuFlags, CpuTemp, Battery] = await Promise.all([
|
||||
systeminformation.cpu(),
|
||||
systeminformation.mem(),
|
||||
systeminformation.osInfo(),
|
||||
systeminformation.currentLoad(),
|
||||
systeminformation.cpuFlags(),
|
||||
systeminformation.cpuTemperature(),
|
||||
systeminformation.battery(),
|
||||
]);
|
||||
const cpu = `${Cpu.manufacturer} ${Cpu.brand}`;
|
||||
const memory = `${size(Memory.active)}/${size(Memory.total)}`;
|
||||
const osinfo = `${OsInfo.distro} ${OsInfo.release} ${OsInfo.codename} ${OsInfo.kernel} ${OsInfo.arch}`;
|
||||
const load = `${CurrentLoad.avgload}`;
|
||||
const flags = CpuFlags;
|
||||
let battery;
|
||||
if (!Battery.hasbattery) battery = 'No battery';
|
||||
else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`;
|
||||
const mid = OsInfo.serial;
|
||||
cache.cpu = cpu;
|
||||
cache.osinfo = osinfo;
|
||||
cache.flags = flags;
|
||||
cache.mid = mid;
|
||||
return {
|
||||
mid, cpu, memory, osinfo, load, flags, CpuTemp, battery,
|
||||
};
|
||||
}
|
||||
|
||||
export async function update(): Promise<[string, any, any]> {
|
||||
const [Memory, CurrentLoad, CpuTemp, Battery] = await Promise.all([
|
||||
systeminformation.mem(),
|
||||
systeminformation.currentLoad(),
|
||||
systeminformation.cpuTemperature(),
|
||||
systeminformation.battery(),
|
||||
]);
|
||||
const {
|
||||
mid, cpu, osinfo, flags,
|
||||
} = cache;
|
||||
const memory = `${size(Memory.active)}/${size(Memory.total)}`;
|
||||
const load = `${CurrentLoad.avgload}`;
|
||||
let battery;
|
||||
if (!Battery.hasbattery) battery = 'No battery';
|
||||
else battery = `${Battery.type} ${Battery.model} ${Battery.percent}%${Battery.ischarging ? ' Charging' : ''}`;
|
||||
return [
|
||||
mid,
|
||||
{
|
||||
memory, load, battery, CpuTemp,
|
||||
},
|
||||
{
|
||||
mid, cpu, memory, osinfo, load, flags, battery, CpuTemp,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
global.Hydro.lib.sysinfo = { get, update };
|
||||
@@ -0,0 +1,32 @@
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export default function convert(ini: string) {
|
||||
const f = ini.split('\n');
|
||||
const count = parseInt(f[0], 10);
|
||||
const res = { cases: [] };
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const [input, output, time, score, memory] = f[i].split('|');
|
||||
const cur = {
|
||||
input, output, score: parseInt(score, 10), time: `${time}s`, memory: '128m',
|
||||
};
|
||||
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
|
||||
res.cases.push(cur);
|
||||
}
|
||||
if (res.cases.length > 0) {
|
||||
const { time, score, memory } = res.cases[0];
|
||||
for (let i = 1; i < res.cases.length; i++) {
|
||||
if (res.cases[i].time !== time
|
||||
|| res.cases[i].score !== score
|
||||
|| res.cases[i].memory !== memory) {
|
||||
return yaml.safeDump(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
return yaml.safeDump({
|
||||
time: res.cases[0].time,
|
||||
score: res.cases[0].score,
|
||||
memory: res.cases[0].memory,
|
||||
});
|
||||
}
|
||||
|
||||
global.Hydro.lib['testdata.convert.ini'] = convert;
|
||||
@@ -0,0 +1,60 @@
|
||||
import Zip from 'adm-zip';
|
||||
import { safeLoad } from 'js-yaml';
|
||||
import { LocalProblemConfig } from '../interface';
|
||||
|
||||
export async function readConfig(filePath: string) {
|
||||
const data = new Zip(filePath);
|
||||
const entries = data.getEntries();
|
||||
for (const entry of entries) {
|
||||
if (entry.name.toLowerCase() === 'config.yaml') {
|
||||
return entry.getData().toString();
|
||||
}
|
||||
if (entry.name.toLowerCase() === 'config.ini') {
|
||||
const ini = entry.getData().toString();
|
||||
const conv = global.Hydro.lib['testdata.convert.ini'];
|
||||
return conv(ini);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
interface ParseResult {
|
||||
count: number,
|
||||
memoryMax: number,
|
||||
memoryMin: number,
|
||||
timeMax: number,
|
||||
timeMin: number,
|
||||
}
|
||||
|
||||
export async function parseConfig(config: string | LocalProblemConfig) {
|
||||
const result: ParseResult = {
|
||||
count: 0,
|
||||
memoryMin: Number.MAX_SAFE_INTEGER,
|
||||
memoryMax: 0,
|
||||
timeMin: Number.MAX_SAFE_INTEGER,
|
||||
timeMax: 0,
|
||||
};
|
||||
let cfg: LocalProblemConfig = {};
|
||||
if (typeof config === 'string') {
|
||||
// TODO should validate here?
|
||||
cfg = safeLoad(config) as LocalProblemConfig;
|
||||
} else cfg = config;
|
||||
if (cfg.cases) {
|
||||
for (const c of cfg.cases) {
|
||||
result.memoryMax = Math.max(result.memoryMax, c.memory);
|
||||
result.memoryMin = Math.min(result.memoryMin, c.memory);
|
||||
result.timeMax = Math.max(result.timeMax, c.time);
|
||||
result.timeMin = Math.min(result.timeMin, c.time);
|
||||
result.count++;
|
||||
}
|
||||
} else if (cfg.subtasks) {
|
||||
for (const subtask of cfg.subtasks) {
|
||||
result.memoryMax = Math.max(result.memoryMax, subtask.memory);
|
||||
result.memoryMin = Math.min(result.memoryMin, subtask.memory);
|
||||
result.timeMax = Math.max(result.timeMax, subtask.time);
|
||||
result.timeMin = Math.min(result.timeMin, subtask.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global.Hydro.lib.testdataConfig = { readConfig, parseConfig };
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as browser from 'detect-browser';
|
||||
|
||||
export function parse(str: string) {
|
||||
return browser.parseUserAgent(str);
|
||||
}
|
||||
|
||||
export function icon(str: string) {
|
||||
return str.split(' ')[0].toLowerCase();
|
||||
}
|
||||
|
||||
global.Hydro.lib.useragent = { parse, icon };
|
||||
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable no-empty */
|
||||
import { ValidationError } from '../error';
|
||||
|
||||
const RE_UID = /^-?\d+$/i;
|
||||
const RE_PID = /^([a-zA-Z]+[a-zA-Z0-9]*)|$/i;
|
||||
const RE_UNAME = /[^\s\u3000](.{,254}[^\s\u3000])?$/i;
|
||||
const RE_ROLE = /^[_0-9A-Za-z]{1,256}$/i;
|
||||
const RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i;
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
function _validate(scheme: any, arg: any, base: string): void {
|
||||
if (!(scheme instanceof Object)) return _validate({ $type: scheme }, arg, base);
|
||||
if (scheme.$validate) {
|
||||
const result = scheme.$validate(arg);
|
||||
if (result === false) throw new ValidationError(base);
|
||||
}
|
||||
if (scheme.$group) _validate(scheme.$group, arg, base);
|
||||
if (scheme.$or) {
|
||||
if (scheme.$or instanceof Array) {
|
||||
for (const s of scheme.$or) {
|
||||
let success = false;
|
||||
try {
|
||||
_validate(s, arg, base);
|
||||
success = true;
|
||||
} catch { }
|
||||
// TODO beautify output
|
||||
if (!success) throw new ValidationError(scheme.$or);
|
||||
}
|
||||
} else {
|
||||
for (const skey in scheme.$or) {
|
||||
let success = false;
|
||||
try {
|
||||
_validate(scheme.$or[skey], arg[skey], `${base}.${skey}`);
|
||||
success = true;
|
||||
} catch { }
|
||||
// TODO beautify output
|
||||
if (!success) throw new ValidationError(base, scheme.$or);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scheme.$and) for (const s of scheme.$and) _validate(s, arg, base);
|
||||
if (scheme.$type) {
|
||||
if (typeof scheme.$type === 'string') {
|
||||
if (!scheme.$type.endsWith('?')) {
|
||||
if (!typeof arg === scheme.$type) {
|
||||
throw new ValidationError(base);
|
||||
}
|
||||
} else if (arg && !scheme.$type.includes(typeof arg)) {
|
||||
throw new ValidationError(base);
|
||||
}
|
||||
} else if (!(arg instanceof scheme.$type)) {
|
||||
throw new ValidationError(base);
|
||||
}
|
||||
}
|
||||
if (scheme.$eq && scheme.$eq !== arg) throw new ValidationError(base);
|
||||
if (scheme.$not && scheme.$not === arg) throw new ValidationError(base);
|
||||
if (scheme.$in && !(scheme.$in.includes(arg))) throw new ValidationError(base);
|
||||
if (scheme.$nin && scheme.$nin.includes(arg)) throw new ValidationError(base);
|
||||
for (const key in scheme) {
|
||||
if (key[0] !== '$') {
|
||||
if (arg instanceof Object) {
|
||||
_validate(scheme[key], arg[key], `${base}.${key}`);
|
||||
} else {
|
||||
throw new ValidationError(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validate(scheme: any, arg: any) {
|
||||
return _validate(scheme, arg, '');
|
||||
}
|
||||
|
||||
export const isTitle = (s) => s && s.length < 64;
|
||||
export const checkTitle = (s) => { if (!(s && s.length < 64)) throw new ValidationError('title'); else return s; };
|
||||
export const isUid = (s) => RE_UID.test(s);
|
||||
export const checkUid = (s) => { if (!isUid(s)) throw new ValidationError('uid'); else return s; };
|
||||
export const isUname = (s) => RE_UNAME.test(s);
|
||||
export const checkUname = (s) => { if (!isUname(s)) throw new ValidationError('uname'); else return s; };
|
||||
export const isRole = (s) => RE_ROLE.test(s);
|
||||
export const checkRole = (s) => { if (!isRole(s)) throw new ValidationError('role'); else return s; };
|
||||
export const isPassword = (s) => s.length >= 5;
|
||||
export const checkPassword = (s) => { if (!(s && s.length >= 5)) throw new ValidationError('password'); else return s; };
|
||||
export const isEmail = (s) => RE_MAIL.test(s);
|
||||
export const checkEmail = (s) => { if (!RE_MAIL.test(s)) throw new ValidationError('mail'); else return s; };
|
||||
export const isContent = (s: any) => s && s.length < 65536;
|
||||
export const checkContent = (s) => { if (!(s && s.length < 65536)) throw new ValidationError('content'); else return s; };
|
||||
export const isName = (s) => s && s.length < 256;
|
||||
export const checkName = (s) => { if (!isName(s)) throw new ValidationError('name'); else return s; };
|
||||
export const isPid = (s) => RE_PID.test(s.toString());
|
||||
export const checkPid = (s) => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; };
|
||||
export const isIntro = () => true;
|
||||
export const checkIntro = (s) => { if (!isIntro()) throw new ValidationError('intro'); else return s; };
|
||||
export const isDescription = (s: any) => s && s.length < 65536;
|
||||
export const checkDescription = (s) => { if (!(s && s.length < 65536)) throw new ValidationError('description'); else return s; };
|
||||
|
||||
global.Hydro.lib.validator = {
|
||||
validate,
|
||||
isTitle,
|
||||
checkTitle,
|
||||
isUid,
|
||||
checkUid,
|
||||
isUname,
|
||||
checkUname,
|
||||
isRole,
|
||||
checkRole,
|
||||
isPassword,
|
||||
checkPassword,
|
||||
isEmail,
|
||||
checkEmail,
|
||||
isContent,
|
||||
checkContent,
|
||||
isName,
|
||||
checkName,
|
||||
isPid,
|
||||
checkPid,
|
||||
isIntro,
|
||||
checkIntro,
|
||||
isDescription,
|
||||
checkDescription,
|
||||
};
|
||||
/*
|
||||
ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?')
|
||||
|
||||
def is_id(s):
|
||||
return bool(ID_RE.fullmatch(s))
|
||||
|
||||
def check_category_name(s):
|
||||
if not is_id(s):
|
||||
raise error.ValidationError('category_name')
|
||||
|
||||
def check_node_name(s):
|
||||
if not is_id(s):
|
||||
raise error.ValidationError('node_name')
|
||||
|
||||
def is_intro(s):
|
||||
return isinstance(s, str) and 0 < len(s.strip()) < 500
|
||||
|
||||
def check_intro(s):
|
||||
if not is_intro(s):
|
||||
raise error.ValidationError('intro')
|
||||
|
||||
def is_description(s):
|
||||
return isinstance(s, str) and len(s) < 65536
|
||||
|
||||
def check_description(s):
|
||||
if not is_description(s):
|
||||
raise error.ValidationError('description')
|
||||
|
||||
def is_lang(i):
|
||||
return i in export const ant.language.LANG_TEXTS
|
||||
|
||||
def check_lang(i):
|
||||
if not is_lang(i):
|
||||
raise error.ValidationError('lang')
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,241 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-eval */
|
||||
import './interface';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import cluster from 'cluster';
|
||||
import fs from 'fs-extra';
|
||||
import { argv } from 'yargs';
|
||||
import AdmZip from 'adm-zip';
|
||||
|
||||
export * from './interface';
|
||||
|
||||
if (!global.Hydro) {
|
||||
global.Hydro = {
|
||||
stat: { reqCount: 0 },
|
||||
handler: {},
|
||||
// @ts-ignore
|
||||
service: {},
|
||||
// @ts-ignore
|
||||
model: {},
|
||||
script: {},
|
||||
// @ts-ignore
|
||||
lib: {},
|
||||
ui: {
|
||||
manifest: {},
|
||||
nav: [],
|
||||
template: {},
|
||||
},
|
||||
// @ts-ignore
|
||||
error: {},
|
||||
locales: {},
|
||||
postInit: [],
|
||||
};
|
||||
global.onDestory = [];
|
||||
global.addons = [];
|
||||
}
|
||||
|
||||
if (argv.debug) {
|
||||
console.log(process.argv);
|
||||
console.log(argv);
|
||||
}
|
||||
|
||||
async function terminate() {
|
||||
try {
|
||||
for (const task of global.onDestory) await task();
|
||||
} catch (e) {
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function fork(args: string[] = []) {
|
||||
const _args = process.argv.slice(2);
|
||||
_args.push(...args, `--addons=${Buffer.from(JSON.stringify(global.addons)).toString('base64')}`);
|
||||
cluster.setupMaster({ args: _args });
|
||||
return cluster.fork();
|
||||
}
|
||||
|
||||
interface EntryConfig {
|
||||
entry: string,
|
||||
newProcess?: boolean,
|
||||
}
|
||||
|
||||
async function entry(config: EntryConfig) {
|
||||
if (config.entry) {
|
||||
if (config.newProcess) {
|
||||
const p = await fork([`--entry=${config.entry}`]);
|
||||
await new Promise((resolve, reject) => {
|
||||
p.on('exit', (code, signal) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(signal);
|
||||
});
|
||||
p.on('error', (err: Error) => {
|
||||
p.kill();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const loader = require(`./entry/${config.entry}`);
|
||||
return await loader.load(entry, global.addons);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export type Entry = typeof entry;
|
||||
|
||||
async function stopWorker() {
|
||||
cluster.disconnect();
|
||||
}
|
||||
|
||||
async function startWorker(cnt: number) {
|
||||
await fork(['--firstWorker']);
|
||||
for (let i = 1; i < cnt; i++) await fork();
|
||||
}
|
||||
|
||||
async function executeCommand(input: string) {
|
||||
try {
|
||||
const t = eval(input.toString().trim());
|
||||
if (t instanceof Promise) console.log(await t);
|
||||
else console.log(t);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function messageHandler(worker: cluster.Worker, msg: any) {
|
||||
if (!msg) msg = worker;
|
||||
if (msg.event) {
|
||||
if (msg.event === 'bus') {
|
||||
if (cluster.isMaster) {
|
||||
for (const i in cluster.workers) {
|
||||
cluster.workers[i].send(msg);
|
||||
}
|
||||
} else {
|
||||
global.Hydro.service.bus.publish(msg.eventName, msg.payload, false);
|
||||
}
|
||||
} else if (msg.event === 'stat') {
|
||||
global.Hydro.stat.reqCount += msg.count;
|
||||
} else if (msg.event === 'restart') {
|
||||
console.log('Restarting');
|
||||
await stopWorker();
|
||||
console.log('Worker stopped');
|
||||
await startWorker(msg.count);
|
||||
} else if (msg.event === 'run') {
|
||||
await executeCommand(msg.command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const moduleTemp = path.resolve(os.tmpdir(), 'hydro', 'module');
|
||||
const publicTemp = path.resolve(os.tmpdir(), 'hydro', 'public');
|
||||
const tmp = path.resolve(os.tmpdir(), 'hydro', '__');
|
||||
|
||||
export function addon(addonPath: string) {
|
||||
let modulePath = path.resolve(process.cwd(), addonPath);
|
||||
if (!(fs.existsSync(addonPath) && fs.statSync(addonPath).isFile())) {
|
||||
try {
|
||||
// Is a npm package
|
||||
const packagejson = require.resolve(`${addonPath}/package.json`);
|
||||
modulePath = path.dirname(packagejson);
|
||||
const publicPath = path.resolve(modulePath, 'public');
|
||||
if (fs.existsSync(publicPath)) fs.copySync(publicPath, publicTemp);
|
||||
global.addons.push(modulePath);
|
||||
} catch (e) {
|
||||
throw new Error(`Addon not found: ${addonPath}`);
|
||||
}
|
||||
} else if (modulePath.endsWith('.hydro')) {
|
||||
try {
|
||||
// Is *.hydro module
|
||||
const t = modulePath.split(path.sep);
|
||||
const name = t[t.length - 1].split('.')[0];
|
||||
const zip = new AdmZip(modulePath);
|
||||
const targetPath = path.resolve(moduleTemp, name);
|
||||
zip.extractAllTo(targetPath, true);
|
||||
const content = fs.readdirSync(targetPath);
|
||||
const ipath = path.join(targetPath, content[0]);
|
||||
if (content.length === 1 && fs.statSync(ipath).isDirectory()) {
|
||||
fs.moveSync(ipath, tmp);
|
||||
fs.rmdirSync(targetPath);
|
||||
fs.moveSync(tmp, targetPath);
|
||||
}
|
||||
const publicPath = path.resolve(targetPath, 'public');
|
||||
if (fs.existsSync(publicPath)) fs.copySync(publicPath, publicTemp);
|
||||
global.addons.push(targetPath);
|
||||
} catch (e) {
|
||||
console.error('Addon load fail: ', e);
|
||||
throw e;
|
||||
}
|
||||
} else throw new Error(`Addon not found: ${addonPath}`);
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (e) => console.error(e));
|
||||
process.on('SIGINT', terminate);
|
||||
|
||||
export async function load() {
|
||||
addon(path.resolve(__dirname, '..'));
|
||||
Error.stackTraceLimit = 50;
|
||||
process.on('message', messageHandler);
|
||||
cluster.on('message', messageHandler);
|
||||
if (cluster.isMaster || argv.startAsMaster) {
|
||||
console.log(`Master ${process.pid} Starting`);
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (buf) => {
|
||||
const input = buf.toString();
|
||||
if (input[0] === '@') {
|
||||
for (const i in cluster.workers) {
|
||||
cluster.workers[i].send({ event: 'run', command: input.substr(1, input.length - 1) });
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
executeCommand(input);
|
||||
}
|
||||
});
|
||||
const cnt = await entry({ entry: 'master' });
|
||||
console.log('Master started');
|
||||
cluster.on('exit', (worker, code, signal) => {
|
||||
console.log(`Worker ${worker.process.pid} ${worker.id} exit: ${code} ${signal}`);
|
||||
});
|
||||
cluster.on('disconnect', (worker) => {
|
||||
console.log(`Worker ${worker.process.pid} ${worker.id} disconnected`);
|
||||
});
|
||||
cluster.on('listening', (worker, address) => {
|
||||
console.log(`Worker ${worker.process.pid} ${worker.id} listening at `, address);
|
||||
});
|
||||
cluster.on('online', (worker) => {
|
||||
console.log(`Worker ${worker.process.pid} ${worker.id} is online`);
|
||||
});
|
||||
await startWorker(cnt);
|
||||
} else {
|
||||
global.addons = JSON.parse(Buffer.from(argv.addons as string, 'base64').toString());
|
||||
console.log(global.addons);
|
||||
if (argv.entry) {
|
||||
console.log(`Worker ${process.pid} Starting as ${argv.entry}`);
|
||||
await entry({ entry: argv.entry as string });
|
||||
console.log(`Worker ${process.pid} Started as ${argv.entry}`);
|
||||
} else {
|
||||
if (argv.firstWorker) cluster.isFirstWorker = true;
|
||||
else cluster.isFirstWorker = false;
|
||||
console.log(`Worker ${process.pid} Starting`);
|
||||
await entry({ entry: 'worker' });
|
||||
console.log(`Worker ${process.pid} Started`);
|
||||
}
|
||||
}
|
||||
if (global.gc) global.gc();
|
||||
}
|
||||
|
||||
export async function loadCli() {
|
||||
await entry({ entry: 'cli' });
|
||||
return terminate();
|
||||
}
|
||||
|
||||
if (argv.pandora || !module.parent) {
|
||||
const func = argv._[0] === 'cli' ? load : loadCli;
|
||||
func().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as db from '../service/db';
|
||||
import { Bdoc } from '../interface';
|
||||
|
||||
const coll = db.collection('blacklist');
|
||||
|
||||
export async function add(ip: string): Promise<Bdoc> {
|
||||
/**
|
||||
* Add a ip into blacklist.
|
||||
* @param {string} ip
|
||||
* @returns {Promise<Bdoc>}
|
||||
*/
|
||||
const expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000);
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ _id: ip },
|
||||
{ $set: { expireAt } },
|
||||
{ upsert: true, returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export function get(ip: string): Promise<Bdoc | null> {
|
||||
/**
|
||||
* Get a ip, return null if not.
|
||||
* @param {string} ip
|
||||
* @returns {Promise<Bdoc>}
|
||||
*/
|
||||
return coll.findOne({ _id: ip });
|
||||
}
|
||||
|
||||
export function del(ip: string) {
|
||||
return coll.deleteOne({ _id: ip });
|
||||
}
|
||||
|
||||
function ensureIndexes() {
|
||||
return coll.createIndex('expireAt', { expireAfterSeconds: 0 });
|
||||
}
|
||||
|
||||
global.Hydro.postInit.push(ensureIndexes);
|
||||
global.Hydro.model.blacklist = { add, get, del };
|
||||
@@ -0,0 +1,643 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { Udoc } from '../interface';
|
||||
|
||||
/* Why nocheck?
|
||||
* BitInt requires at least ES2020, but we can't use it as
|
||||
* NodeJS doesn't support some of this new features.
|
||||
* e.g.: object?.property
|
||||
*/
|
||||
export const PERM = {
|
||||
PERM_NONE: 0n,
|
||||
|
||||
// Domain Settings
|
||||
PERM_VIEW: 1n << 0n,
|
||||
PERM_EDIT_DOMAIN: 1n << 1n,
|
||||
PERM_MOD_BADGE: 1n << 2n,
|
||||
|
||||
// Problem and Record
|
||||
PERM_CREATE_PROBLEM: 1n << 4n,
|
||||
PERM_EDIT_PROBLEM: 1n << 5n,
|
||||
PERM_EDIT_PROBLEM_SELF: 1n << 6n,
|
||||
PERM_VIEW_PROBLEM: 1n << 7n,
|
||||
PERM_VIEW_PROBLEM_HIDDEN: 1n << 8n,
|
||||
PERM_SUBMIT_PROBLEM: 1n << 9n,
|
||||
PERM_READ_PROBLEM_DATA: 1n << 10n,
|
||||
PERM_READ_PROBLEM_DATA_SELF: 1n << 11n,
|
||||
PERM_READ_RECORD_CODE: 1n << 12n,
|
||||
PERM_REJUDGE_PROBLEM: 1n << 13n,
|
||||
PERM_REJUDGE: 1n << 14n,
|
||||
|
||||
// Problem Solution
|
||||
PERM_VIEW_PROBLEM_SOLUTION: 1n << 15n,
|
||||
PERM_CREATE_PROBLEM_SOLUTION: 1n << 16n,
|
||||
PERM_VOTE_PROBLEM_SOLUTION: 1n << 17n,
|
||||
PERM_EDIT_PROBLEM_SOLUTION: 1n << 18n,
|
||||
PERM_EDIT_PROBLEM_SOLUTION_SELF: 1n << 19n,
|
||||
PERM_DELETE_PROBLEM_SOLUTION: 1n << 20n,
|
||||
PERM_DELETE_PROBLEM_SOLUTION_SELF: 1n << 21n,
|
||||
PERM_REPLY_PROBLEM_SOLUTION: 1n << 22n,
|
||||
PERM_EDIT_PROBLEM_SOLUTION_REPLY: 1n << 23n,
|
||||
PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF: 1n << 24n,
|
||||
PERM_DELETE_PROBLEM_SOLUTION_REPLY: 1n << 25n,
|
||||
PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF: 1n << 26n,
|
||||
|
||||
// Discussion
|
||||
PERM_VIEW_DISCUSSION: 1n << 27n,
|
||||
PERM_CREATE_DISCUSSION: 1n << 28n,
|
||||
PERM_HIGHLIGHT_DISCUSSION: 1n << 29n,
|
||||
PERM_PIN_DISCUSSION: 1n << 59n,
|
||||
PERM_EDIT_DISCUSSION: 1n << 30n,
|
||||
PERM_EDIT_DISCUSSION_SELF: 1n << 31n,
|
||||
PERM_DELETE_DISCUSSION: 1n << 32n,
|
||||
PERM_DELETE_DISCUSSION_SELF: 1n << 33n,
|
||||
PERM_REPLY_DISCUSSION: 1n << 34n,
|
||||
PERM_EDIT_DISCUSSION_REPLY: 1n << 35n,
|
||||
PERM_EDIT_DISCUSSION_REPLY_SELF: 1n << 36n,
|
||||
PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 37n,
|
||||
PERM_DELETE_DISCUSSION_REPLY: 1n << 38n,
|
||||
PERM_DELETE_DISCUSSION_REPLY_SELF: 1n << 39n,
|
||||
PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 40n,
|
||||
|
||||
// Contest
|
||||
PERM_VIEW_CONTEST: 1n << 41n,
|
||||
PERM_VIEW_CONTEST_SCOREBOARD: 1n << 42n,
|
||||
PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD: 1n << 43n,
|
||||
PERM_CREATE_CONTEST: 1n << 44n,
|
||||
PERM_ATTEND_CONTEST: 1n << 45n,
|
||||
PERM_EDIT_CONTEST: 1n << 50n,
|
||||
PERM_EDIT_CONTEST_SELF: 1n << 51n,
|
||||
|
||||
// Homework
|
||||
PERM_VIEW_HOMEWORK: 1n << 52n,
|
||||
PERM_VIEW_HOMEWORK_SCOREBOARD: 1n << 53n,
|
||||
PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD: 1n << 54n,
|
||||
PERM_CREATE_HOMEWORK: 1n << 55n,
|
||||
PERM_ATTEND_HOMEWORK: 1n << 56n,
|
||||
PERM_EDIT_HOMEWORK: 1n << 57n,
|
||||
PERM_EDIT_HOMEWORK_SELF: 1n << 58n,
|
||||
|
||||
// Training
|
||||
PERM_VIEW_TRAINING: 1n << 46n,
|
||||
PERM_CREATE_TRAINING: 1n << 47n,
|
||||
PERM_EDIT_TRAINING: 1n << 48n,
|
||||
PERM_EDIT_TRAINING_SELF: 1n << 49n,
|
||||
|
||||
// Ranking
|
||||
PERM_VIEW_RANKING: 1n << 50n,
|
||||
|
||||
// Placeholder
|
||||
PERM_ALL: -1n,
|
||||
PERM_BASIC: 0n,
|
||||
PERM_DEFAULT: 0n,
|
||||
PERM_ADMIN: -1n,
|
||||
};
|
||||
|
||||
export const Permission = (family: string, key: BigInt, desc: string) => ({ family, key, desc });
|
||||
|
||||
export const PERMS = [
|
||||
Permission('perm_general', PERM.PERM_VIEW, 'View this domain'),
|
||||
Permission('perm_general', PERM.PERM_EDIT_DOMAIN, 'Edit domain settings'),
|
||||
Permission('perm_general', PERM.PERM_MOD_BADGE, 'Show MOD badge'),
|
||||
Permission('perm_problem', PERM.PERM_CREATE_PROBLEM, 'Create problems'),
|
||||
Permission('perm_problem', PERM.PERM_EDIT_PROBLEM, 'Edit problems'),
|
||||
Permission('perm_problem', PERM.PERM_EDIT_PROBLEM_SELF, 'Edit own problems'),
|
||||
Permission('perm_problem', PERM.PERM_VIEW_PROBLEM, 'View problems'),
|
||||
Permission('perm_problem', PERM.PERM_VIEW_PROBLEM_HIDDEN, 'View hidden problems'),
|
||||
Permission('perm_problem', PERM.PERM_SUBMIT_PROBLEM, 'Submit problem'),
|
||||
Permission('perm_problem', PERM.PERM_READ_PROBLEM_DATA, 'Read data of problem'),
|
||||
Permission('perm_problem', PERM.PERM_READ_PROBLEM_DATA_SELF, 'Read data of own problems'),
|
||||
Permission('perm_record', PERM.PERM_READ_RECORD_CODE, 'Read record codes'),
|
||||
Permission('perm_record', PERM.PERM_REJUDGE_PROBLEM, 'Rejudge problems'),
|
||||
Permission('perm_record', PERM.PERM_REJUDGE, 'Rejudge records'),
|
||||
Permission('perm_problem_solution', PERM.PERM_VIEW_PROBLEM_SOLUTION, 'View problem solutions'),
|
||||
Permission('perm_problem_solution', PERM.PERM_CREATE_PROBLEM_SOLUTION, 'Create problem solutions'),
|
||||
Permission('perm_problem_solution', PERM.PERM_VOTE_PROBLEM_SOLUTION, 'Vote problem solutions'),
|
||||
Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION, 'Edit problem solutions'),
|
||||
Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF, 'Edit own problem solutions'),
|
||||
Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION, 'Delete problem solutions'),
|
||||
Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF, 'Delete own problem solutions'),
|
||||
Permission('perm_problem_solution', PERM.PERM_REPLY_PROBLEM_SOLUTION, 'Reply problem solutions'),
|
||||
Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY, 'Edit problem solution replies'),
|
||||
Permission('perm_problem_solution', PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF, 'Edit own problem solution replies'),
|
||||
Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY, 'Delete problem solution replies'),
|
||||
Permission('perm_problem_solution', PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF, 'Delete own problem solution replies'),
|
||||
Permission('perm_discussion', PERM.PERM_VIEW_DISCUSSION, 'View discussions'),
|
||||
Permission('perm_discussion', PERM.PERM_CREATE_DISCUSSION, 'Create discussions'),
|
||||
Permission('perm_discussion', PERM.PERM_HIGHLIGHT_DISCUSSION, 'Highlight discussions'),
|
||||
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION, 'Edit discussions'),
|
||||
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_SELF, 'Edit own discussions'),
|
||||
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION, 'Delete discussions'),
|
||||
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_SELF, 'Delete own discussions'),
|
||||
Permission('perm_discussion', PERM.PERM_REPLY_DISCUSSION, 'Reply discussions'),
|
||||
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_REPLY, 'Edit discussion replies'),
|
||||
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_REPLY_SELF, 'Edit own discussion replies'),
|
||||
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION, 'Edit discussion replies of own discussion'),
|
||||
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_REPLY, 'Delete discussion replies'),
|
||||
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_REPLY_SELF, 'Delete own discussion replies'),
|
||||
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION, 'Delete discussion replies of own discussion'),
|
||||
Permission('perm_contest', PERM.PERM_VIEW_CONTEST, 'View contests'),
|
||||
Permission('perm_contest', PERM.PERM_VIEW_CONTEST_SCOREBOARD, 'View contest scoreboard'),
|
||||
Permission('perm_contest', PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD, 'View hidden contest submission status and scoreboard'),
|
||||
Permission('perm_contest', PERM.PERM_CREATE_CONTEST, 'Create contests'),
|
||||
Permission('perm_contest', PERM.PERM_ATTEND_CONTEST, 'Attend contests'),
|
||||
Permission('perm_contest', PERM.PERM_EDIT_CONTEST, 'Edit any contests'),
|
||||
Permission('perm_contest', PERM.PERM_EDIT_CONTEST_SELF, 'Edit own contests'),
|
||||
Permission('perm_homework', PERM.PERM_VIEW_HOMEWORK, 'View homework'),
|
||||
Permission('perm_homework', PERM.PERM_VIEW_HOMEWORK_SCOREBOARD, 'View homework scoreboard'),
|
||||
Permission('perm_homework', PERM.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD, 'View hidden homework submission status and scoreboard'),
|
||||
Permission('perm_homework', PERM.PERM_CREATE_HOMEWORK, 'Create homework'),
|
||||
Permission('perm_homework', PERM.PERM_ATTEND_HOMEWORK, 'Claim homework'),
|
||||
Permission('perm_homework', PERM.PERM_EDIT_HOMEWORK, 'Edit any homework'),
|
||||
Permission('perm_homework', PERM.PERM_EDIT_HOMEWORK_SELF, 'Edit own homework'),
|
||||
Permission('perm_training', PERM.PERM_VIEW_TRAINING, 'View training plans'),
|
||||
Permission('perm_training', PERM.PERM_CREATE_TRAINING, 'Create training plans'),
|
||||
Permission('perm_training', PERM.PERM_EDIT_TRAINING, 'Edit training plans'),
|
||||
Permission('perm_training', PERM.PERM_EDIT_TRAINING_SELF, 'Edit own training plans'),
|
||||
Permission('perm_ranking', PERM.PERM_VIEW_RANKING, 'View ranking'),
|
||||
];
|
||||
|
||||
export const PERMS_BY_FAMILY = {};
|
||||
for (const p of PERMS) {
|
||||
if (!PERMS_BY_FAMILY[p.family]) PERMS_BY_FAMILY[p.family] = [p];
|
||||
else PERMS_BY_FAMILY[p.family].push(p);
|
||||
}
|
||||
|
||||
PERM.PERM_BASIC = PERM.PERM_VIEW
|
||||
| PERM.PERM_VIEW_PROBLEM
|
||||
| PERM.PERM_VIEW_PROBLEM_SOLUTION
|
||||
| PERM.PERM_VIEW_DISCUSSION
|
||||
| PERM.PERM_VIEW_CONTEST
|
||||
| PERM.PERM_VIEW_CONTEST_SCOREBOARD
|
||||
| PERM.PERM_VIEW_HOMEWORK
|
||||
| PERM.PERM_VIEW_HOMEWORK_SCOREBOARD
|
||||
| PERM.PERM_VIEW_TRAINING
|
||||
| PERM.PERM_VIEW_RANKING;
|
||||
|
||||
PERM.PERM_DEFAULT = PERM.PERM_VIEW
|
||||
| PERM.PERM_VIEW_PROBLEM
|
||||
| PERM.PERM_EDIT_PROBLEM_SELF
|
||||
| PERM.PERM_SUBMIT_PROBLEM
|
||||
| PERM.PERM_READ_PROBLEM_DATA_SELF
|
||||
| PERM.PERM_VIEW_PROBLEM_SOLUTION
|
||||
| PERM.PERM_CREATE_PROBLEM_SOLUTION
|
||||
| PERM.PERM_VOTE_PROBLEM_SOLUTION
|
||||
| PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF
|
||||
| PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF
|
||||
| PERM.PERM_REPLY_PROBLEM_SOLUTION
|
||||
| PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF
|
||||
| PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF
|
||||
| PERM.PERM_VIEW_DISCUSSION
|
||||
| PERM.PERM_CREATE_DISCUSSION
|
||||
| PERM.PERM_EDIT_DISCUSSION_SELF
|
||||
| PERM.PERM_REPLY_DISCUSSION
|
||||
| PERM.PERM_EDIT_DISCUSSION_REPLY_SELF
|
||||
// PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION
|
||||
| PERM.PERM_DELETE_DISCUSSION_REPLY_SELF
|
||||
| PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION
|
||||
| PERM.PERM_VIEW_CONTEST
|
||||
| PERM.PERM_VIEW_CONTEST_SCOREBOARD
|
||||
| PERM.PERM_ATTEND_CONTEST
|
||||
| PERM.PERM_EDIT_CONTEST_SELF
|
||||
| PERM.PERM_VIEW_HOMEWORK
|
||||
| PERM.PERM_VIEW_HOMEWORK_SCOREBOARD
|
||||
| PERM.PERM_ATTEND_HOMEWORK
|
||||
| PERM.PERM_EDIT_HOMEWORK_SELF
|
||||
| PERM.PERM_VIEW_TRAINING
|
||||
| PERM.PERM_CREATE_TRAINING
|
||||
| PERM.PERM_EDIT_TRAINING_SELF
|
||||
| PERM.PERM_VIEW_RANKING
|
||||
| PERM.PERM_SUBMIT_PROBLEM
|
||||
| PERM.PERM_CREATE_PROBLEM_SOLUTION
|
||||
| PERM.PERM_VOTE_PROBLEM_SOLUTION
|
||||
| PERM.PERM_REPLY_PROBLEM_SOLUTION
|
||||
| PERM.PERM_CREATE_DISCUSSION
|
||||
| PERM.PERM_REPLY_DISCUSSION
|
||||
| PERM.PERM_ATTEND_CONTEST
|
||||
| PERM.PERM_CREATE_TRAINING
|
||||
| PERM.PERM_ATTEND_HOMEWORK
|
||||
| PERM.PERM_VIEW_RANKING;
|
||||
|
||||
PERM.PERM_ADMIN = PERM.PERM_ALL;
|
||||
|
||||
export const PRIV = {
|
||||
PRIV_NONE: 0,
|
||||
PRIV_EDIT_SYSTEM: 1 << 0, // renamed from PRIV_SET_PRIV
|
||||
PRIV_SET_PERM: 1 << 1,
|
||||
PRIV_USER_PROFILE: 1 << 2,
|
||||
PRIV_REGISTER_USER: 1 << 3,
|
||||
PRIV_READ_PROBLEM_DATA: 1 << 4,
|
||||
PRIV_READ_PRETEST_DATA: 1 << 5, // deprecated
|
||||
PRIV_READ_PRETEST_DATA_SELF: 1 << 6, // deprecated
|
||||
PRIV_READ_RECORD_CODE: 1 << 7,
|
||||
PRIV_VIEW_HIDDEN_RECORD: 1 << 8,
|
||||
PRIV_JUDGE: 1 << 9, // (renamed)
|
||||
PRIV_CREATE_DOMAIN: 1 << 10,
|
||||
PRIV_VIEW_ALL_DOMAIN: 1 << 11,
|
||||
PRIV_MANAGE_ALL_DOMAIN: 1 << 12,
|
||||
PRIV_REJUDGE: 1 << 13,
|
||||
PRIV_VIEW_USER_SECRET: 1 << 14,
|
||||
PRIV_VIEW_JUDGE_STATISTICS: 1 << 15,
|
||||
PRIV_CREATE_FILE: 1 << 16,
|
||||
PRIV_UNLIMITED_QUOTA: 1 << 17,
|
||||
PRIV_DELETE_FILE: 1 << 18,
|
||||
PRIV_DELETE_FILE_SELF: 1 << 19,
|
||||
PRIV_ALL: -1,
|
||||
|
||||
PRIV_DEFAULT: 0,
|
||||
};
|
||||
|
||||
PRIV.PRIV_DEFAULT = PRIV.PRIV_USER_PROFILE
|
||||
+ PRIV.PRIV_REGISTER_USER
|
||||
+ PRIV.PRIV_READ_PRETEST_DATA_SELF
|
||||
+ PRIV.PRIV_CREATE_DOMAIN
|
||||
+ PRIV.PRIV_CREATE_FILE
|
||||
+ PRIV.PRIV_DELETE_FILE_SELF;
|
||||
|
||||
// [10, 1] means that people whose rank is less than 1% will get Level 10
|
||||
export const LEVELS = [
|
||||
[10, 1],
|
||||
[9, 2],
|
||||
[8, 10],
|
||||
[7, 20],
|
||||
[6, 30],
|
||||
[5, 40],
|
||||
[4, 70],
|
||||
[3, 90],
|
||||
[2, 95],
|
||||
[1, 100],
|
||||
];
|
||||
|
||||
export const BUILTIN_USERS: Udoc[] = [
|
||||
{
|
||||
_id: 0,
|
||||
mail: 'Guest@hydro.local',
|
||||
mailLower: 'guest@hydro.local',
|
||||
uname: 'Guest',
|
||||
unameLower: 'guest',
|
||||
salt: '',
|
||||
hash: '',
|
||||
hashType: 'hydro',
|
||||
regat: new Date(0),
|
||||
regip: '127.0.0.1',
|
||||
loginat: new Date(0),
|
||||
loginip: '127.0.0.1',
|
||||
priv: PRIV.PRIV_REGISTER_USER,
|
||||
},
|
||||
{
|
||||
_id: 1,
|
||||
mail: 'Hydro@hydro.local',
|
||||
mailLower: 'hydro@hydro.local',
|
||||
uname: 'Hydro',
|
||||
unameLower: 'hydro',
|
||||
salt: '',
|
||||
hash: '',
|
||||
hashType: 'hydro',
|
||||
regat: new Date(0),
|
||||
regip: '127.0.0.1',
|
||||
loginat: new Date(0),
|
||||
loginip: '127.0.0.1',
|
||||
priv: PRIV.PRIV_USER_PROFILE,
|
||||
},
|
||||
];
|
||||
|
||||
export const BUILTIN_ROLES = {
|
||||
guest: PERM.PERM_BASIC,
|
||||
default: PERM.PERM_DEFAULT,
|
||||
admin: PERM.PERM_ADMIN,
|
||||
};
|
||||
|
||||
export const DEFAULT_NODES = {
|
||||
探索: [
|
||||
{ pic: 'qa', name: '问答' },
|
||||
{ pic: 'share', name: '分享' },
|
||||
{ pic: 'solution', name: '题解' },
|
||||
],
|
||||
Vijos: [
|
||||
{ pic: 'hydro', name: 'Hydro' },
|
||||
{ pic: null, name: '团队' },
|
||||
{ pic: null, name: '月赛' },
|
||||
{ pic: 'advice', name: '建议' },
|
||||
],
|
||||
数据结构: [
|
||||
{ pic: null, name: '散列表' },
|
||||
{ pic: null, name: '搜索树' },
|
||||
{ pic: null, name: '栈和队列' },
|
||||
{ pic: null, name: '图' },
|
||||
{ pic: null, name: '堆' },
|
||||
],
|
||||
算法: [
|
||||
{ pic: null, name: '数论' },
|
||||
{ pic: null, name: '几何' },
|
||||
{ pic: null, name: '图论' },
|
||||
{ pic: null, name: '网络流' },
|
||||
{ pic: null, name: '动态规划' },
|
||||
{ pic: null, name: '背包' },
|
||||
{ pic: null, name: '排序' },
|
||||
{ pic: null, name: '搜索' },
|
||||
{ pic: null, name: '并查集' },
|
||||
{ pic: null, name: '贪心' },
|
||||
{ pic: null, name: '博弈论' },
|
||||
],
|
||||
在线题库: [
|
||||
{ pic: null, name: 'CodeForces' },
|
||||
{ pic: null, name: 'TopCoder' },
|
||||
{ pic: null, name: 'POJ' },
|
||||
{ pic: null, name: 'BZOJ' },
|
||||
{ pic: null, name: 'USACO' },
|
||||
{ pic: null, name: 'RQNOJ' },
|
||||
{ pic: null, name: 'UOJ' },
|
||||
{ pic: null, name: 'LOJ' },
|
||||
{ pic: null, name: '洛谷' },
|
||||
],
|
||||
泛: [
|
||||
{ pic: null, name: '数学' },
|
||||
{ pic: null, name: '编程' },
|
||||
{ pic: null, name: '数据库' },
|
||||
{ pic: null, name: 'C' },
|
||||
{ pic: null, name: 'C++' },
|
||||
{ pic: null, name: 'Pascal' },
|
||||
{ pic: null, name: 'Java' },
|
||||
{ pic: null, name: 'PHP' },
|
||||
{ pic: null, name: 'Python' },
|
||||
{ pic: null, name: '游戏' },
|
||||
{ pic: null, name: '保送' },
|
||||
{ pic: null, name: 'ACM' },
|
||||
],
|
||||
};
|
||||
|
||||
export const CATEGORIES = {
|
||||
动态规划: [
|
||||
'LCS',
|
||||
'LIS',
|
||||
'背包',
|
||||
'单调性DP',
|
||||
'环形DP',
|
||||
'树形DP',
|
||||
'状态压缩DP',
|
||||
],
|
||||
搜索: [
|
||||
'枚举',
|
||||
'搜索与剪枝',
|
||||
'启发式搜索',
|
||||
'DLX',
|
||||
'双向搜索',
|
||||
'折半搜索',
|
||||
'记忆化搜索',
|
||||
'模拟退火',
|
||||
],
|
||||
计算几何: [
|
||||
'半平面交',
|
||||
'凸包',
|
||||
'几何图形的交与并',
|
||||
'旋转卡壳',
|
||||
'点定位',
|
||||
'坐标变换',
|
||||
'离散化与扫描',
|
||||
'反演',
|
||||
'Voronoi图',
|
||||
'平面图的对偶图',
|
||||
'三角剖分',
|
||||
'梯形剖分',
|
||||
'几何知识',
|
||||
],
|
||||
贪心: [],
|
||||
树结构: [
|
||||
'最近公共祖先',
|
||||
'生成树',
|
||||
'DFS序列',
|
||||
'树上倍增',
|
||||
'树的分治',
|
||||
'树链剖分',
|
||||
'Link-Cut-Tree',
|
||||
],
|
||||
图结构: [
|
||||
'平面图',
|
||||
'二分图',
|
||||
'二分图匹配',
|
||||
'最短路',
|
||||
'差分约束',
|
||||
'拓扑排序',
|
||||
'网络流',
|
||||
'强连通分量',
|
||||
'割点割边',
|
||||
'欧拉回路',
|
||||
'2-SAT',
|
||||
],
|
||||
数论: [
|
||||
'素数判定',
|
||||
'欧几里得算法',
|
||||
'不定方程',
|
||||
'数位统计',
|
||||
'解线性同余方程',
|
||||
'baby-step-giant-step',
|
||||
'Pell方程',
|
||||
'大整数质因数分解',
|
||||
'勾股方程',
|
||||
'积性函数',
|
||||
'Fibonacci数列',
|
||||
],
|
||||
模拟: [],
|
||||
数据结构: [
|
||||
'栈',
|
||||
'队列',
|
||||
'链表',
|
||||
'单调队列',
|
||||
'并查集',
|
||||
'堆',
|
||||
'平衡树',
|
||||
'线段树',
|
||||
'树状数组',
|
||||
'树套树',
|
||||
'四分树',
|
||||
'划分树',
|
||||
'归并树',
|
||||
'k-d树',
|
||||
'块状链表',
|
||||
'Hashing',
|
||||
'函数式编程',
|
||||
],
|
||||
博弈论: [],
|
||||
字符串: [
|
||||
'KMP',
|
||||
'后缀数据结构',
|
||||
'Trie树',
|
||||
'AC自动机',
|
||||
'Manacher',
|
||||
'表达式处理',
|
||||
'最小表示法',
|
||||
],
|
||||
组合数学: [
|
||||
'生成函数',
|
||||
'容斥原理',
|
||||
'康托展开',
|
||||
'Catalan数列',
|
||||
'Stirling数',
|
||||
'差分',
|
||||
'Polya定理',
|
||||
],
|
||||
线性代数: [
|
||||
'矩阵乘法',
|
||||
'高斯消元',
|
||||
'线性规划',
|
||||
],
|
||||
高精度: [
|
||||
'FFT',
|
||||
],
|
||||
递推: [],
|
||||
概率论: [
|
||||
'随机化',
|
||||
],
|
||||
NPC: [],
|
||||
其他: [
|
||||
'二分查找',
|
||||
'三分查找',
|
||||
'双指针扫描',
|
||||
'分治',
|
||||
'分块',
|
||||
'RMQ',
|
||||
'快速幂',
|
||||
'数学',
|
||||
'排序',
|
||||
'构造',
|
||||
],
|
||||
};
|
||||
|
||||
export const LANG_TEXTS = {
|
||||
c: 'C',
|
||||
cc: 'C++',
|
||||
cs: 'C#',
|
||||
pas: 'Pascal',
|
||||
java: 'Java',
|
||||
py: 'Python',
|
||||
py3: 'Python 3',
|
||||
php: 'PHP',
|
||||
rs: 'Rust',
|
||||
hs: 'Haskell',
|
||||
js: 'JavaScript',
|
||||
go: 'Go',
|
||||
rb: 'Ruby',
|
||||
};
|
||||
|
||||
export const LANG_HIGHLIGHT_ID = {
|
||||
c: 'c',
|
||||
cc: 'cpp',
|
||||
cs: 'csharp',
|
||||
pas: 'pascal',
|
||||
java: 'java',
|
||||
py: 'python',
|
||||
py3: 'python',
|
||||
php: 'php',
|
||||
rs: 'rust',
|
||||
hs: 'haskell',
|
||||
js: 'javascript',
|
||||
go: 'go',
|
||||
rb: 'ruby',
|
||||
};
|
||||
|
||||
export const STATUS = {
|
||||
STATUS_WAITING: 0,
|
||||
STATUS_ACCEPTED: 1,
|
||||
STATUS_WRONG_ANSWER: 2,
|
||||
STATUS_TIME_LIMIT_EXCEEDED: 3,
|
||||
STATUS_MEMORY_LIMIT_EXCEEDED: 4,
|
||||
STATUS_OUTPUT_LIMIT_EXCEEDED: 5,
|
||||
STATUS_RUNTIME_ERROR: 6,
|
||||
STATUS_COMPILE_ERROR: 7,
|
||||
STATUS_SYSTEM_ERROR: 8,
|
||||
STATUS_CANCELED: 9,
|
||||
STATUS_ETC: 10,
|
||||
STATUS_JUDGING: 20,
|
||||
STATUS_COMPILING: 21,
|
||||
STATUS_FETCHED: 22,
|
||||
STATUS_IGNORED: 30,
|
||||
};
|
||||
|
||||
export const STATUS_TEXTS = {
|
||||
[STATUS.STATUS_WAITING]: 'Waiting',
|
||||
[STATUS.STATUS_ACCEPTED]: 'Accepted',
|
||||
[STATUS.STATUS_WRONG_ANSWER]: 'Wrong Answer',
|
||||
[STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'Time Exceeded',
|
||||
[STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'Memory Exceeded',
|
||||
[STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED]: 'Output Exceeded',
|
||||
[STATUS.STATUS_RUNTIME_ERROR]: 'Runtime Error',
|
||||
[STATUS.STATUS_COMPILE_ERROR]: 'Compile Error',
|
||||
[STATUS.STATUS_SYSTEM_ERROR]: 'System Error',
|
||||
[STATUS.STATUS_CANCELED]: 'Cancelled',
|
||||
[STATUS.STATUS_ETC]: 'Unknown Error',
|
||||
[STATUS.STATUS_JUDGING]: 'Running',
|
||||
[STATUS.STATUS_COMPILING]: 'Compiling',
|
||||
[STATUS.STATUS_FETCHED]: 'Fetched',
|
||||
[STATUS.STATUS_IGNORED]: 'Ignored',
|
||||
};
|
||||
|
||||
export const STATUS_CODES = {
|
||||
0: 'pending',
|
||||
1: 'pass',
|
||||
2: 'fail',
|
||||
3: 'fail',
|
||||
4: 'fail',
|
||||
5: 'fail',
|
||||
6: 'fail',
|
||||
7: 'fail',
|
||||
8: 'fail',
|
||||
9: 'ignored',
|
||||
10: 'fail',
|
||||
20: 'progress',
|
||||
21: 'progress',
|
||||
22: 'progress',
|
||||
30: 'ignored',
|
||||
};
|
||||
|
||||
export const USER_GENDER_MALE = 0;
|
||||
export const USER_GENDER_FEMALE = 1;
|
||||
export const USER_GENDER_OTHER = 2;
|
||||
export const USER_GENDERS = [USER_GENDER_MALE, USER_GENDER_FEMALE, USER_GENDER_OTHER];
|
||||
export const USER_GENDER_RANGE = {
|
||||
[USER_GENDER_MALE]: 'Boy ♂',
|
||||
[USER_GENDER_FEMALE]: 'Girl ♀',
|
||||
[USER_GENDER_OTHER]: 'Other',
|
||||
};
|
||||
export const USER_GENDER_ICONS = {
|
||||
[USER_GENDER_MALE]: '♂',
|
||||
[USER_GENDER_FEMALE]: '♀',
|
||||
[USER_GENDER_OTHER]: '?',
|
||||
};
|
||||
|
||||
export const CONSTANT = {
|
||||
PROBLEM_PER_PAGE: 100,
|
||||
CONTEST_PER_PAGE: 20,
|
||||
DISCUSSION_PER_PAGE: 50,
|
||||
RECORD_PER_PAGE: 100,
|
||||
SOLUTION_PER_PAGE: 20,
|
||||
TRAINING_PER_PAGE: 10,
|
||||
REPLY_PER_PAGE: 50,
|
||||
HOMEWORK_ON_MAIN: 5,
|
||||
DISCUSSION_ON_MAIN: 20,
|
||||
CONTEST_ON_MAIN: 10,
|
||||
TRAINING_ON_MAIN: 10,
|
||||
};
|
||||
|
||||
global.Hydro.model.builtin = {
|
||||
Permission,
|
||||
PERM,
|
||||
PERMS,
|
||||
PERMS_BY_FAMILY,
|
||||
PRIV,
|
||||
LEVELS,
|
||||
BUILTIN_USERS,
|
||||
BUILTIN_ROLES,
|
||||
DEFAULT_NODES,
|
||||
CATEGORIES,
|
||||
LANG_TEXTS,
|
||||
LANG_HIGHLIGHT_ID,
|
||||
STATUS,
|
||||
STATUS_TEXTS,
|
||||
STATUS_CODES,
|
||||
USER_GENDER_MALE,
|
||||
USER_GENDER_FEMALE,
|
||||
USER_GENDER_OTHER,
|
||||
USER_GENDERS,
|
||||
USER_GENDER_RANGE,
|
||||
USER_GENDER_ICONS,
|
||||
};
|
||||
@@ -0,0 +1,604 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import * as user from './user';
|
||||
import * as problem from './problem';
|
||||
import * as document from './document';
|
||||
import { PERM } from './builtin';
|
||||
import {
|
||||
ValidationError, ContestNotFoundError, ContestAlreadyAttendedError,
|
||||
ContestNotAttendedError, ContestScoreboardHiddenError,
|
||||
} from '../error';
|
||||
import {
|
||||
ContestRule, ContestRules, ScoreboardNode, Tdoc, Udict,
|
||||
} from '../interface';
|
||||
import * as misc from '../lib/misc';
|
||||
import ranked from '../lib/rank';
|
||||
|
||||
const acm: ContestRule = {
|
||||
TEXT: 'ACM/ICPC',
|
||||
check: () => { },
|
||||
statusSort: { accept: -1, time: 1 },
|
||||
showScoreboard: () => true,
|
||||
showRecord: (tdoc, now) => now > tdoc.endAt,
|
||||
stat: (tdoc, journal) => {
|
||||
const naccept = {};
|
||||
const effective = {};
|
||||
const detail = [];
|
||||
let accept = 0;
|
||||
let time = 0;
|
||||
for (const j of journal) {
|
||||
if (tdoc.pids.includes(j.pid)
|
||||
&& !(effective[j.pid] && effective[j.pid].accept)) {
|
||||
effective[j.pid] = j;
|
||||
if (!j.accept) naccept[j.pid] = (naccept[j.pid] || 0) + 1;
|
||||
}
|
||||
}
|
||||
function _time(jdoc) {
|
||||
const real = jdoc.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000);
|
||||
const penalty = 20 * 60 * naccept[jdoc.pid];
|
||||
return real + penalty;
|
||||
}
|
||||
for (const key in effective) {
|
||||
const j = effective[key];
|
||||
detail.push({
|
||||
...j, naccept: naccept[j.pid], time: _time(j),
|
||||
});
|
||||
}
|
||||
for (const d of detail) {
|
||||
accept += d.accept;
|
||||
if (d.accept) time += d.time;
|
||||
}
|
||||
return { accept, time, detail };
|
||||
},
|
||||
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
|
||||
const columns: ScoreboardNode[] = [
|
||||
{ type: 'rank', value: _('Rank') },
|
||||
{ type: 'user', value: _('User') },
|
||||
{ type: 'solved_problems', value: _('Solved Problems') },
|
||||
];
|
||||
if (isExport) {
|
||||
columns.push(
|
||||
{ type: 'total_time', value: _('Total Time (Seconds)') },
|
||||
{ type: 'total_time_str', value: _('Total Time') },
|
||||
);
|
||||
}
|
||||
for (let i = 1; i <= tdoc.pids.length; i++) {
|
||||
const pid = tdoc.pids[i - 1];
|
||||
if (isExport) {
|
||||
columns.push(
|
||||
{
|
||||
type: 'problem_flag',
|
||||
value: '#{0} {1}'.format(i, pdict[pid].title),
|
||||
},
|
||||
{
|
||||
type: 'problem_time',
|
||||
value: '#{0} {1}'.format(i, _('Time (Seconds)')),
|
||||
},
|
||||
{
|
||||
type: 'problem_time_str',
|
||||
value: '#{0} {1}'.format(i, _('Time')),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
columns.push({
|
||||
type: 'problem_detail',
|
||||
value: '#{0}'.format(i),
|
||||
raw: pid,
|
||||
});
|
||||
}
|
||||
}
|
||||
const rows = [columns];
|
||||
for (const [rank, tsdoc] of rankedTsdocs) {
|
||||
const tsddict = {};
|
||||
if (tsdoc.detail) {
|
||||
for (const item of tsdoc.detail) tsddict[item.pid] = item;
|
||||
}
|
||||
const row = [];
|
||||
row.push(
|
||||
{ type: 'string', value: rank },
|
||||
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
|
||||
{ type: 'string', value: tsdoc.accept || 0 },
|
||||
);
|
||||
if (isExport) {
|
||||
row.push(
|
||||
{ type: 'string', value: tsdoc.time || 0.0 },
|
||||
{ type: 'string', value: tsdoc.time || 0.0 },
|
||||
);
|
||||
}
|
||||
for (const pid of tdoc.pids) {
|
||||
let rid;
|
||||
let colAccepted;
|
||||
let colTime;
|
||||
let colTimeStr;
|
||||
if (tsddict[pid]?.accept) {
|
||||
rid = tsddict[pid].rid;
|
||||
colAccepted = _('Accepted');
|
||||
colTime = tsddict[pid].time;
|
||||
colTimeStr = misc.formatSeconds(colTime);
|
||||
} else {
|
||||
rid = null;
|
||||
colAccepted = '-';
|
||||
colTime = '-';
|
||||
colTimeStr = '-';
|
||||
}
|
||||
if (isExport) {
|
||||
row.push({ type: 'string', value: colAccepted });
|
||||
row.push({ type: 'string', value: colTime });
|
||||
row.push({ type: 'string', value: colTimeStr });
|
||||
} else {
|
||||
row.push({
|
||||
type: 'record',
|
||||
value: '{0}\n{1}'.format(colAccepted, colTimeStr),
|
||||
raw: rid,
|
||||
});
|
||||
}
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
|
||||
};
|
||||
|
||||
const oi: ContestRule = {
|
||||
TEXT: 'OI',
|
||||
check: () => { },
|
||||
statusSort: { score: -1 },
|
||||
stat: (tdoc, journal) => {
|
||||
const detail = {};
|
||||
let score = 0;
|
||||
for (const j of journal) {
|
||||
if (tdoc.pids.includes(j.pid)) {
|
||||
detail[j.pid] = j;
|
||||
score += j.score;
|
||||
}
|
||||
}
|
||||
return { score, detail };
|
||||
},
|
||||
showScoreboard: (tdoc, now) => now > tdoc.endAt,
|
||||
showRecord: (tdoc, now) => now > tdoc.endAt,
|
||||
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
|
||||
const columns: ScoreboardNode[] = [
|
||||
{ type: 'rank', value: _('Rank') },
|
||||
{ type: 'user', value: _('User') },
|
||||
{ type: 'total_score', value: _('Total Score') },
|
||||
];
|
||||
for (let i = 1; i <= tdoc.pids.length; i++) {
|
||||
if (isExport) {
|
||||
columns.push({
|
||||
type: 'problem_score',
|
||||
value: '#{0} {1}'.format(i, pdict[tdoc.pids[i - 1]].title),
|
||||
});
|
||||
} else {
|
||||
columns.push({
|
||||
type: 'problem_detail',
|
||||
value: '#{0}'.format(i),
|
||||
raw: tdoc.pids[i - 1],
|
||||
});
|
||||
}
|
||||
}
|
||||
const rows = [columns];
|
||||
for (const [rank, tsdoc] of rankedTsdocs) {
|
||||
const tsddict = {};
|
||||
if (tsdoc.journal) {
|
||||
for (const item of tsdoc.journal) tsddict[item.pid] = item;
|
||||
}
|
||||
const row = [];
|
||||
row.push(
|
||||
{ type: 'string', value: rank },
|
||||
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
|
||||
{ type: 'string', value: tsdoc.score || 0 },
|
||||
);
|
||||
for (const pid of tdoc.pids) {
|
||||
row.push({
|
||||
type: 'record',
|
||||
value: tsddict[pid]?.score || '-',
|
||||
raw: tsddict[pid]?.rid || null,
|
||||
});
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
|
||||
};
|
||||
|
||||
const homework: ContestRule = {
|
||||
TEXT: 'Assignment',
|
||||
check: () => { },
|
||||
statusSort: { penaltyScore: -1, time: 1 },
|
||||
stat: (tdoc, journal) => {
|
||||
const effective = {};
|
||||
for (const j of journal) {
|
||||
if (tdoc.pids.includes(j.pid)
|
||||
&& !effective[j.pid]
|
||||
&& journal[j.pid].accept) {
|
||||
effective[j.pid] = j;
|
||||
}
|
||||
}
|
||||
function time(jdoc) {
|
||||
const real = jdoc.rid.generationTime - tdoc.beginAt.getTime() / 1000;
|
||||
return Math.floor(real);
|
||||
}
|
||||
|
||||
function penaltyScore(jdoc) {
|
||||
const { score } = jdoc;
|
||||
const exceedSeconds = Math.floor(
|
||||
jdoc.rid.generationTime - tdoc.penaltySince.getTime() / 1000,
|
||||
);
|
||||
if (exceedSeconds < 0) return score;
|
||||
let coefficient = 1;
|
||||
const keys = Object.keys(tdoc.penaltyRules)
|
||||
.map((k) => parseFloat(k)).sort((a, b) => a - b);
|
||||
for (const i of keys) {
|
||||
if (i * 3600 <= exceedSeconds) coefficient = tdoc.penaltyRules[i];
|
||||
else break;
|
||||
}
|
||||
return score * coefficient;
|
||||
}
|
||||
const detail = [];
|
||||
for (const j in effective) {
|
||||
detail.push({
|
||||
...effective[j],
|
||||
penaltyScore: penaltyScore(effective[j]),
|
||||
time: time(j),
|
||||
});
|
||||
}
|
||||
return {
|
||||
score: Math.sum(detail.map((d) => d.score)),
|
||||
penaltyScore: Math.sum(detail.map((d) => d.penaltySince)),
|
||||
time: Math.sum(detail.map((d) => d.time)),
|
||||
detail,
|
||||
};
|
||||
},
|
||||
showScoreboard: () => true,
|
||||
showRecord: () => true,
|
||||
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
|
||||
const columns: ScoreboardNode[] = [
|
||||
{ type: 'rank', value: _('Rank') },
|
||||
{ type: 'user', value: _('User') },
|
||||
{ type: 'total_score', value: _('Score') },
|
||||
];
|
||||
if (isExport) {
|
||||
columns.push(
|
||||
{ type: 'total_original_score', value: _('Original Score') },
|
||||
{ type: 'total_time', value: _('Total Time (Seconds)') },
|
||||
);
|
||||
}
|
||||
columns.push({ type: 'total_time_str', value: _('Total Time') });
|
||||
for (let i = 1; i <= tdoc.pids.length; i++) {
|
||||
const pid = tdoc.pids[i - 1];
|
||||
if (isExport) {
|
||||
columns.push(
|
||||
{
|
||||
type: 'problem_score',
|
||||
value: '#{0} {1}'.format(i, pdict[pid].title),
|
||||
},
|
||||
{
|
||||
type: 'problem_original_score',
|
||||
value: '#{0} {1}'.format(i, _('Original Score')),
|
||||
},
|
||||
{
|
||||
type: 'problem_time',
|
||||
value: '#{0} {1}'.format(i, _('Time (Seconds)')),
|
||||
},
|
||||
{
|
||||
type: 'problem_time_str',
|
||||
value: '#{0} {1}'.format(i, _('Time')),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
columns.push({
|
||||
type: 'problem_detail',
|
||||
value: '#{0}'.format(i),
|
||||
raw: pid,
|
||||
});
|
||||
}
|
||||
}
|
||||
const rows = [columns];
|
||||
for (const [rank, tsdoc] of rankedTsdocs) {
|
||||
const tsddict = {};
|
||||
for (const item of tsdoc.journal || []) {
|
||||
tsddict[item.pid] = item;
|
||||
}
|
||||
const row = [
|
||||
{ type: 'string', value: rank },
|
||||
{
|
||||
type: 'user',
|
||||
value: udict[tsdoc.uid].uname,
|
||||
raw: tsdoc.uid,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
value: tsdoc.penaltyScore || 0,
|
||||
},
|
||||
];
|
||||
if (isExport) {
|
||||
row.push({ type: 'string', value: tsdoc.score || 0 });
|
||||
row.push({ type: 'string', value: tsdoc.time || 0.0 });
|
||||
}
|
||||
row.push({ type: 'string', value: misc.formatSeconds(tsdoc.time || 0) });
|
||||
for (const pid of tdoc.pids) {
|
||||
const rid = tsddict[pid]?.rid;
|
||||
const colScore = tsddict[pid]?.penaltyScore || '-';
|
||||
const colOriginalScore = tsddict[pid]?.score || '-';
|
||||
const colTime = tsddict[pid]?.time || '-';
|
||||
const colTimeStr = colTime !== '-' ? misc.formatSeconds(colTime || 0) : '-';
|
||||
if (isExport) {
|
||||
row.push(
|
||||
{ type: 'string', value: colScore },
|
||||
{ type: 'string', value: colOriginalScore },
|
||||
{ type: 'string', value: colTime },
|
||||
{ type: 'string', value: colTimeStr },
|
||||
);
|
||||
} else {
|
||||
row.push({
|
||||
type: 'record',
|
||||
value: '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr),
|
||||
raw: rid,
|
||||
});
|
||||
}
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
|
||||
};
|
||||
|
||||
export const RULES: ContestRules = {
|
||||
acm, oi, homework,
|
||||
};
|
||||
|
||||
function _getStatusJournal(tsdoc) {
|
||||
return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime));
|
||||
}
|
||||
|
||||
export function add(
|
||||
domainId: string, title: string, content: string, owner: number,
|
||||
rule: string, beginAt = new Date(), endAt = new Date(), pids = [],
|
||||
rated = false, data = {}, type = document.TYPE_CONTEST,
|
||||
) {
|
||||
if (!this.RULES[rule]) throw new ValidationError('rule');
|
||||
if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt');
|
||||
Object.assign(data, {
|
||||
content, owner, title, rule, beginAt, endAt, pids, attend: 0,
|
||||
});
|
||||
this.RULES[rule].check(data);
|
||||
return document.add(domainId, content, owner, type, null, null, null, {
|
||||
...data, title, rule, beginAt, endAt, pids, attend: 0, rated,
|
||||
});
|
||||
}
|
||||
|
||||
export async function edit(
|
||||
domainId: string, tid: ObjectID,
|
||||
$set: any, type = document.TYPE_CONTEST,
|
||||
) {
|
||||
if ($set.rule) {
|
||||
if (!this.RULES[$set.rule]) throw new ValidationError('rule');
|
||||
}
|
||||
const tdoc = await document.get(domainId, type, tid);
|
||||
if (!tdoc) throw new ContestNotFoundError(tid);
|
||||
this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set));
|
||||
return await document.set(domainId, type, tid, $set);
|
||||
}
|
||||
|
||||
export async function get(
|
||||
domainId: string, tid: ObjectID, type = document.TYPE_CONTEST,
|
||||
) {
|
||||
let tdoc: Tdoc;
|
||||
if (type === -1) {
|
||||
tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
|
||||
if (!tdoc) tdoc = await document.get(domainId, document.TYPE_HOMEWORK, tid);
|
||||
} else tdoc = await document.get(domainId, type, tid);
|
||||
if (!tdoc) throw new ContestNotFoundError(tid);
|
||||
return tdoc;
|
||||
}
|
||||
|
||||
export async function updateStatus(
|
||||
domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: number,
|
||||
accept = false, score = 0, type = document.TYPE_CONTEST,
|
||||
) {
|
||||
const tdoc = await get(domainId, tid, type);
|
||||
const tsdoc = await document.revPushStatus(domainId, type, tid, uid, 'journal', {
|
||||
rid, pid, accept, score,
|
||||
});
|
||||
if (!tsdoc.attend) throw new ContestNotAttendedError(tid, uid);
|
||||
const journal = _getStatusJournal(tsdoc);
|
||||
const stats = RULES[tdoc.rule].stat(tdoc, journal);
|
||||
return await document.revSetStatus(domainId, type, tid, uid, tsdoc.rev, { journal, ...stats });
|
||||
}
|
||||
|
||||
export function getStatus(
|
||||
domainId: string, tid: ObjectID, uid: number,
|
||||
type = document.TYPE_CONTEST,
|
||||
) {
|
||||
return document.getStatus(domainId, type, tid, uid);
|
||||
}
|
||||
|
||||
export async function getListStatus(
|
||||
domainId: string, uid: number, tids: ObjectID[],
|
||||
type = document.TYPE_CONTEST,
|
||||
) {
|
||||
const r = {};
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
for (const tid of tids) r[tid.toHexString()] = await getStatus(domainId, tid, uid, type);
|
||||
return r;
|
||||
}
|
||||
|
||||
export async function attend(
|
||||
domainId: string, tid: ObjectID, uid: number,
|
||||
type = document.TYPE_CONTEST,
|
||||
) {
|
||||
try {
|
||||
await document.cappedIncStatus(domainId, type, tid, uid, 'attend', 1, 0, 1);
|
||||
} catch (e) {
|
||||
throw new ContestAlreadyAttendedError(tid, uid);
|
||||
}
|
||||
await document.inc(domainId, type, tid, 'attend', 1);
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getMultiStatus(domainId: string, query: any, docType = document.TYPE_CONTEST) {
|
||||
return document.getMultiStatus(domainId, docType, query);
|
||||
}
|
||||
|
||||
export function isNew(tdoc: Tdoc, days = 1) {
|
||||
const now = new Date().getTime();
|
||||
const readyAt = tdoc.beginAt.getTime();
|
||||
return (now < readyAt - days * 24 * 3600 * 1000);
|
||||
}
|
||||
|
||||
export function isUpcoming(tdoc: Tdoc, days = 1) {
|
||||
const now = new Date().getTime();
|
||||
const readyAt = tdoc.beginAt.getTime();
|
||||
return (now > readyAt - days * 24 * 3600 * 1000 && now < tdoc.beginAt.getTime());
|
||||
}
|
||||
|
||||
export function isNotStarted(tdoc: Tdoc) {
|
||||
return (new Date()) < tdoc.beginAt;
|
||||
}
|
||||
|
||||
export function isOngoing(tdoc: Tdoc) {
|
||||
const now = new Date();
|
||||
return (tdoc.beginAt <= now && now < tdoc.endAt);
|
||||
}
|
||||
|
||||
export function isDone(tdoc: Tdoc) {
|
||||
return tdoc.endAt <= new Date();
|
||||
}
|
||||
|
||||
export function isExtended(tdoc: Tdoc) {
|
||||
const now = new Date().getTime();
|
||||
return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime();
|
||||
}
|
||||
|
||||
export const ContestHandlerMixin = (c) => class extends c {
|
||||
canViewHiddenScoreboard() {
|
||||
return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||
}
|
||||
|
||||
canShowRecord(tdoc: Tdoc, allowPermOverride = true) {
|
||||
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
|
||||
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
canShowScoreboard(tdoc: Tdoc, allowPermOverride = true) {
|
||||
if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true;
|
||||
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async getScoreboard(
|
||||
domainId: string, tid: ObjectID,
|
||||
isExport = false, docType = document.TYPE_CONTEST,
|
||||
): Promise<[Tdoc, ScoreboardNode[][], Udict]> {
|
||||
const tdoc = await get(domainId, tid, docType);
|
||||
if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid);
|
||||
const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType).toArray();
|
||||
const uids = tsdocs.map((tsdoc) => tsdoc.uid);
|
||||
const [udict, pdict] = await Promise.all([
|
||||
user.getList(domainId, uids),
|
||||
problem.getList(domainId, tdoc.pids, true),
|
||||
]);
|
||||
const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs);
|
||||
const rows = RULES[tdoc.rule].scoreboard(
|
||||
isExport, this.translate.bind(this),
|
||||
tdoc, rankedTsdocs, udict, pdict,
|
||||
);
|
||||
return [tdoc, rows, udict];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async verifyProblems(domainId: string, pids: Array<number | string>) {
|
||||
const pdict = await problem.getList(domainId, pids, true);
|
||||
const _pids: Set<number> = new Set();
|
||||
for (const i in pdict) _pids.add(pdict[i].docId);
|
||||
return Array.from(_pids);
|
||||
}
|
||||
};
|
||||
|
||||
export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: any) {
|
||||
return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set);
|
||||
}
|
||||
|
||||
export function count(domainId: string, query: any, type = document.TYPE_CONTEST) {
|
||||
return document.count(domainId, type, query);
|
||||
}
|
||||
|
||||
export function getMulti(domainId: string, query = {}, type = document.TYPE_CONTEST) {
|
||||
return document.getMulti(domainId, type, query);
|
||||
}
|
||||
|
||||
export async function getAndListStatus(
|
||||
domainId: string, tid: ObjectID, docType = document.TYPE_CONTEST,
|
||||
): Promise<[Tdoc, any[]]> {
|
||||
// TODO(iceboy): projection, pagination.
|
||||
const tdoc = await get(domainId, tid, docType);
|
||||
const tsdocs = await document.getMultiStatus(domainId, docType, { docId: tid })
|
||||
.sort(RULES[tdoc.rule].statusSort).toArray();
|
||||
return [tdoc, tsdocs];
|
||||
}
|
||||
|
||||
export async function recalcStatus(domainId: string, tid: ObjectID, type = document.TYPE_CONTEST) {
|
||||
const [tdoc, tsdocs] = await Promise.all([
|
||||
document.get(domainId, type, tid),
|
||||
document.getMultiStatus(domainId, type, { docId: tid }).toArray(),
|
||||
]);
|
||||
const tasks = [];
|
||||
for (const tsdoc of tsdocs || []) {
|
||||
if (tsdoc.journal) {
|
||||
const journal = _getStatusJournal(tsdoc);
|
||||
const stats = RULES[tdoc.rule].stat(tdoc, journal);
|
||||
tasks.push(
|
||||
document.revSetStatus(
|
||||
domainId, type, tid,
|
||||
tsdoc.uid, tsdoc.rev, { journal, ...stats }, false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return await Promise.all(tasks);
|
||||
}
|
||||
|
||||
export const statusText = (tdoc: Tdoc) => (
|
||||
isNew(tdoc)
|
||||
? 'New'
|
||||
: isUpcoming(tdoc)
|
||||
? 'Ready (☆▽☆)'
|
||||
: isOngoing(tdoc)
|
||||
? 'Live...'
|
||||
: 'Done');
|
||||
|
||||
export const getStatusText = (tdoc: Tdoc) => (
|
||||
isNotStarted(tdoc)
|
||||
? 'not_started'
|
||||
: isOngoing(tdoc)
|
||||
? 'ongoing'
|
||||
: 'finished');
|
||||
|
||||
global.Hydro.model.contest = {
|
||||
RULES,
|
||||
ContestHandlerMixin,
|
||||
add,
|
||||
getListStatus,
|
||||
getMultiStatus,
|
||||
attend,
|
||||
edit,
|
||||
get,
|
||||
updateStatus,
|
||||
getStatus,
|
||||
count,
|
||||
getMulti,
|
||||
setStatus,
|
||||
getAndListStatus,
|
||||
recalcStatus,
|
||||
isNew,
|
||||
isUpcoming,
|
||||
isNotStarted,
|
||||
isOngoing,
|
||||
isDone,
|
||||
isExtended,
|
||||
statusText,
|
||||
getStatusText,
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import * as problem from './problem';
|
||||
import * as contest from './contest';
|
||||
import * as training from './training';
|
||||
import * as document from './document';
|
||||
import { DocumentNotFoundError } from '../error';
|
||||
import { Ddoc, Drdoc, Drrdoc } from '../interface';
|
||||
|
||||
export const typeDisplay = {
|
||||
[document.TYPE_PROBLEM]: 'problem',
|
||||
[document.TYPE_CONTEST]: 'contest',
|
||||
[document.TYPE_DISCUSSION_NODE]: 'node',
|
||||
[document.TYPE_TRAINING]: 'training',
|
||||
[document.TYPE_HOMEWORK]: 'homework',
|
||||
};
|
||||
|
||||
export 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> {
|
||||
return document.add(
|
||||
domainId, content, owner, document.TYPE_DISCUSSION,
|
||||
null, parentType, parentId,
|
||||
{
|
||||
title,
|
||||
ip,
|
||||
nReply: 0,
|
||||
highlight,
|
||||
pin,
|
||||
updateAt: new Date(),
|
||||
views: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function get(domainId: string, did: ObjectID): Promise<Ddoc | null> {
|
||||
return document.get(domainId, document.TYPE_DISCUSSION, did);
|
||||
}
|
||||
|
||||
export function edit(
|
||||
domainId: string, did: ObjectID,
|
||||
title: string, content: string, highlight: boolean, pin: boolean,
|
||||
): Promise<Ddoc | null> {
|
||||
return document.set(domainId, document.TYPE_DISCUSSION, did, {
|
||||
title, content, highlight, pin,
|
||||
});
|
||||
}
|
||||
|
||||
export function del(domainId: string, did: ObjectID) {
|
||||
return 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 }),
|
||||
]);
|
||||
}
|
||||
|
||||
export function count(domainId: string, query: any) {
|
||||
return document.count(domainId, document.TYPE_DISCUSSION, query);
|
||||
}
|
||||
|
||||
export function getMulti(domainId: string, query: any = {}) {
|
||||
return document.getMulti(domainId, document.TYPE_DISCUSSION, query)
|
||||
.sort({ pin: -1, updateAt: -1 });
|
||||
}
|
||||
|
||||
export async function addReply(
|
||||
domainId: string, did: ObjectID, owner: number,
|
||||
content: string, ip: string,
|
||||
): Promise<Drdoc> {
|
||||
const [drdoc] = await Promise.all([
|
||||
document.add(
|
||||
domainId, content, owner, document.TYPE_DISCUSSION_REPLY,
|
||||
null, document.TYPE_DISCUSSION, did, { ip },
|
||||
),
|
||||
document.incAndSet(domainId, document.TYPE_DISCUSSION, did, 'nReply', 1, { updateAt: new Date() }),
|
||||
]);
|
||||
return drdoc;
|
||||
}
|
||||
|
||||
export function getReply(domainId: string, drid: ObjectID): Promise<Drdoc | null> {
|
||||
return document.get(domainId, document.TYPE_DISCUSSION_REPLY, drid);
|
||||
}
|
||||
|
||||
export async function editReply(
|
||||
domainId: string, drid: ObjectID, content: string,
|
||||
): Promise<Drdoc | null> {
|
||||
return document.set(domainId, document.TYPE_DISCUSSION_REPLY, drid, { content });
|
||||
}
|
||||
|
||||
export async function delReply(domainId: string, drid: ObjectID) {
|
||||
const drdoc = await getReply(domainId, drid);
|
||||
if (!drdoc) throw new DocumentNotFoundError(drid);
|
||||
return await Promise.all([
|
||||
document.deleteOne(domainId, document.TYPE_DISCUSSION_REPLY, drid),
|
||||
document.inc(domainId, document.TYPE_DISCUSSION, drdoc.parentId, 'nReply', -1),
|
||||
]);
|
||||
}
|
||||
|
||||
export function getMultiReply(domainId: string, did: ObjectID) {
|
||||
return document.getMulti(
|
||||
domainId, document.TYPE_DISCUSSION_REPLY,
|
||||
{ parentType: document.TYPE_DISCUSSION, parentId: did },
|
||||
).sort('_id', -1);
|
||||
}
|
||||
|
||||
export function getListReply(domainId: string, did: ObjectID): Promise<Drdoc[]> {
|
||||
return getMultiReply(domainId, did).toArray();
|
||||
}
|
||||
|
||||
export async function addTailReply(
|
||||
domainId: string, drid: ObjectID,
|
||||
owner: number, content: string, ip: string,
|
||||
): Promise<[Drdoc, ObjectID]> {
|
||||
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() },
|
||||
);
|
||||
return [drdoc, subId];
|
||||
}
|
||||
|
||||
export function getTailReply(
|
||||
domainId: string, drid: ObjectID, drrid: ObjectID,
|
||||
): Promise<[Drdoc, Drrdoc] | [null, null]> {
|
||||
// @ts-ignore
|
||||
return document.getSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid);
|
||||
}
|
||||
|
||||
export function editTailReply(
|
||||
domainId: string, drid: ObjectID, drrid: ObjectID, content: string,
|
||||
): Promise<Drrdoc> {
|
||||
return document.setSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid, { content });
|
||||
}
|
||||
|
||||
export async function delTailReply(domainId: string, drid: ObjectID, drrid: ObjectID) {
|
||||
return document.deleteSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid);
|
||||
}
|
||||
|
||||
export function setStar(domainId: string, did: ObjectID, uid: number, star: boolean) {
|
||||
return document.setStatus(domainId, document.TYPE_DISCUSSION, did, uid, { star });
|
||||
}
|
||||
|
||||
export function getStatus(domainId: string, did: ObjectID, uid: number) {
|
||||
return document.getStatus(domainId, document.TYPE_DISCUSSION, did, uid);
|
||||
}
|
||||
|
||||
export function addNode(domainId: string, _id: string, category: string, args: any = {}) {
|
||||
return document.add(
|
||||
domainId, category, 1, document.TYPE_DISCUSSION_NODE,
|
||||
_id, null, null, args,
|
||||
);
|
||||
}
|
||||
|
||||
export function getNode(domainId: string, _id: string) {
|
||||
return document.get(domainId, document.TYPE_DISCUSSION_NODE, _id);
|
||||
}
|
||||
|
||||
export async function getVnode(domainId: string, type: number, id: string) {
|
||||
if (type === document.TYPE_PROBLEM) {
|
||||
// @ts-ignore
|
||||
if (Number.isSafeInteger(parseInt(id, 10))) id = parseInt(id, 10);
|
||||
const pdoc = await problem.get(domainId, id);
|
||||
if (!pdoc) return null;
|
||||
return { ...pdoc, type, id };
|
||||
}
|
||||
if (type === document.TYPE_CONTEST) {
|
||||
const tdoc = await contest.get(domainId, new ObjectID(id));
|
||||
return { ...tdoc, type, id };
|
||||
}
|
||||
if (type === document.TYPE_DISCUSSION_NODE) {
|
||||
const ndoc = await getNode(domainId, id);
|
||||
return {
|
||||
...ndoc,
|
||||
title: id,
|
||||
type,
|
||||
id,
|
||||
};
|
||||
}
|
||||
if (type === document.TYPE_TRAINING) {
|
||||
const tdoc = await training.get(domainId, id);
|
||||
return { ...tdoc, type, id };
|
||||
}
|
||||
if (type === document.TYPE_HOMEWORK) {
|
||||
const tdoc = await contest.get(domainId, new ObjectID(id), document.TYPE_HOMEWORK);
|
||||
return { ...tdoc, type, id };
|
||||
}
|
||||
return {
|
||||
title: 'Missing Node',
|
||||
type: 'Unknown',
|
||||
id: new ObjectID(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNodes(domainId: string) {
|
||||
return document.getMulti(domainId, document.TYPE_DISCUSSION_NODE).toArray();
|
||||
}
|
||||
|
||||
export async function getListVnodes(domainId: string, ddocs: any, getHidden: boolean) {
|
||||
const tasks = [];
|
||||
const res = {};
|
||||
async function task(ddoc: Ddoc) {
|
||||
const vnode = await getVnode(domainId, ddoc.parentType, ddoc.parentId.toString());
|
||||
if (!res[ddoc.parentType]) res[ddoc.parentType] = {};
|
||||
if (vnode.hidden && !getHidden) return;
|
||||
res[ddoc.parentType][ddoc.parentId] = vnode;
|
||||
}
|
||||
for (const ddoc of ddocs) tasks.push(task(ddoc));
|
||||
await Promise.all(tasks);
|
||||
return res;
|
||||
}
|
||||
|
||||
global.Hydro.model.discussion = {
|
||||
typeDisplay,
|
||||
add,
|
||||
get,
|
||||
edit,
|
||||
del,
|
||||
count,
|
||||
getMulti,
|
||||
addReply,
|
||||
getReply,
|
||||
editReply,
|
||||
delReply,
|
||||
getMultiReply,
|
||||
getListReply,
|
||||
addTailReply,
|
||||
getTailReply,
|
||||
editTailReply,
|
||||
delTailReply,
|
||||
setStar,
|
||||
getStatus,
|
||||
addNode,
|
||||
getNode,
|
||||
getNodes,
|
||||
getVnode,
|
||||
getListVnodes,
|
||||
};
|
||||
@@ -0,0 +1,407 @@
|
||||
import assert from 'assert';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import * as db from '../service/db';
|
||||
|
||||
type DocID = ObjectID | string | number;
|
||||
|
||||
const coll = db.collection('document');
|
||||
const collStatus = db.collection('document.status');
|
||||
|
||||
export const TYPE_PROBLEM = 10;
|
||||
export const TYPE_PROBLEM_SOLUTION = 11;
|
||||
export const TYPE_PROBLEM_LIST = 12;
|
||||
export const TYPE_DISCUSSION_NODE = 20;
|
||||
export const TYPE_DISCUSSION = 21;
|
||||
export const TYPE_DISCUSSION_REPLY = 22;
|
||||
export const TYPE_CONTEST = 30;
|
||||
export const TYPE_TRAINING = 40;
|
||||
export const TYPE_FILE = 50;
|
||||
export const TYPE_HOMEWORK = 60;
|
||||
|
||||
export async function add<T extends DocID>(
|
||||
domainId: string, content: string, owner: number,
|
||||
docType: number, docId: T,
|
||||
parentType?: number | null, parentId?: DocID,
|
||||
args?: any,
|
||||
): Promise<T>
|
||||
export async function add(
|
||||
domainId: string, content: string, owner: number,
|
||||
docType: number, docId: null,
|
||||
parentType?: number, parentId?: DocID,
|
||||
args?: any,
|
||||
): Promise<ObjectID>
|
||||
export async function add(
|
||||
domainId: string, content: string, owner: number,
|
||||
docType: number, docId: DocID = null,
|
||||
parentType: number | null = null, parentId: DocID = null,
|
||||
args: any = {},
|
||||
) {
|
||||
const _id = new ObjectID();
|
||||
const doc: any = {
|
||||
_id,
|
||||
content,
|
||||
owner,
|
||||
domainId,
|
||||
docType,
|
||||
docId: docId || _id,
|
||||
...args,
|
||||
};
|
||||
if (parentType || parentId) {
|
||||
assert(parentType && parentId);
|
||||
doc.parentType = parentType;
|
||||
doc.parentId = parentId;
|
||||
}
|
||||
const res = await coll.insertOne(doc);
|
||||
return docId || res.insertedId;
|
||||
}
|
||||
|
||||
export function get(domainId: string, docType: number, docId: DocID) {
|
||||
return coll.findOne({ domainId, docType, docId });
|
||||
}
|
||||
|
||||
export async function set(domainId: string, docType: number, docId: DocID, args: any) {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ domainId, docType, docId },
|
||||
{ $set: args },
|
||||
{ returnOriginal: false, upsert: true },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export function deleteOne(domainId: string, docType: number, docId: DocID) {
|
||||
return Promise.all([
|
||||
collStatus.deleteMany({ domainId, docType, docId }),
|
||||
coll.deleteOne({ domainId, docType, docId }),
|
||||
]);
|
||||
}
|
||||
|
||||
export function deleteMulti(domainId: string, docType: number, args: any = {}) {
|
||||
return coll.deleteMany({ ...args, domainId, docType });
|
||||
}
|
||||
|
||||
export function deleteMultiStatus(domainId: string, docType: number, args: any = {}) {
|
||||
return collStatus.deleteMany({ ...args, domainId, docType });
|
||||
}
|
||||
|
||||
export function getMulti(domainId: string, docType: number, args: any = {}) {
|
||||
return coll.find({ ...args, docType, domainId });
|
||||
}
|
||||
|
||||
export async function inc(
|
||||
domainId: string, docType: number, docId: DocID,
|
||||
key: string, value: number,
|
||||
) {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ domainId, docType, docId },
|
||||
{ $inc: { [key]: value } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function incAndSet(
|
||||
domainId: string, docType: number, docId: DocID,
|
||||
key: string, value: number, args: any,
|
||||
) {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ domainId, docType, docId },
|
||||
{ $inc: { [key]: value }, $set: args },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export function count(domainId: string, docType: number, query: any) {
|
||||
return coll.find({ ...query, docType, domainId }).count();
|
||||
}
|
||||
|
||||
export async function push(
|
||||
domainId: string, docType: number, docId: DocID,
|
||||
key: string, content: string, owner: number, args: any = {},
|
||||
) {
|
||||
const _id = new ObjectID();
|
||||
const doc = await coll.findOneAndUpdate(
|
||||
{ domainId, docType, docId },
|
||||
{
|
||||
$push: {
|
||||
[key]: {
|
||||
...args, content, owner, _id,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return [doc.value, _id];
|
||||
}
|
||||
|
||||
export async function pull(
|
||||
domainId: string, docType: number, docId: DocID,
|
||||
setKey: string, contents: string[],
|
||||
) {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ domainId, docType, docId },
|
||||
{ $pull: { [setKey]: { $in: contents } } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function deleteSub(
|
||||
domainId: string, docType: number, docId: DocID,
|
||||
key: string, subId: ObjectID,
|
||||
) {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ domainId, docType, docId },
|
||||
{ $pull: { [key]: { _id: subId } } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function getSub(
|
||||
domainId: string, docType: number, docId: DocID,
|
||||
key: string, subId: ObjectID,
|
||||
) {
|
||||
const doc = await coll.findOne({
|
||||
domainId,
|
||||
docType,
|
||||
docId,
|
||||
[key]: { $elemMatch: { _id: subId } },
|
||||
});
|
||||
if (!doc) return [null, null];
|
||||
for (const sdoc of doc[key] || []) {
|
||||
if (sdoc._id === subId) return [doc, sdoc];
|
||||
}
|
||||
return [doc, null];
|
||||
}
|
||||
|
||||
export async function setSub(
|
||||
domainId: string, docType: number, docId: DocID,
|
||||
key: string, subId: ObjectID, args: any,
|
||||
) {
|
||||
const $set = {};
|
||||
for (const k in args) {
|
||||
$set[`${key}.$.${k}`] = args[k];
|
||||
}
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{
|
||||
domainId,
|
||||
docType,
|
||||
docId,
|
||||
[key]: { $elemMatch: { _id: subId } },
|
||||
},
|
||||
{ $set },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function addToSet(
|
||||
domainId: string, docType: number, docId: DocID,
|
||||
setKey: string, content: string,
|
||||
) {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ domainId, docType, docId },
|
||||
{ $addToSet: { [setKey]: content } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export function getStatus(domainId: string, docType: number, docId: DocID, uid: number) {
|
||||
return collStatus.findOne({
|
||||
domainId, docType, docId, uid,
|
||||
});
|
||||
}
|
||||
|
||||
export function getMultiStatus(domainId: string, docType: number, args: any) {
|
||||
return collStatus.find({ ...args, docType, domainId });
|
||||
}
|
||||
|
||||
export function getMultiStatusWithoutDomain(docType: number, args: any) {
|
||||
return collStatus.find({ ...args, docType });
|
||||
}
|
||||
|
||||
export async function setStatus(
|
||||
domainId: string, docType: number, docId: DocID, uid: number, args: any,
|
||||
) {
|
||||
const res = await collStatus.findOneAndUpdate(
|
||||
{
|
||||
domainId, docType, docId, uid,
|
||||
},
|
||||
{ $set: args },
|
||||
{ upsert: true, returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export function setMultiStatus(domainId: string, docType: number, query: any, args: any) {
|
||||
return collStatus.updateMany(
|
||||
{ domainId, docType, ...query },
|
||||
{ $set: args },
|
||||
);
|
||||
}
|
||||
|
||||
export async function setIfNotStatus(
|
||||
domainId: string, docType: number, docId: DocID, uid: number,
|
||||
key: string, value: number, ifNot: any, args: any,
|
||||
) {
|
||||
const res = await collStatus.findOneAndUpdate(
|
||||
{
|
||||
domainId, docType, docId, uid, key: { $not: { $eq: ifNot } },
|
||||
},
|
||||
{ $set: { [key]: value, ...args } },
|
||||
{ upsert: true, returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function cappedIncStatus(
|
||||
domainId: string, docType: number, docId: DocID, uid: number,
|
||||
key: string, value: number, minValue = -1, maxValue = 1,
|
||||
) {
|
||||
assert(value !== 0);
|
||||
const $not = value > 0 ? { $gte: maxValue } : { $lte: minValue };
|
||||
const res = await collStatus.findOneAndUpdate(
|
||||
{
|
||||
domainId, docType, docId, uid, [key]: { $not },
|
||||
},
|
||||
{ $inc: { [key]: value } },
|
||||
{ upsert: true, returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function incStatus(
|
||||
domainId: string, docType: number, docId: DocID, uid: number,
|
||||
key: string, value: number,
|
||||
) {
|
||||
const res = await collStatus.findOneAndUpdate(
|
||||
{
|
||||
domainId, docType, docId, uid,
|
||||
},
|
||||
{ $inc: { [key]: value } },
|
||||
{ upsert: true, returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function revPushStatus(
|
||||
domainId: string, docType: number, docId: DocID, uid: number,
|
||||
key: string, value: any,
|
||||
) {
|
||||
const res = await collStatus.findOneAndUpdate(
|
||||
{
|
||||
domainId, docType, docId, uid,
|
||||
},
|
||||
{ $push: { [key]: value }, $inc: { rev: 1 } },
|
||||
{ upsert: true, returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function revInitStatus(
|
||||
domainId: string, docType: number, docId: DocID, uid: number,
|
||||
) {
|
||||
const res = await collStatus.findOneAndUpdate(
|
||||
{
|
||||
domainId, docType, docId, uid,
|
||||
},
|
||||
{ $inc: { rev: 1 } },
|
||||
{ upsert: true, returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function revSetStatus(
|
||||
domainId: string, docType: number, docId: DocID, uid: number,
|
||||
rev: number, args: any, returnDoc = true,
|
||||
) {
|
||||
const filter = {
|
||||
domainId, docType, docId, uid, rev,
|
||||
};
|
||||
const update = { $set: args, $inc: { rev: 1 } };
|
||||
if (returnDoc) {
|
||||
const res = await collStatus.findOneAndUpdate(filter, update, { returnOriginal: false });
|
||||
return res.value;
|
||||
}
|
||||
return await collStatus.updateOne(filter, update);
|
||||
}
|
||||
|
||||
/* eslint-disable object-curly-newline */
|
||||
async function ensureIndexes() {
|
||||
const ic = coll.createIndex.bind(coll);
|
||||
const is = collStatus.createIndex.bind(coll);
|
||||
const u = { unique: true };
|
||||
const s = { sparse: true };
|
||||
await ic({ domainId: 1, docType: 1, docId: 1 }, u);
|
||||
await ic({ domainId: 1, docType: 1, owner: 1, docId: -1 });
|
||||
// For problem
|
||||
await ic({ domainId: 1, docType: 1, category: 1, docId: 1 }, s);
|
||||
await ic({ domainId: 1, docType: 1, hidden: 1, category: 1, docId: 1 }, s);
|
||||
await ic({ domainId: 1, docType: 1, tag: 1, docId: 1 }, s);
|
||||
await ic({ domainId: 1, docType: 1, hidden: 1, tag: 1, docId: 1 }, s);
|
||||
// For problem solution
|
||||
await ic({ domainId: 1, docType: 1, parentType: 1, parentId: 1, vote: -1, docId: -1 }, s);
|
||||
// For discussion
|
||||
await ic({ domainId: 1, docType: 1, pin: -1, updateAt: -1, docId: -1 }, s);
|
||||
await ic({ domainId: 1, docType: 1, parentType: 1, parentId: 1, updateAt: -1, docId: -1 }, s);
|
||||
// Hidden doc
|
||||
await ic({ domainId: 1, docType: 1, hidden: 1, docId: -1 }, s);
|
||||
// For contest
|
||||
await ic({ domainId: 1, docType: 1, pids: 1 }, s);
|
||||
await ic({ domainId: 1, docType: 1, rule: 1, docId: -1 }, s);
|
||||
// For training
|
||||
await ic({ domainId: 1, docType: 1, 'dag.pids': 1 }, s);
|
||||
await is({ domainId: 1, docType: 1, uid: 1, docId: 1 }, u);
|
||||
// For rp system
|
||||
await is({ domainId: 1, docType: 1, docId: 1, status: 1, rid: 1, rp: 1 }, s);
|
||||
// For contest rule OI
|
||||
await is({ domainId: 1, docType: 1, docId: 1, score: -1 }, s);
|
||||
// For contest rule ACM
|
||||
await is({ domainId: 1, docType: 1, docId: 1, accept: -1, time: 1 }, s);
|
||||
// For training
|
||||
await is({ domainId: 1, docType: 1, uid: 1, enroll: 1, docId: 1 }, s);
|
||||
}
|
||||
|
||||
global.Hydro.postInit.push(ensureIndexes);
|
||||
global.Hydro.model.document = {
|
||||
add,
|
||||
addToSet,
|
||||
cappedIncStatus,
|
||||
count,
|
||||
deleteMulti,
|
||||
deleteMultiStatus,
|
||||
deleteOne,
|
||||
deleteSub,
|
||||
get,
|
||||
getMulti,
|
||||
getMultiStatus,
|
||||
getMultiStatusWithoutDomain,
|
||||
getStatus,
|
||||
getSub,
|
||||
inc,
|
||||
incAndSet,
|
||||
incStatus,
|
||||
pull,
|
||||
push,
|
||||
revInitStatus,
|
||||
revPushStatus,
|
||||
revSetStatus,
|
||||
set,
|
||||
setIfNotStatus,
|
||||
setStatus,
|
||||
setMultiStatus,
|
||||
setSub,
|
||||
|
||||
TYPE_CONTEST,
|
||||
TYPE_DISCUSSION,
|
||||
TYPE_DISCUSSION_NODE,
|
||||
TYPE_DISCUSSION_REPLY,
|
||||
TYPE_HOMEWORK,
|
||||
TYPE_PROBLEM,
|
||||
TYPE_PROBLEM_LIST,
|
||||
TYPE_PROBLEM_SOLUTION,
|
||||
TYPE_FILE,
|
||||
TYPE_TRAINING,
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
import { Dictionary } from 'lodash';
|
||||
import { BUILTIN_ROLES, PRIV } from './builtin';
|
||||
import { DomainDoc } from '../interface';
|
||||
import * as db from '../service/db';
|
||||
|
||||
const coll = db.collection('domain');
|
||||
const collUser = db.collection('domain.user');
|
||||
|
||||
export async function add(domainId: string, owner: number, name: string, bulletin: string) {
|
||||
const ddoc: DomainDoc = {
|
||||
_id: domainId,
|
||||
owner,
|
||||
name,
|
||||
bulletin,
|
||||
roles: {},
|
||||
gravatar: '',
|
||||
pidCounter: 0,
|
||||
};
|
||||
await coll.insertOne(ddoc);
|
||||
return domainId;
|
||||
}
|
||||
|
||||
export function get(domainId: string): Promise<DomainDoc | null> {
|
||||
return coll.findOne({ _id: domainId });
|
||||
}
|
||||
|
||||
export function getMulti(query: any = {}) {
|
||||
return coll.find(query);
|
||||
}
|
||||
|
||||
export function edit(domainId: string, $set: any) {
|
||||
return coll.updateOne({ _id: domainId }, { $set });
|
||||
}
|
||||
|
||||
export async function inc(domainId: string, field: string, n: number): Promise<number | null> {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ _id: domainId },
|
||||
{ $inc: { [field]: n } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value[field];
|
||||
}
|
||||
|
||||
export async function getList(domainIds: string[]): Promise<Dictionary<DomainDoc>> {
|
||||
const r = {};
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
for (const domainId of domainIds) r[domainId] = await get(domainId);
|
||||
return r;
|
||||
}
|
||||
|
||||
export function setUserRole(domainId: string, uid: number, role: string) {
|
||||
return collUser.updateOne({ uid, domainId }, { role }, { upsert: true });
|
||||
}
|
||||
|
||||
export async function getRoles(arg: string | DomainDoc) {
|
||||
let ddoc: DomainDoc;
|
||||
if (typeof arg === 'string') ddoc = await get(arg);
|
||||
else ddoc = arg;
|
||||
const roles = [];
|
||||
const r = [];
|
||||
for (const role in ddoc.roles) {
|
||||
roles.push({ _id: role, perm: BigInt(ddoc.roles[role]) });
|
||||
r.push(role);
|
||||
}
|
||||
for (const role in BUILTIN_ROLES) {
|
||||
if (!r.includes(role)) {
|
||||
roles.push({ _id: role, perm: BUILTIN_ROLES[role] });
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
export async function setRoles(domainId: string, roles: Dictionary<bigint>) {
|
||||
const current = await get(domainId);
|
||||
for (const role in roles) {
|
||||
current.roles[role] = roles[role].toString();
|
||||
}
|
||||
return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } });
|
||||
}
|
||||
|
||||
export async function addRole(domainId: string, name: string, permission: bigint) {
|
||||
const current = await get(domainId);
|
||||
current.roles[name] = permission.toString();
|
||||
return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } });
|
||||
}
|
||||
|
||||
export async function deleteRoles(domainId: string, roles: string[]) {
|
||||
const current = await get(domainId);
|
||||
for (const role of roles) delete current.roles[role];
|
||||
await Promise.all([
|
||||
coll.updateOne({ _id: domainId }, { $set: current }),
|
||||
collUser.updateMany({ domainId, role: { $in: roles } }, { $set: { role: 'default' } }),
|
||||
]);
|
||||
}
|
||||
|
||||
interface DomainUserArg {
|
||||
_id: number,
|
||||
priv: number,
|
||||
}
|
||||
export async function getDomainUser(domainId: string, udoc: DomainUserArg) {
|
||||
let dudoc = await collUser.findOne({ domainId, uid: udoc._id });
|
||||
dudoc = dudoc || {};
|
||||
if (!(udoc.priv & PRIV.PRIV_USER_PROFILE)) dudoc.role = 'guest';
|
||||
if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'admin';
|
||||
dudoc.role = dudoc.role || 'default';
|
||||
const ddoc = await get(domainId);
|
||||
dudoc.perm = ddoc.roles[dudoc.role]
|
||||
? BigInt(ddoc.roles[dudoc.role])
|
||||
: BUILTIN_ROLES[dudoc.role];
|
||||
return dudoc;
|
||||
}
|
||||
|
||||
export function setMultiUserInDomain(domainId: string, query: any, params: any) {
|
||||
return collUser.updateMany({ domainId, ...query }, { $set: params });
|
||||
}
|
||||
|
||||
export function getMultiInDomain(domainId: string, query: any = {}) {
|
||||
return collUser.find({ domainId, ...query });
|
||||
}
|
||||
|
||||
export function setUserInDomain(domainId: string, uid: number, params: any) {
|
||||
return collUser.updateOne({ domainId, uid }, { $set: params });
|
||||
}
|
||||
|
||||
export async function incUserInDomain(domainId: string, uid: number, field: string, n = 1) {
|
||||
// @ts-ignore
|
||||
const dudoc = await getDomainUser(domainId, { _id: uid });
|
||||
dudoc[field] = dudoc[field] + n || n;
|
||||
await setUserInDomain(domainId, uid, { [field]: dudoc[field] });
|
||||
return dudoc;
|
||||
}
|
||||
|
||||
export async function getDictUserByDomainId(uid: number) {
|
||||
const dudocs = await collUser.find({ uid }).toArray();
|
||||
const ddocs = await coll.find({ owner: uid }).toArray();
|
||||
const dudict = {};
|
||||
for (const dudoc of dudocs) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
dudict[dudoc.domainId] = await get(dudoc.domainId);
|
||||
}
|
||||
for (const ddoc of ddocs) {
|
||||
dudict[ddoc._id] = ddoc;
|
||||
}
|
||||
return dudict;
|
||||
}
|
||||
|
||||
export async function getPrefixSearch(prefix: string, limit = 50) {
|
||||
const $regex = new RegExp(prefix, 'mi');
|
||||
const ddocs = await coll.find({
|
||||
$or: [{ _id: { $regex } }, { name: { $regex } }],
|
||||
}).limit(limit).toArray();
|
||||
return ddocs;
|
||||
}
|
||||
|
||||
global.Hydro.model.domain = {
|
||||
getRoles,
|
||||
add,
|
||||
inc,
|
||||
get,
|
||||
edit,
|
||||
getMulti,
|
||||
getList,
|
||||
setRoles,
|
||||
addRole,
|
||||
deleteRoles,
|
||||
setUserRole,
|
||||
getDomainUser,
|
||||
setMultiUserInDomain,
|
||||
setUserInDomain,
|
||||
incUserInDomain,
|
||||
getMultiInDomain,
|
||||
getDictUserByDomainId,
|
||||
getPrefixSearch,
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import * as fs from 'fs';
|
||||
import { Dictionary } from 'lodash';
|
||||
import { ForbiddenError, NotFoundError } from '../error';
|
||||
import * as db from '../service/db';
|
||||
import gridfs from '../service/gridfs';
|
||||
import hash from '../lib/hash.hydro';
|
||||
import { Ufdoc } from '../interface';
|
||||
|
||||
const coll = db.collection('file');
|
||||
const collFile = db.collection('fs.files');
|
||||
const collChunk = db.collection('fs.chunks');
|
||||
|
||||
function _timestamp() {
|
||||
return Math.floor(Number(new Date()) / 1000000);
|
||||
}
|
||||
|
||||
export async function add(
|
||||
streamOrPath: fs.ReadStream | string, filename: string,
|
||||
owner = 1, meta = {},
|
||||
) {
|
||||
let stream: fs.ReadStream;
|
||||
if (typeof streamOrPath === 'string') stream = fs.createReadStream(streamOrPath);
|
||||
else stream = streamOrPath;
|
||||
const w = gridfs.openUploadStream(filename);
|
||||
await coll.insertOne({
|
||||
...meta,
|
||||
_id: w.id,
|
||||
secret: String.random(32),
|
||||
owner,
|
||||
filename,
|
||||
count: 1,
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
w.on('error', reject);
|
||||
w.on('finish', resolve);
|
||||
stream.pipe(w);
|
||||
});
|
||||
const c = await gridfs.find({ _id: w.id }).toArray();
|
||||
await coll.updateOne({ _id: w.id }, { $set: { md5: c[0].md5, size: c[0].length } });
|
||||
return w.id as ObjectID;
|
||||
}
|
||||
|
||||
export function del(_id: ObjectID) {
|
||||
return Promise.all([
|
||||
coll.deleteOne({ _id }),
|
||||
collFile.deleteOne({ _id }),
|
||||
collChunk.deleteMany({ files_id: _id }),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function inc(_id: ObjectID): Promise<number> {
|
||||
const doc = await coll.findOneAndUpdate(
|
||||
{ _id },
|
||||
{ $inc: { count: 1 } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return doc.value?.count;
|
||||
}
|
||||
|
||||
export async function dec(_id: ObjectID): Promise<number> {
|
||||
const file = await coll.findOneAndUpdate(
|
||||
{ _id },
|
||||
{ $inc: { count: -1 } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
if (!file.value) return 0;
|
||||
if (!file.value.count) await del(_id);
|
||||
return file.value.count;
|
||||
}
|
||||
|
||||
export async function getWithSecret(_id: ObjectID, secret: string, reject?: Function) {
|
||||
const file = await coll.findOne({ _id });
|
||||
if (!file) throw new NotFoundError(_id);
|
||||
const timestamp = _timestamp();
|
||||
if (!(hash(file.secret, timestamp.toString()) === secret)) {
|
||||
if (!(hash(file.secret, (timestamp - 1).toString()) === secret)) {
|
||||
throw new ForbiddenError();
|
||||
}
|
||||
}
|
||||
const stream = gridfs.openDownloadStream(_id);
|
||||
stream.on('error', (err) => {
|
||||
console.error(err);
|
||||
if (reject) reject();
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
export async function get(_id: ObjectID, reject?: Function) {
|
||||
const file = await coll.findOne({ _id });
|
||||
if (!file) throw new NotFoundError(_id);
|
||||
const stream = gridfs.openDownloadStream(_id);
|
||||
stream.on('error', (err) => {
|
||||
console.error(err);
|
||||
if (reject) reject();
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
export function getMeta(_id: ObjectID): Promise<Ufdoc> {
|
||||
return coll.findOne({ _id });
|
||||
}
|
||||
|
||||
export async function getMetaDict(ufids: ObjectID[]): Promise<Dictionary<Ufdoc>> {
|
||||
const r = {};
|
||||
ufids = Array.from(new Set(ufids));
|
||||
const ufdocs = await coll.find({ _id: { $in: ufids } }).toArray();
|
||||
for (const ufdoc of ufdocs) {
|
||||
r[ufdoc._id.toHexString()] = ufdoc;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
export function getMulti(query: any) {
|
||||
return coll.find(query);
|
||||
}
|
||||
|
||||
async function _url(_id: ObjectID, name: string | undefined) {
|
||||
const file = await coll.findOne({ _id });
|
||||
const secret = hash(file.secret, _timestamp().toString());
|
||||
if (name) return `/fs/${_id}/${Buffer.from(name).toString('base64')}/${secret}`;
|
||||
return `/fs/${_id}/${secret}`;
|
||||
}
|
||||
|
||||
function __url(file: any, name: string | undefined) {
|
||||
const secret = hash(file.secret, _timestamp().toString());
|
||||
if (name) return `/fs/${file._id}/${Buffer.from(name).toString('base64')}/${secret}`;
|
||||
return `/fs/${file._id}/${secret}`;
|
||||
}
|
||||
|
||||
export function url(_id: ObjectID, name: string | undefined): Promise<string>;
|
||||
export function url(file: any, name: string | undefined): string;
|
||||
export function url(arg0: any, name: any) {
|
||||
if (arg0 instanceof ObjectID) return _url(arg0, name);
|
||||
return __url(arg0, name);
|
||||
}
|
||||
|
||||
function ensureIndexes() {
|
||||
return Promise.all([
|
||||
collChunk.createIndex({ files_id: 1, n: 1 }, { unique: true }),
|
||||
collFile.createIndex({ md5: 1 }),
|
||||
]);
|
||||
}
|
||||
|
||||
global.Hydro.postInit.push(ensureIndexes);
|
||||
global.Hydro.model.file = {
|
||||
add, get, del, inc, dec, getMeta, getWithSecret, getMulti, getMetaDict, url,
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import * as user from './user';
|
||||
import { Mdoc } from '../interface';
|
||||
import * as db from '../service/db';
|
||||
import * as bus from '../service/bus';
|
||||
|
||||
const coll = db.collection('message');
|
||||
|
||||
export const FLAG_UNREAD = 1;
|
||||
export const FLAG_ALERT = 2;
|
||||
|
||||
export async function send(
|
||||
from: number, to: number,
|
||||
content: string, flag: number = FLAG_UNREAD,
|
||||
): Promise<Mdoc> {
|
||||
const res = await coll.insertOne({
|
||||
from, to, content, flag,
|
||||
});
|
||||
const mdoc = {
|
||||
from, to, content, _id: res.insertedId, flag,
|
||||
};
|
||||
if (from !== to) {
|
||||
// ENHANCE domainId?
|
||||
const udoc = await user.getById('system', to);
|
||||
bus.publish(`user_message-${to}`, { mdoc, udoc });
|
||||
}
|
||||
return mdoc;
|
||||
}
|
||||
|
||||
export async function get(_id: ObjectID): Promise<Mdoc | null> {
|
||||
return await coll.findOne({ _id });
|
||||
}
|
||||
|
||||
export async function getByUser(uid: number): Promise<Mdoc[]> {
|
||||
return await coll.find({ $or: [{ from: uid }, { to: uid }] }).sort('_id', 1).toArray();
|
||||
}
|
||||
|
||||
export async function getMany(query: any, sort: any, page: number, limit: number): Promise<Mdoc[]> {
|
||||
return await coll.find(query).sort(sort)
|
||||
.skip((page - 1) * limit).limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
export async function setFlag(messageId: ObjectID, flag: number): Promise<Mdoc | null> {
|
||||
const result = await coll.findOneAndUpdate(
|
||||
{ _id: messageId },
|
||||
{ $bit: { flag: { xor: flag } } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return result.value;
|
||||
}
|
||||
|
||||
export async function del(_id: ObjectID) {
|
||||
return await coll.deleteOne({ _id });
|
||||
}
|
||||
|
||||
export function count(query: any) {
|
||||
return coll.find(query).count();
|
||||
}
|
||||
|
||||
export function getMulti(uid: number) {
|
||||
return coll.find({ $or: [{ from: uid }, { to: uid }] });
|
||||
}
|
||||
|
||||
function ensureIndexes() {
|
||||
return Promise.all([
|
||||
coll.createIndex({ to: 1, _id: -1 }),
|
||||
coll.createIndex({ from: 1, _id: -1 }),
|
||||
]);
|
||||
}
|
||||
|
||||
global.Hydro.postInit.push(ensureIndexes);
|
||||
global.Hydro.model.message = {
|
||||
FLAG_UNREAD,
|
||||
FLAG_ALERT,
|
||||
|
||||
count,
|
||||
get,
|
||||
getByUser,
|
||||
del,
|
||||
setFlag,
|
||||
getMany,
|
||||
getMulti,
|
||||
send,
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { OpcountExceededError } from '../error';
|
||||
import * as db from '../service/db';
|
||||
|
||||
const coll = db.collection('opcount');
|
||||
|
||||
export async function inc(op: string, ident: string, periodSecs: number, maxOperations: number) {
|
||||
const curTime = new Date().getTime();
|
||||
const beginAt = new Date(curTime - (curTime % (periodSecs * 1000)));
|
||||
const expireAt = new Date(beginAt.getTime() + periodSecs * 1000);
|
||||
try {
|
||||
await coll.findOneAndUpdate({
|
||||
ident,
|
||||
beginAt,
|
||||
expireAt,
|
||||
op: { $not: { $gte: maxOperations } },
|
||||
}, { $inc: { op: 1 } }, { upsert: true });
|
||||
} catch (e) {
|
||||
throw new OpcountExceededError(op, periodSecs, maxOperations);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureIndexes() {
|
||||
return coll.createIndex('expireAt', { expireAfterSeconds: 0 });
|
||||
}
|
||||
|
||||
global.Hydro.postInit.push(ensureIndexes);
|
||||
global.Hydro.model.opcount = { inc };
|
||||
@@ -0,0 +1,169 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { Dictionary } from 'lodash';
|
||||
import { STATUS } from './builtin';
|
||||
import * as file from './file';
|
||||
import * as document from './document';
|
||||
import * as domain from './domain';
|
||||
import {
|
||||
Pdoc, Pdict, ProblemDataSource, ProblemStatusDoc,
|
||||
} from '../interface';
|
||||
import { ProblemNotFoundError } from '../error';
|
||||
import * as testdataConfig from '../lib/testdataConfig';
|
||||
|
||||
export const pdocHidden: Pdoc = {
|
||||
_id: new ObjectID(),
|
||||
domainId: 'system',
|
||||
docType: document.TYPE_PROBLEM,
|
||||
docId: -1,
|
||||
pid: '',
|
||||
owner: 1,
|
||||
title: '*',
|
||||
content: '',
|
||||
nSubmit: 0,
|
||||
nAccept: 0,
|
||||
difficulty: 0,
|
||||
tag: [],
|
||||
category: [],
|
||||
data: null,
|
||||
hidden: true,
|
||||
config: {},
|
||||
acMsg: '',
|
||||
};
|
||||
|
||||
export async function add(
|
||||
domainId: string, pid: string = null, title: string, content: string, owner: number,
|
||||
tag: string[] = [], category: string[] = [], data: ProblemDataSource = null, hidden = false,
|
||||
) {
|
||||
const pidCounter = await domain.inc(domainId, 'pidCounter', 1);
|
||||
if (!pid) pid = pidCounter.toString();
|
||||
return await document.add(
|
||||
domainId, content, owner, document.TYPE_PROBLEM, pidCounter, null, null,
|
||||
{
|
||||
pid, title, data, category, tag, hidden, nSubmit: 0, nAccept: 0,
|
||||
},
|
||||
) as number;
|
||||
}
|
||||
|
||||
export async function get(
|
||||
domainId: string, pid: string | number,
|
||||
uid: number = null,
|
||||
): Promise<Pdoc> {
|
||||
if (typeof pid !== 'number') {
|
||||
if (Number.isSafeInteger(parseInt(pid, 10))) pid = parseInt(pid, 10);
|
||||
}
|
||||
const pdoc = Number.isInteger(pid)
|
||||
? await document.get(domainId, document.TYPE_PROBLEM, pid)
|
||||
: (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0];
|
||||
if (!pdoc) return null;
|
||||
if (uid) {
|
||||
pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid);
|
||||
}
|
||||
return pdoc;
|
||||
}
|
||||
|
||||
export function getMulti(domainId: string, query: object) {
|
||||
return document.getMulti(domainId, document.TYPE_PROBLEM, query);
|
||||
}
|
||||
|
||||
export function getMultiStatus(domainId: string, query: object) {
|
||||
return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query);
|
||||
}
|
||||
|
||||
export function edit(domainId: string, _id: number, $set: any): Promise<Pdoc> {
|
||||
return document.set(domainId, document.TYPE_PROBLEM, _id, $set);
|
||||
}
|
||||
|
||||
export function inc(domainId: string, _id: number, field: string, n: number): Promise<Pdoc> {
|
||||
return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n);
|
||||
}
|
||||
|
||||
export function count(domainId: string, query: any) {
|
||||
return document.count(domainId, document.TYPE_PROBLEM, query);
|
||||
}
|
||||
|
||||
export async function random(domainId: string, query: any): Promise<string | null> {
|
||||
const cursor = document.getMulti(domainId, document.TYPE_PROBLEM, query);
|
||||
const pcount = await cursor.count();
|
||||
if (pcount) {
|
||||
const pdoc = await cursor.skip(Math.floor(Math.random() * pcount)).limit(1).toArray();
|
||||
return pdoc[0].pid;
|
||||
} return null;
|
||||
}
|
||||
|
||||
export async function getList(
|
||||
domainId: string, pids: Array<number | string>,
|
||||
getHidden = false, doThrow = true,
|
||||
): Promise<Pdict> {
|
||||
pids = Array.from(new Set(pids));
|
||||
const r = {};
|
||||
const l = {};
|
||||
const q: any = { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] };
|
||||
if (!getHidden) q.hidden = false;
|
||||
const pdocs = await document.getMulti(domainId, document.TYPE_PROBLEM, q).toArray();
|
||||
for (const pdoc of pdocs) {
|
||||
r[pdoc.docId] = pdoc;
|
||||
l[pdoc.pid] = pdoc;
|
||||
}
|
||||
// TODO enhance
|
||||
if (pdocs.length !== pids.length) {
|
||||
for (const pid of pids) {
|
||||
if (!(r[pid] || l[pid])) {
|
||||
if (doThrow) {
|
||||
throw new ProblemNotFoundError(domainId, pid);
|
||||
} else {
|
||||
r[pid] = pdocHidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
export async function getListStatus(domainId: string, uid: number, pids: number[]) {
|
||||
const psdocs = await getMultiStatus(
|
||||
domainId, { uid, docId: { $in: Array.from(new Set(pids)) } },
|
||||
).toArray();
|
||||
const r: Dictionary<ProblemStatusDoc> = {};
|
||||
for (const psdoc of psdocs) r[psdoc.docId] = psdoc;
|
||||
return r;
|
||||
}
|
||||
|
||||
export async function updateStatus(
|
||||
domainId: string, pid: number, uid: number,
|
||||
rid: ObjectID, status: number,
|
||||
) {
|
||||
const res = await document.setIfNotStatus(
|
||||
domainId, document.TYPE_PROBLEM, pid, uid,
|
||||
'status', status, STATUS.STATUS_ACCEPTED, { rid },
|
||||
);
|
||||
return !!res;
|
||||
}
|
||||
|
||||
export function setStar(domainId: string, pid: number, uid: number, star: boolean) {
|
||||
return document.setStatus(domainId, document.TYPE_PROBLEM, pid, uid, { star });
|
||||
}
|
||||
|
||||
export async function setTestdata(domainId: string, _id: number, filePath: string) {
|
||||
const pdoc = await get(domainId, _id);
|
||||
const config = await testdataConfig.readConfig(filePath);
|
||||
const id = await file.add(filePath, 'data.zip', 1);
|
||||
if (pdoc.data instanceof ObjectID) file.del(pdoc.data);
|
||||
return await edit(domainId, _id, { data: id, config });
|
||||
}
|
||||
|
||||
global.Hydro.model.problem = {
|
||||
pdocHidden,
|
||||
add,
|
||||
inc,
|
||||
get,
|
||||
edit,
|
||||
count,
|
||||
random,
|
||||
getMulti,
|
||||
getList,
|
||||
getListStatus,
|
||||
getMultiStatus,
|
||||
setStar,
|
||||
setTestdata,
|
||||
updateStatus,
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { Dictionary } from 'lodash';
|
||||
import { STATUS } from './builtin';
|
||||
import * as task from './task';
|
||||
import * as problem from './problem';
|
||||
import {
|
||||
Rdoc, TestCase, PretestConfig, ContestInfo, ProblemConfig,
|
||||
} from '../interface';
|
||||
import * as db from '../service/db';
|
||||
|
||||
const coll = db.collection('record');
|
||||
|
||||
export interface RdocBase {
|
||||
_id?: ObjectID,
|
||||
domainId?: string,
|
||||
pid: number
|
||||
uid: number,
|
||||
lang: string,
|
||||
code: string,
|
||||
score?: number,
|
||||
memory?: number,
|
||||
time?: number,
|
||||
judgeTexts?: string[],
|
||||
compilerTexts?: string[],
|
||||
testCases?: TestCase[],
|
||||
rejudged?: boolean,
|
||||
judger?: string,
|
||||
judgeAt?: Date,
|
||||
status?: number,
|
||||
type?: string,
|
||||
hidden?: boolean,
|
||||
input?: string,
|
||||
tid?: ObjectID,
|
||||
ttype?: number,
|
||||
}
|
||||
|
||||
export interface JudgeTask {
|
||||
_id: ObjectID,
|
||||
rid: ObjectID,
|
||||
domainId: string,
|
||||
pid: number
|
||||
lang: string,
|
||||
code: string,
|
||||
data?: ObjectID,
|
||||
config: string,
|
||||
type?: string,
|
||||
}
|
||||
|
||||
export async function get(domainId: string, _id: ObjectID): Promise<Rdoc | null> {
|
||||
const res = await coll.findOne({ _id });
|
||||
if (res && res.domainId === domainId) return res;
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function judge(domainId: string, rid: ObjectID) {
|
||||
const rdoc = await get(domainId, rid);
|
||||
let config: ProblemConfig = rdoc.config;
|
||||
if (!config) {
|
||||
const pdoc = await problem.get(domainId, rdoc.pid);
|
||||
config = pdoc?.config || {};
|
||||
}
|
||||
await task.add({
|
||||
...rdoc,
|
||||
type: 'judge',
|
||||
rid,
|
||||
domainId,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
export async function add(
|
||||
domainId: string, pid: number, uid: number,
|
||||
lang: string, code: string, addTask: boolean, contestOrConfig?: ContestInfo | PretestConfig,
|
||||
) {
|
||||
const data: Rdoc = {
|
||||
status: STATUS.STATUS_WAITING,
|
||||
_id: new ObjectID(),
|
||||
uid,
|
||||
code,
|
||||
lang,
|
||||
pid,
|
||||
domainId,
|
||||
score: 0,
|
||||
time: 0,
|
||||
memory: 0,
|
||||
hidden: !!contestOrConfig,
|
||||
judgeTexts: [],
|
||||
compilerTexts: [],
|
||||
testCases: [],
|
||||
judger: null,
|
||||
judgeAt: null,
|
||||
rejudged: false,
|
||||
};
|
||||
if (contestOrConfig) {
|
||||
if ((contestOrConfig as ContestInfo).type) {
|
||||
data.contest = contestOrConfig as ContestInfo;
|
||||
} else {
|
||||
data.config = contestOrConfig as PretestConfig;
|
||||
}
|
||||
}
|
||||
const res = await coll.insertOne(data);
|
||||
if (addTask) await judge(domainId, res.insertedId);
|
||||
return res.insertedId;
|
||||
}
|
||||
|
||||
export function getMulti(domainId: string, query: any) {
|
||||
return coll.find({ ...query, domainId });
|
||||
}
|
||||
|
||||
export async function update(
|
||||
domainId: string, _id: ObjectID,
|
||||
$set: any = {}, $push: any = {}, $unset: any = {},
|
||||
): Promise<Rdoc | null> {
|
||||
const $update: any = {};
|
||||
if ($set && Object.keys($set).length) $update.$set = $set;
|
||||
if ($push && Object.keys($push).length) $update.$push = $push;
|
||||
if ($unset && Object.keys($unset).length) $update.$unset = $unset;
|
||||
if (Object.keys($update).length) {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ _id, domainId },
|
||||
$update,
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
return await get(domainId, _id);
|
||||
}
|
||||
|
||||
export function reset(domainId: string, rid: ObjectID, isRejudge: boolean) {
|
||||
const upd: any = {
|
||||
score: 0,
|
||||
status: STATUS.STATUS_WAITING,
|
||||
time: 0,
|
||||
memory: 0,
|
||||
testCases: [],
|
||||
judgeTexts: [],
|
||||
compilerTexts: [],
|
||||
judgeAt: null,
|
||||
judger: null,
|
||||
};
|
||||
if (isRejudge) upd.rejudged = true;
|
||||
return update(domainId, rid, upd);
|
||||
}
|
||||
|
||||
export function count(domainId: string, query: any) {
|
||||
return coll.find({ domainId, ...query }).count();
|
||||
}
|
||||
|
||||
export async function getList(
|
||||
domainId: string, rids: ObjectID[], showHidden: boolean,
|
||||
): Promise<Dictionary<Rdoc>> {
|
||||
const r = {};
|
||||
rids = Array.from(new Set(rids));
|
||||
const rdocs = await coll.find({ domainId, _id: { $in: rids } }).toArray();
|
||||
for (const rdoc of rdocs) {
|
||||
if (rdoc.hidden && !showHidden) r[rdoc._id.toHexString()] = null;
|
||||
else r[rdoc._id.toHexString()] = rdoc;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
export function getUserInProblemMulti(
|
||||
domainId: string, uid: number, pid: number,
|
||||
getHidden = false,
|
||||
) {
|
||||
if (!getHidden) {
|
||||
return coll.find({
|
||||
domainId, uid, pid, hidden: false,
|
||||
});
|
||||
}
|
||||
return coll.find({ domainId, uid, pid });
|
||||
}
|
||||
|
||||
export function getByUid(domainId: string, uid: number, limit: number): Promise<Rdoc[]> {
|
||||
return coll.find({ domainId, uid }).limit(limit).toArray();
|
||||
}
|
||||
|
||||
global.Hydro.model.record = {
|
||||
add,
|
||||
get,
|
||||
getMulti,
|
||||
update,
|
||||
count,
|
||||
reset,
|
||||
getList,
|
||||
getUserInProblemMulti,
|
||||
getByUid,
|
||||
judge,
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import moment from 'moment-timezone';
|
||||
import { Dictionary } from 'lodash';
|
||||
import * as builtin from './builtin';
|
||||
import { Setting as _Setting } from '../interface';
|
||||
|
||||
type SettingDict = Dictionary<_Setting>;
|
||||
|
||||
const countries = moment.tz.countries();
|
||||
const tzs = new Set();
|
||||
for (const country of countries) {
|
||||
const tz = moment.tz.zonesForCountry(country);
|
||||
for (const t of tz) tzs.add(t);
|
||||
}
|
||||
const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]) as [string, string][];
|
||||
export const langRange: Dictionary<string> = {};
|
||||
|
||||
for (const lang in global.Hydro.locales) {
|
||||
langRange[lang] = global.Hydro.locales[lang].__langname;
|
||||
}
|
||||
|
||||
export const FLAG_HIDDEN = 1;
|
||||
export const FLAG_DISABLED = 2;
|
||||
export const FLAG_SECRET = 4;
|
||||
|
||||
export const PREFERENCE_SETTINGS: _Setting[] = [];
|
||||
export const ACCOUNT_SETTINGS: _Setting[] = [];
|
||||
export const DOMAIN_SETTINGS: _Setting[] = [];
|
||||
export const DOMAIN_USER_SETTINGS: _Setting[] = [];
|
||||
export const SYSTEM_SETTINGS: _Setting[] = [];
|
||||
export const SETTINGS: _Setting[] = [];
|
||||
export const SETTINGS_BY_KEY: SettingDict = {};
|
||||
export const DOMAIN_USER_SETTINGS_BY_KEY: SettingDict = {};
|
||||
export const DOMAIN_SETTINGS_BY_KEY: SettingDict = {};
|
||||
export const SYSTEM_SETTINGS_BY_KEY: SettingDict = {};
|
||||
|
||||
export const Setting = (
|
||||
family: string, key: string, range: Array<[string, string]> | Dictionary<string> = null,
|
||||
value: any = null, type = 'text', name = '',
|
||||
desc = '', flag = 0,
|
||||
): _Setting => ({
|
||||
family, key, range, value, type, name, desc, flag,
|
||||
});
|
||||
|
||||
export const PreferenceSetting = (...settings: _Setting[]) => {
|
||||
for (const setting of settings) {
|
||||
PREFERENCE_SETTINGS.push(setting);
|
||||
SETTINGS.push(setting);
|
||||
SETTINGS_BY_KEY[setting.key] = setting;
|
||||
}
|
||||
};
|
||||
export const AccountSetting = (...settings: _Setting[]) => {
|
||||
for (const setting of settings) {
|
||||
ACCOUNT_SETTINGS.push(setting);
|
||||
SETTINGS.push(setting);
|
||||
SETTINGS_BY_KEY[setting.key] = setting;
|
||||
}
|
||||
};
|
||||
export const DomainUserSetting = (...settings: _Setting[]) => {
|
||||
for (const setting of settings) {
|
||||
DOMAIN_USER_SETTINGS.push(setting);
|
||||
DOMAIN_USER_SETTINGS_BY_KEY[setting.key] = setting;
|
||||
}
|
||||
};
|
||||
export const DomainSetting = (...settings: _Setting[]) => {
|
||||
for (const setting of settings) {
|
||||
DOMAIN_SETTINGS.push(setting);
|
||||
DOMAIN_SETTINGS_BY_KEY[setting.key] = setting;
|
||||
}
|
||||
};
|
||||
export const SystemSetting = (...settings: _Setting[]) => {
|
||||
for (const setting of settings) {
|
||||
SYSTEM_SETTINGS.push(setting);
|
||||
SYSTEM_SETTINGS_BY_KEY[setting.key] = setting;
|
||||
}
|
||||
};
|
||||
|
||||
PreferenceSetting(
|
||||
Setting('setting_display', 'viewLang', langRange, 'zh_CN', 'select', 'UI Language'),
|
||||
Setting('setting_display', 'timeZone', timezones, 'Asia/Shanghai', 'select', 'Timezone'),
|
||||
Setting('setting_usage', 'codeLang', builtin.LANG_TEXTS, 'c', 'select', 'Default Code Language'),
|
||||
Setting('setting_usage', 'codeTemplate', null, '', 'textarea', 'Default Code Template',
|
||||
'If left blank, the built-in template of the corresponding language will be used.'),
|
||||
);
|
||||
|
||||
AccountSetting(
|
||||
Setting('setting_info', 'gravatar', null, null, 'text', 'Gravatar Email',
|
||||
'We use <a href="https://en.gravatar.com/" target="_blank">Gravatar</a> to present your avatar icon.'),
|
||||
Setting('setting_info', 'qq', null, null, 'text', 'QQ'),
|
||||
Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE, builtin.USER_GENDER_OTHER, 'select', 'Gender'),
|
||||
Setting('setting_info', 'bio', null, null, 'markdown', 'Bio'),
|
||||
Setting('setting_customize', 'backgroundImage', null,
|
||||
'/components/profile/backgrounds/1.jpg', 'text', 'Profile Background Image',
|
||||
'Choose the background image in your profile page.'),
|
||||
Setting('setting_storage', 'usage', null, 0, 'number', 'Userfile Usage', null, FLAG_DISABLED | FLAG_HIDDEN),
|
||||
);
|
||||
|
||||
DomainSetting(
|
||||
Setting('setting_domain', 'name', null, 'New domain', 'text', 'name'),
|
||||
Setting('setting_domain', 'gravatar', null, '', 'text', 'gravatar', 'Will be used as the domain icon.'),
|
||||
Setting('setting_domain', 'bulletin', null, '', 'markdown', 'Bulletin'),
|
||||
Setting('setting_storage', 'pidCounter', null, 0, 'number', 'Problem ID Counter', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
);
|
||||
|
||||
DomainUserSetting(
|
||||
Setting('setting_info', 'displayName', null, null, 'string', 'display name'),
|
||||
Setting('setting_storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'rp', null, 1500, 'number', 'RP', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
);
|
||||
|
||||
SystemSetting(
|
||||
Setting('setting_smtp', 'smtp.user', null, null, 'text', 'SMTP Username'),
|
||||
Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password', null, FLAG_SECRET),
|
||||
Setting('setting_smtp', 'smtp.host', null, null, 'text', 'SMTP Server Host'),
|
||||
Setting('setting_smtp', 'smtp.port', null, 465, 'number', 'SMTP Server Port'),
|
||||
Setting('setting_smtp', 'smtp.from', null, null, 'text', 'Mail From'),
|
||||
Setting('setting_smtp', 'smtp.secure', null, false, 'boolean', 'SSL'),
|
||||
Setting('setting_server', 'server.name', null, 'Hydro', 'text', 'Server Name'),
|
||||
Setting('setting_server', 'server.worker', null, 1, 'number', 'Server Workers Number'),
|
||||
Setting('setting_server', 'server.hostname', null, null, 'text', 'Server Hostname'),
|
||||
Setting('setting_server', 'server.host', null, null, 'text', 'Server Host'),
|
||||
Setting('setting_server', 'server.url', null, null, 'text', 'Server BaseURL'),
|
||||
Setting('setting_server', 'server.cdn', null, '/', 'text', 'CDN Prefix', 'Ends with /'),
|
||||
Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'),
|
||||
Setting('setting_server', 'server.xff', null, null, 'text', 'IP Header', 'e.g. x-forwarded-for (lowercase)'),
|
||||
Setting('setting_session', 'session.keys', null, [String.random(32)], 'text', 'session.keys', null, FLAG_HIDDEN),
|
||||
Setting('setting_session', 'session.secure', null, false, 'boolean', 'session.secure'),
|
||||
Setting('setting_session', 'session.saved_expire_seconds', null, 3600 * 24 * 30, 'number', 'Saved session expire seconds'),
|
||||
Setting('setting_session', 'session.unsaved_expire_seconds', null, 3600 * 3, 'number', 'Unsaved session expire seconds'),
|
||||
Setting('setting_storage', 'db.ver', null, 1, 'number', 'Database version', null, FLAG_DISABLED | FLAG_HIDDEN),
|
||||
Setting('setting_storage', 'user', null, 1, 'number', 'User Counter', null, FLAG_DISABLED | FLAG_HIDDEN),
|
||||
);
|
||||
|
||||
global.Hydro.postInit.push(
|
||||
async () => {
|
||||
for (const setting of SYSTEM_SETTINGS) {
|
||||
if (setting.value) {
|
||||
const current = await global.Hydro.model.system.get(setting.key);
|
||||
if (current === null || current === '') {
|
||||
await global.Hydro.model.system.set(setting.key, setting.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
global.Hydro.model.setting = {
|
||||
langRange,
|
||||
Setting,
|
||||
PreferenceSetting,
|
||||
AccountSetting,
|
||||
DomainSetting,
|
||||
DomainUserSetting,
|
||||
SystemSetting,
|
||||
FLAG_HIDDEN,
|
||||
FLAG_DISABLED,
|
||||
FLAG_SECRET,
|
||||
PREFERENCE_SETTINGS,
|
||||
ACCOUNT_SETTINGS,
|
||||
SETTINGS,
|
||||
SETTINGS_BY_KEY,
|
||||
SYSTEM_SETTINGS,
|
||||
SYSTEM_SETTINGS_BY_KEY,
|
||||
DOMAIN_SETTINGS,
|
||||
DOMAIN_SETTINGS_BY_KEY,
|
||||
DOMAIN_USER_SETTINGS,
|
||||
DOMAIN_USER_SETTINGS_BY_KEY,
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import * as document from './document';
|
||||
import { SolutionNotFoundError } from '../error';
|
||||
|
||||
export function add(domainId: string, pid: number, owner: number, content: string) {
|
||||
return document.add(
|
||||
domainId, content, owner, document.TYPE_PROBLEM_SOLUTION,
|
||||
null, document.TYPE_PROBLEM, pid, { reply: [], vote: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
export async function get(domainId: string, psid: ObjectID) {
|
||||
const psdoc = await document.get(domainId, document.TYPE_PROBLEM_SOLUTION, psid);
|
||||
if (!psdoc) throw new SolutionNotFoundError();
|
||||
return psdoc;
|
||||
}
|
||||
|
||||
export function getMany(domainId: string, query: any, sort: any, page: number, limit: number) {
|
||||
return document.getMulti(domainId, document.TYPE_PROBLEM_SOLUTION, query)
|
||||
.sort(sort)
|
||||
.skip((page - 1) * limit).limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
export function edit(domainId: string, psid: ObjectID, content: string) {
|
||||
return document.set(domainId, document.TYPE_PROBLEM_SOLUTION, psid, { content });
|
||||
}
|
||||
|
||||
export function del(domainId: string, psid: ObjectID) {
|
||||
return document.deleteOne(domainId, document.TYPE_PROBLEM_SOLUTION, psid);
|
||||
}
|
||||
|
||||
export function count(domainId: string, query: any) {
|
||||
return document.count(domainId, document.TYPE_PROBLEM_SOLUTION, query);
|
||||
}
|
||||
|
||||
export function getMulti(domainId: string, pid: number) {
|
||||
return document.getMulti(
|
||||
domainId, document.TYPE_PROBLEM_SOLUTION,
|
||||
{ parentType: document.TYPE_PROBLEM, parentId: pid },
|
||||
).sort({ vote: -1 });
|
||||
}
|
||||
|
||||
export function reply(domainId: string, psid: ObjectID, owner: number, content: string) {
|
||||
return document.push(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', content, owner);
|
||||
}
|
||||
|
||||
export function getReply(domainId: string, psid: ObjectID, psrid: ObjectID) {
|
||||
return document.getSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid);
|
||||
}
|
||||
|
||||
export function editReply(domainId: string, psid: ObjectID, psrid: ObjectID, content: string) {
|
||||
return document.setSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid, { content });
|
||||
}
|
||||
|
||||
export function delReply(domainId: string, psid: ObjectID, psrid: ObjectID) {
|
||||
return document.deleteSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid);
|
||||
}
|
||||
|
||||
export async function vote(domainId: string, psid: ObjectID, uid: number, value: number) {
|
||||
let pssdoc = await document.getStatus(domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid);
|
||||
await document.setStatus(domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid, { vote: value });
|
||||
if (pssdoc) value += -pssdoc.vote;
|
||||
const psdoc = await document.inc(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'vote', value);
|
||||
pssdoc = await document.getStatus(domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid);
|
||||
return [psdoc, pssdoc];
|
||||
}
|
||||
|
||||
export async function getListStatus(domainId: string, psids: ObjectID[], uid: number) {
|
||||
const result = {};
|
||||
const res = await document.getMultiStatus(
|
||||
domainId, document.TYPE_PROBLEM_SOLUTION, { uid, psid: { $in: psids } },
|
||||
).toArray();
|
||||
for (const i of res) result[i.psid] = i;
|
||||
return result;
|
||||
}
|
||||
|
||||
global.Hydro.model.solution = {
|
||||
count,
|
||||
add,
|
||||
get,
|
||||
edit,
|
||||
del,
|
||||
getMany,
|
||||
getMulti,
|
||||
reply,
|
||||
getReply,
|
||||
editReply,
|
||||
delReply,
|
||||
vote,
|
||||
getListStatus,
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import Lru from 'lru-cache';
|
||||
import * as db from '../service/db';
|
||||
|
||||
const coll = db.collection('system');
|
||||
const cache = new Lru({
|
||||
maxAge: 5000,
|
||||
});
|
||||
|
||||
export async function get(_id: string) {
|
||||
const res = cache.get(_id);
|
||||
if (res !== undefined) return res;
|
||||
const doc = await coll.findOne({ _id });
|
||||
if (doc) {
|
||||
cache.set(_id, doc.value);
|
||||
return doc.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getMany(keys: string[]): Promise<any[]> {
|
||||
const r = [];
|
||||
let success = true;
|
||||
for (const key of keys) {
|
||||
const res = cache.get(key);
|
||||
if (res !== undefined) r.push(res);
|
||||
else success = false;
|
||||
}
|
||||
if (success) return r;
|
||||
const docs = await coll.find({ _id: { $in: keys } }).toArray();
|
||||
const dict = {};
|
||||
const res = [];
|
||||
for (const doc of docs) {
|
||||
dict[doc._id] = doc.value;
|
||||
}
|
||||
for (const key of keys) {
|
||||
res.push(dict[key] || null);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function set(_id: string, value: any) {
|
||||
cache.set(_id, value);
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ _id },
|
||||
{ $set: { value } },
|
||||
{ upsert: true, returnOriginal: false },
|
||||
);
|
||||
return res.value.value;
|
||||
}
|
||||
|
||||
export async function inc(_id: string) {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ _id },
|
||||
{ $inc: { value: 1 } },
|
||||
{ upsert: true, returnOriginal: false },
|
||||
);
|
||||
cache.set(_id, res.value.value);
|
||||
return res.value.value;
|
||||
}
|
||||
|
||||
global.Hydro.model.system = {
|
||||
get,
|
||||
getMany,
|
||||
inc,
|
||||
set,
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import moment from 'moment-timezone';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import * as db from '../service/db';
|
||||
|
||||
const coll = db.collection('task');
|
||||
|
||||
export async function add(task: any) {
|
||||
const t = { ...task };
|
||||
if (typeof t.executeAfter === 'object') t.executeAfter = t.executeAfter.getTime();
|
||||
t.count = t.count || 1;
|
||||
t.executeAfter = t.executeAfter || new Date().getTime();
|
||||
const res = await coll.insertOne(t);
|
||||
return res.insertedId;
|
||||
}
|
||||
|
||||
export function get(_id: ObjectID) {
|
||||
return coll.findOne({ _id });
|
||||
}
|
||||
|
||||
export function count(query: any) {
|
||||
return coll.find(query).count();
|
||||
}
|
||||
|
||||
export function del(_id: ObjectID) {
|
||||
return coll.deleteOne({ _id });
|
||||
}
|
||||
|
||||
export async function getFirst(query: any) {
|
||||
const q = { ...query };
|
||||
q.executeAfter = q.executeAfter || { $lt: new Date().getTime() };
|
||||
const res = await coll.findOneAndDelete(q);
|
||||
if (res.value) {
|
||||
if (res.value.interval) {
|
||||
await coll.insertOne({
|
||||
...res.value, executeAfter: moment().add(...res.value.interval).toDate(),
|
||||
});
|
||||
}
|
||||
return res.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function consume(query: any, cb: Function) {
|
||||
let isRunning = false;
|
||||
const interval = setInterval(async () => {
|
||||
if (isRunning) return;
|
||||
isRunning = true;
|
||||
const res = await getFirst(query);
|
||||
if (res) {
|
||||
try {
|
||||
await cb(res);
|
||||
} catch (e) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
isRunning = false;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export class Consumer {
|
||||
consuming: boolean;
|
||||
|
||||
running?: Promise<any>;
|
||||
|
||||
interval: NodeJS.Timeout;
|
||||
|
||||
filter: any;
|
||||
|
||||
func: Function;
|
||||
|
||||
constructor(filter: any, func: Function) {
|
||||
this.consuming = true;
|
||||
this.filter = filter;
|
||||
this.func = func;
|
||||
this.interval = setInterval(this.consume.bind(this), 100);
|
||||
}
|
||||
|
||||
async consume() {
|
||||
if (this.running || !this.consuming) return;
|
||||
const res = await getFirst(this.filter);
|
||||
if (res) {
|
||||
this.running = this.func(res);
|
||||
if (this.running instanceof Promise) await this.running;
|
||||
this.running = null;
|
||||
}
|
||||
}
|
||||
|
||||
async destory() {
|
||||
this.consuming = false;
|
||||
clearInterval(this.interval);
|
||||
if (this.running) await this.running;
|
||||
}
|
||||
}
|
||||
|
||||
global.Hydro.model.task = {
|
||||
Consumer, add, get, del, count, getFirst, consume,
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import { TokenDoc } from '../interface';
|
||||
import * as db from '../service/db';
|
||||
|
||||
const coll = db.collection('token');
|
||||
|
||||
export const TYPE_SESSION = 0;
|
||||
export const TYPE_REGISTRATION = 2;
|
||||
export const TYPE_CHANGEMAIL = 3;
|
||||
export const TYPE_OAUTH = 4;
|
||||
export const TYPE_LOSTPASS = 5;
|
||||
|
||||
function ensureIndexes() {
|
||||
return Promise.all([
|
||||
coll.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true }),
|
||||
coll.createIndex('expireAt', { expireAfterSeconds: 0 }),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function add(
|
||||
tokenType: number, expireSeconds: number, data: any,
|
||||
): Promise<[string, TokenDoc]> {
|
||||
const now = new Date();
|
||||
const str = String.random(32);
|
||||
const res = await coll.insertOne({
|
||||
...data,
|
||||
_id: str,
|
||||
tokenType,
|
||||
createAt: now,
|
||||
updateAt: now,
|
||||
expireAt: new Date(now.getTime() + expireSeconds * 1000),
|
||||
});
|
||||
return [str, res.ops[0]];
|
||||
}
|
||||
|
||||
export async function get(tokenId: string, tokenType: number): Promise<TokenDoc | null> {
|
||||
return await coll.findOne({ _id: tokenId, tokenType });
|
||||
}
|
||||
|
||||
export async function update(
|
||||
tokenId: string, tokenType: number, expireSeconds: number,
|
||||
data: object,
|
||||
) {
|
||||
const now = new Date();
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ _id: tokenId, tokenType },
|
||||
{
|
||||
$set: {
|
||||
...data,
|
||||
updateAt: now,
|
||||
expireAt: new Date(now.getTime() + expireSeconds * 1000),
|
||||
tokenType,
|
||||
},
|
||||
},
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export async function del(tokenId: string, tokenType: number) {
|
||||
const result = await coll.deleteOne({ _id: tokenId, tokenType });
|
||||
return !!result.deletedCount;
|
||||
}
|
||||
|
||||
export async function createOrUpdate(
|
||||
tokenType: number, expireSeconds: number, data: any,
|
||||
): Promise<string> {
|
||||
const d = await coll.findOne({ tokenType, ...data });
|
||||
if (!d) {
|
||||
const res = await add(tokenType, expireSeconds, data);
|
||||
return res[0];
|
||||
}
|
||||
await update(d._id, tokenType, expireSeconds, data);
|
||||
return d._id;
|
||||
}
|
||||
|
||||
export function getSessionListByUid(uid: number) {
|
||||
return coll.find({ uid, tokenType: TYPE_SESSION }).sort('updateAt', -1).toArray();
|
||||
}
|
||||
|
||||
export function getMostRecentSessionByUid(uid: number) {
|
||||
return coll.findOne({ uid, tokenType: TYPE_SESSION }, { sort: { updateAt: -1 } });
|
||||
}
|
||||
|
||||
export function delByUid(uid: number) {
|
||||
return coll.deleteMany({ uid });
|
||||
}
|
||||
|
||||
global.Hydro.postInit.push(ensureIndexes);
|
||||
global.Hydro.model.token = {
|
||||
TYPE_SESSION,
|
||||
TYPE_CHANGEMAIL,
|
||||
TYPE_OAUTH,
|
||||
TYPE_REGISTRATION,
|
||||
TYPE_LOSTPASS,
|
||||
|
||||
add,
|
||||
createOrUpdate,
|
||||
get,
|
||||
update,
|
||||
del,
|
||||
delByUid,
|
||||
getMostRecentSessionByUid,
|
||||
getSessionListByUid,
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import * as document from './document';
|
||||
import { TrainingNotFoundError, TrainingAlreadyEnrollError } from '../error';
|
||||
|
||||
export function getStatus(domainId: string, tid: ObjectID, uid: number) {
|
||||
return document.getStatus(domainId, document.TYPE_TRAINING, tid, uid);
|
||||
}
|
||||
|
||||
export function getMultiStatus(domainId: string, query: any) {
|
||||
return document.getMultiStatus(domainId, document.TYPE_TRAINING, query);
|
||||
}
|
||||
|
||||
export async function getListStatus(domainId: string, uid: number, tids: ObjectID[]) {
|
||||
const tsdocs = await getMultiStatus(
|
||||
domainId, { uid, tid: { $in: Array.from(new Set(tids)) } },
|
||||
).toArray();
|
||||
const r = {};
|
||||
for (const tsdoc of tsdocs) r[tsdoc.pid] = tsdoc;
|
||||
return r;
|
||||
}
|
||||
|
||||
export async function enroll(domainId: string, tid: ObjectID, uid: number) {
|
||||
try {
|
||||
await document.setStatus(domainId, document.TYPE_TRAINING, tid, uid, { enroll: 1 });
|
||||
} catch (e) {
|
||||
throw new TrainingAlreadyEnrollError(tid, uid);
|
||||
}
|
||||
return await document.inc(domainId, document.TYPE_TRAINING, tid, 'enroll', 1);
|
||||
}
|
||||
|
||||
export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: any) {
|
||||
return document.setStatus(domainId, document.TYPE_TRAINING, tid, uid, $set);
|
||||
}
|
||||
|
||||
export function add(
|
||||
domainId: string, title: string, content: string,
|
||||
owner: number, dag = [], description = '',
|
||||
) {
|
||||
return document.add(domainId, content, owner, document.TYPE_TRAINING, null, null, null, {
|
||||
dag,
|
||||
title,
|
||||
description,
|
||||
enroll: 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function edit(domainId: string, tid: ObjectID, $set) {
|
||||
return document.set(domainId, document.TYPE_TRAINING, tid, $set);
|
||||
}
|
||||
|
||||
export function getPids(tdoc): number[] {
|
||||
const pids: Set<number> = new Set();
|
||||
for (const node of tdoc.dag) {
|
||||
for (const pid of node.pids) pids.add(pid);
|
||||
}
|
||||
return Array.from(pids);
|
||||
}
|
||||
|
||||
export function isDone(node, doneNids, donePids) {
|
||||
return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids))
|
||||
&& Set.isSuperset(new Set(donePids), new Set(node.pids)));
|
||||
}
|
||||
|
||||
export function isProgress(node, doneNids, donePids, progPids) {
|
||||
return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids))
|
||||
&& !Set.isSuperset(new Set(donePids), new Set(node.pids))
|
||||
&& Set.intersection(
|
||||
Set.union(new Set(donePids), new Set(progPids)),
|
||||
new Set(node.pids),
|
||||
).size);
|
||||
}
|
||||
|
||||
export function isOpen(node, doneNids, donePids, progPids) {
|
||||
return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids))
|
||||
&& !Set.isSuperset(new Set(donePids), new Set(node.pids))
|
||||
&& !Set.intersection(
|
||||
Set.union(new Set(donePids), new Set(progPids)),
|
||||
new Set(node.pids),
|
||||
).size);
|
||||
}
|
||||
|
||||
export const isInvalid = (node, doneNids) =>
|
||||
!Set.isSuperset(new Set(doneNids), new Set(node.requireNids));
|
||||
|
||||
export const count = (domainId: string, query: any) =>
|
||||
document.count(domainId, document.TYPE_TRAINING, query);
|
||||
|
||||
export async function get(domainId, tid) {
|
||||
const tdoc = await document.get(domainId, document.TYPE_TRAINING, tid);
|
||||
if (!tdoc) throw new TrainingNotFoundError(tid);
|
||||
for (const i in tdoc.dag) {
|
||||
for (const j in tdoc.dag[i].pids) {
|
||||
if (Number.isSafeInteger(parseInt(tdoc.dag[i].pids[j], 10))) {
|
||||
tdoc.dag[i].pids[j] = parseInt(tdoc.dag[i].pids[j], 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tdoc;
|
||||
}
|
||||
|
||||
export async function getList(domainId: string, tids: ObjectID[]) {
|
||||
const tdocs = await this.getMulti(
|
||||
domainId, { _id: { $in: Array.from(new Set(tids)) } },
|
||||
).toArray();
|
||||
const r = {};
|
||||
for (const tdoc of tdocs) r[tdoc.docId] = tdoc;
|
||||
return r;
|
||||
}
|
||||
|
||||
export const getMulti = (domainId: string, query: any = {}) =>
|
||||
document.getMulti(domainId, document.TYPE_TRAINING, query).sort('_id', 1);
|
||||
|
||||
global.Hydro.model.training = {
|
||||
getPids,
|
||||
isDone,
|
||||
isProgress,
|
||||
isOpen,
|
||||
isInvalid,
|
||||
add,
|
||||
edit,
|
||||
count,
|
||||
get,
|
||||
getList,
|
||||
getMulti,
|
||||
getMultiStatus,
|
||||
getStatus,
|
||||
enroll,
|
||||
setStatus,
|
||||
getListStatus,
|
||||
};
|
||||
@@ -0,0 +1,252 @@
|
||||
import * as system from './system';
|
||||
import * as token from './token';
|
||||
import * as setting from './setting';
|
||||
import * as domain from './domain';
|
||||
import { BUILTIN_USERS, PRIV } from './builtin';
|
||||
import { UserNotFoundError, UserAlreadyExistError, LoginError } from '../error';
|
||||
import pwhash from '../lib/hash.hydro';
|
||||
import * as db from '../service/db';
|
||||
import { User as _User, Udoc, Udict } from '../interface';
|
||||
|
||||
const coll = db.collection('user');
|
||||
|
||||
export async function setPassword(uid: number, password: string): Promise<Udoc> {
|
||||
const salt = String.random();
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ _id: uid },
|
||||
{ $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return res.value;
|
||||
}
|
||||
|
||||
export class User implements _User {
|
||||
udoc: () => any;
|
||||
|
||||
dudoc: () => any;
|
||||
|
||||
_id: number;
|
||||
|
||||
mail: string;
|
||||
|
||||
uname: string;
|
||||
|
||||
salt: () => string;
|
||||
|
||||
hash: () => string;
|
||||
|
||||
hashType: string;
|
||||
|
||||
priv: number;
|
||||
|
||||
regat: Date;
|
||||
|
||||
loginat: Date;
|
||||
|
||||
perm: () => bigint;
|
||||
|
||||
role: string;
|
||||
|
||||
regip: () => string;
|
||||
|
||||
loginip: () => string;
|
||||
|
||||
[key: string]: any;
|
||||
|
||||
constructor(udoc, dudoc) {
|
||||
this.udoc = () => udoc;
|
||||
this.dudoc = () => dudoc;
|
||||
this._id = udoc._id;
|
||||
this.mail = udoc.mail;
|
||||
this.uname = udoc.uname;
|
||||
this.salt = () => udoc.salt;
|
||||
this.hash = () => udoc.hash;
|
||||
this.hashType = udoc.hashType || 'hydro';
|
||||
this.priv = udoc.priv;
|
||||
this.regat = udoc.regat;
|
||||
this.regip = () => udoc.regip;
|
||||
this.loginat = udoc.loginat;
|
||||
this.loginip = () => udoc.loginip;
|
||||
this.perm = () => dudoc.perm;
|
||||
this.role = dudoc.role || 'default';
|
||||
|
||||
for (const key in setting.SETTINGS_BY_KEY) {
|
||||
if (udoc[key]) this[key] = udoc[key];
|
||||
else if (setting.SETTINGS_BY_KEY[key].value !== null) {
|
||||
this[key] = setting.SETTINGS_BY_KEY[key].value;
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in setting.DOMAIN_USER_SETTINGS_BY_KEY) {
|
||||
if (dudoc[key]) this[key] = dudoc[key];
|
||||
else if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value !== null) {
|
||||
this[key] = setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasPerm(...perm: bigint[]) {
|
||||
for (const i in perm) {
|
||||
if ((this.perm() & perm[i]) === perm[i]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
hasPriv(...priv: number[]) {
|
||||
for (const i in priv) {
|
||||
if ((this.priv & priv[i]) === priv[i]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
checkPassword(password: string) {
|
||||
const h = global.Hydro.lib[`hash.${this.hashType}`];
|
||||
if (!h) throw new Error('Unknown hash method');
|
||||
if (!(h(password, this.salt(), this) === this.hash())) {
|
||||
throw new LoginError(this.uname);
|
||||
} else if (this.hashType !== 'hydro') {
|
||||
setPassword(this._id, password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getById(domainId: string, _id: number): Promise<User | null> {
|
||||
const udoc = _id === 0 || _id === 1
|
||||
? BUILTIN_USERS[_id]
|
||||
: await coll.findOne({ _id });
|
||||
if (!udoc) return null;
|
||||
const dudoc = await domain.getDomainUser(domainId, udoc);
|
||||
return new User(udoc, dudoc);
|
||||
}
|
||||
|
||||
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);
|
||||
return r;
|
||||
}
|
||||
|
||||
export async function getByUname(domainId: string, uname: string): Promise<User | null> {
|
||||
const unameLower = uname.trim().toLowerCase();
|
||||
const udoc = (unameLower === 'guest')
|
||||
? BUILTIN_USERS[0]
|
||||
: unameLower === 'hydro'
|
||||
? BUILTIN_USERS[1]
|
||||
: await coll.findOne({ unameLower });
|
||||
if (!udoc) return null;
|
||||
const dudoc = await domain.getDomainUser(domainId, udoc);
|
||||
return new User(udoc, dudoc);
|
||||
}
|
||||
|
||||
export async function getByEmail(domainId: string, mail: string): Promise<User | null> {
|
||||
const mailLower = mail.trim().toLowerCase();
|
||||
const udoc = (mailLower === 'guest@hydro.local')
|
||||
? BUILTIN_USERS[0]
|
||||
: mailLower === 'hydro@hydro.local'
|
||||
? BUILTIN_USERS[1]
|
||||
: await coll.findOne({ mailLower });
|
||||
if (!udoc) return null;
|
||||
const dudoc = await domain.getDomainUser(domainId, udoc);
|
||||
return new User(udoc, dudoc);
|
||||
}
|
||||
|
||||
export function setById(uid: number, $set = {}, $unset = {}) {
|
||||
const op: any = {};
|
||||
if ($set && Object.keys($set).length) op.$set = $set;
|
||||
if ($unset && Object.keys($unset).length) op.$unset = $unset;
|
||||
return coll.findOneAndUpdate({ _id: uid }, op);
|
||||
}
|
||||
|
||||
export function setEmail(uid: number, mail: string) {
|
||||
return setById(uid, { mail, mailLower: mail.trim().toLowerCase() });
|
||||
}
|
||||
|
||||
export async function inc(_id: number, field: string, n = 1) {
|
||||
const udoc = await coll.findOne({ _id });
|
||||
if (!udoc) throw new UserNotFoundError(_id);
|
||||
udoc[field] = udoc[field] + n || n;
|
||||
await coll.updateOne({ _id }, { $set: { [field]: udoc[field] } });
|
||||
return udoc;
|
||||
}
|
||||
|
||||
export async function create(
|
||||
mail: string, uname: string, password: string,
|
||||
uid: number, regip = '127.0.0.1', priv = PRIV.PRIV_DEFAULT,
|
||||
) {
|
||||
const salt = String.random();
|
||||
if (!uid) uid = await system.inc('user');
|
||||
try {
|
||||
await coll.insertOne({
|
||||
_id: uid,
|
||||
mail,
|
||||
mailLower: mail.trim().toLowerCase(),
|
||||
uname,
|
||||
unameLower: uname.trim().toLowerCase(),
|
||||
hash: pwhash(password, salt),
|
||||
salt,
|
||||
hashType: 'hydro',
|
||||
regat: new Date(),
|
||||
regip,
|
||||
loginat: new Date(),
|
||||
loginip: regip,
|
||||
priv,
|
||||
gravatar: mail,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new UserAlreadyExistError([uid, uname, mail]);
|
||||
}
|
||||
return uid;
|
||||
}
|
||||
|
||||
export function getMulti(params: any) {
|
||||
return coll.find(params);
|
||||
}
|
||||
|
||||
export async function getPrefixList(prefix: string, limit = 50) {
|
||||
prefix = prefix.toLowerCase();
|
||||
const $regex = new RegExp(`\\A\\Q${prefix}\\E`, 'gmi');
|
||||
const udocs = await coll.find({ unameLower: { $regex } }).limit(limit).toArray();
|
||||
return udocs;
|
||||
}
|
||||
|
||||
export async function setPriv(uid: number, priv: number): Promise<Udoc | null> {
|
||||
const udoc = await coll.findOneAndUpdate(
|
||||
{ _id: uid },
|
||||
{ $set: { priv } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
return udoc.value;
|
||||
}
|
||||
|
||||
export function ban(uid: number) {
|
||||
return Promise.all([
|
||||
setPriv(uid, PRIV.PRIV_NONE),
|
||||
token.delByUid(uid),
|
||||
]);
|
||||
}
|
||||
|
||||
function ensureIndexes() {
|
||||
return Promise.all([
|
||||
coll.createIndex('unameLower', { unique: true }),
|
||||
coll.createIndex('mailLower', { sparse: true }),
|
||||
]);
|
||||
}
|
||||
|
||||
global.Hydro.postInit.push(ensureIndexes);
|
||||
global.Hydro.model.user = {
|
||||
User,
|
||||
create,
|
||||
getByEmail,
|
||||
getById,
|
||||
getByUname,
|
||||
getMulti,
|
||||
inc,
|
||||
setById,
|
||||
setEmail,
|
||||
setPassword,
|
||||
getPrefixList,
|
||||
setPriv,
|
||||
getList,
|
||||
ban,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
export default function load() {
|
||||
let f = path.resolve(process.cwd(), 'config.json');
|
||||
if (!fs.existsSync(f)) f = path.resolve(__dirname, 'config.json');
|
||||
if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.config', 'hydro', 'config.json');
|
||||
if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.hydro', 'config.json');
|
||||
if (!fs.existsSync(f)) f = path.resolve('/config/config.json');
|
||||
if (!fs.existsSync(f)) return null;
|
||||
let result: any = {};
|
||||
try {
|
||||
result = JSON.parse(fs.readFileSync(f).toString());
|
||||
} catch (e) {
|
||||
console.error('Cannot read config file', e);
|
||||
result = {};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import { ObjectID } from 'mongodb';
|
||||
import * as document from '../model/document';
|
||||
import * as discussion from '../model/discussion';
|
||||
import * as user from '../model/user';
|
||||
import * as blacklist from '../model/blacklist';
|
||||
import * as db from '../service/db';
|
||||
|
||||
export const description = 'Add blacklist by ip, uid';
|
||||
|
||||
async function _address(
|
||||
ip: string,
|
||||
bset: Set<string>, uset: Set<number>, dset: Set<ObjectID>,
|
||||
dryrun: boolean, report: Function,
|
||||
) {
|
||||
if (bset.has(ip)) return;
|
||||
bset.add(ip);
|
||||
report({ message: `ip ${ip}` });
|
||||
const users = await db.collection('user').find({ loginip: ip }).toArray();
|
||||
const tasks = [];
|
||||
for (const udoc of users) {
|
||||
tasks.push(_user(udoc._id, bset, uset, dset, dryrun, report));
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
if (!dryrun) await blacklist.add(ip);
|
||||
}
|
||||
|
||||
async function _discussion(
|
||||
domainId: string, did: ObjectID,
|
||||
bset: Set<string>, uset: Set<number>, dset: Set<ObjectID>,
|
||||
dryrun: boolean, report: Function,
|
||||
) {
|
||||
if (dset.has(did)) return;
|
||||
dset.add(did);
|
||||
const ddoc = await discussion.get(domainId, did);
|
||||
if (!ddoc) return;
|
||||
report({ message: `discussion ${ddoc.title}` });
|
||||
await _user(ddoc.owner, bset, uset, dset, dryrun, report);
|
||||
if (ddoc.ip) await _address(ddoc.ip, bset, uset, dset, dryrun, report);
|
||||
if (!dryrun) await discussion.del(domainId, ddoc.docId);
|
||||
}
|
||||
|
||||
async function _user(
|
||||
uid: number,
|
||||
bset: Set<string>, uset: Set<number>, dset: Set<ObjectID>,
|
||||
dryrun: boolean, report: Function,
|
||||
) {
|
||||
if (uset.has(uid)) return;
|
||||
uset.add(uid);
|
||||
const udoc = await user.getById('system', uid);
|
||||
if (!udoc) return;
|
||||
report({ message: `user ${udoc._id} ${udoc.uname}` });
|
||||
await _address(udoc.loginip(), bset, uset, dset, dryrun, report);
|
||||
const ddocs = await db.collection('document').find({ docType: document.TYPE_DISCUSSION })
|
||||
.sort({ domainId: 1, docId: 1 }).toArray();
|
||||
const tasks = [];
|
||||
for (const ddoc of ddocs) {
|
||||
tasks.push(_discussion(ddoc.domainId, ddoc.docId, bset, uset, dset, dryrun, report));
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
if (!dryrun) await user.ban(uid);
|
||||
}
|
||||
|
||||
export async function run({
|
||||
// eslint-disable-next-line no-shadow
|
||||
address = null, discuss = null, user = null, dryrun = true,
|
||||
}, report) {
|
||||
if (address) await _address(address, new Set(), new Set(), new Set(), dryrun, report);
|
||||
if (discuss) {
|
||||
await _discussion(
|
||||
discuss.domainId, new ObjectID(discuss.did),
|
||||
new Set(), new Set(), new Set(), dryrun, report,
|
||||
);
|
||||
}
|
||||
if (user) await _user(user, new Set(), new Set(), new Set(), dryrun, report);
|
||||
}
|
||||
|
||||
export const validate = {
|
||||
$or: {
|
||||
address: 'string',
|
||||
discuss: ObjectID,
|
||||
user: 'number',
|
||||
},
|
||||
dryrun: 'boolean',
|
||||
};
|
||||
|
||||
global.Hydro.script.blacklist = { run, description, validate };
|
||||
@@ -0,0 +1,94 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import * as user from '../model/user';
|
||||
import * as document from '../model/document';
|
||||
import * as setting from '../model/setting';
|
||||
import * as db from '../service/db';
|
||||
import paginate from '../lib/paginate';
|
||||
import { STATUS, PRIV } from '../model/builtin';
|
||||
|
||||
export const description = 'Delete a user';
|
||||
|
||||
const collDocument = db.collection('document');
|
||||
const collDomainUser = db.collection('domain.user');
|
||||
const collRecord = db.collection('record');
|
||||
|
||||
const $unset = {
|
||||
regip: '', regat: '', loginat: '', loginip: '',
|
||||
};
|
||||
for (const s of setting.PREFERENCE_SETTINGS) {
|
||||
$unset[s.key] = '';
|
||||
}
|
||||
|
||||
async function deleteRecord(uid: number) {
|
||||
const [, pcount] = await paginate(
|
||||
collRecord.find(),
|
||||
1,
|
||||
100,
|
||||
);
|
||||
for (let i = 1; i <= pcount; i++) {
|
||||
const [rdocs] = await paginate(
|
||||
collRecord.find(),
|
||||
1,
|
||||
100,
|
||||
);
|
||||
const tasks = [];
|
||||
for (const rdoc of rdocs) {
|
||||
if (rdoc.pid) {
|
||||
tasks.push(document.inc(rdoc.domainId, document.TYPE_PROBLEM, rdoc.pid, 'nSubmit', -1));
|
||||
}
|
||||
if (rdoc.status === STATUS.STATUS_ACCEPTED) {
|
||||
tasks.push(document.inc(rdoc.domainId, document.TYPE_PROBLEM, rdoc.pid, 'nAccept', -1));
|
||||
}
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
await collRecord.deleteMany({ uid });
|
||||
}
|
||||
|
||||
async function deleteDiscussion(uid: number) {
|
||||
const [, pcount] = await paginate(
|
||||
collDocument.find({ docType: document.TYPE_DISCUSSION, owner: uid }),
|
||||
1,
|
||||
100,
|
||||
);
|
||||
for (let i = 1; i <= pcount; i++) {
|
||||
const [ddocs] = await paginate(
|
||||
collDocument.find({ docType: document.TYPE_DISCUSSION, owner: uid }),
|
||||
1,
|
||||
100,
|
||||
);
|
||||
const tasks = [];
|
||||
for (const ddoc of ddocs) {
|
||||
tasks.push(document.deleteMulti(ddoc.domainId, document.TYPE_DISCUSSION_REPLY, {
|
||||
parentId: ddoc.docId,
|
||||
parentType: document.TYPE_DISCUSSION,
|
||||
}));
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
await collDocument.deleteMany({ docType: document.TYPE_DISCUSSION, owner: uid });
|
||||
// TODO delete replies
|
||||
}
|
||||
|
||||
export async function run({
|
||||
uid,
|
||||
}) {
|
||||
await deleteRecord(uid);
|
||||
await deleteDiscussion(uid);
|
||||
await collDomainUser.deleteMany({ uid });
|
||||
const uname = String.random(32);
|
||||
await user.setById(uid, {
|
||||
uname,
|
||||
unameLower: uname.toLowerCase(),
|
||||
priv: PRIV.PRIV_NONE,
|
||||
}, $unset);
|
||||
await user.setPassword(uid, String.random(32));
|
||||
// TODO delete contest, homework, etc
|
||||
return uid;
|
||||
}
|
||||
|
||||
export const validate = {
|
||||
uid: 'number',
|
||||
};
|
||||
|
||||
global.Hydro.script.deleteUser = { run, description, validate };
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as db from '../service/db';
|
||||
import { STATUS } from '../model/builtin';
|
||||
import * as document from '../model/document';
|
||||
|
||||
export const description = 'Recalcuates nSubmit and nAccept in problem status.';
|
||||
|
||||
export async function run() {
|
||||
const pipeline = [
|
||||
{
|
||||
$match: { hidden: false, type: { $ne: 'run' } },
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
||||
nSubmit: { $sum: 1 },
|
||||
nAccept: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ['$status', STATUS.STATUS_ACCEPTED] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: { domainId: '$_id.domainId', pid: '$_id.pid' },
|
||||
nSubmit: { $sum: '$nSubmit' },
|
||||
nAccept: { $sum: { $min: ['$nAccept', 1] } },
|
||||
},
|
||||
},
|
||||
];
|
||||
const bulk = db.collection('document').initializeUnorderedBulkOp();
|
||||
db.collection('record').aggregate(pipeline).each(
|
||||
(err, adoc: any) => bulk.find({
|
||||
domainId: adoc._id.domainId,
|
||||
docType: document.TYPE_PROBLEM,
|
||||
docId: adoc._id.pid,
|
||||
}).updateOne({
|
||||
$set: {
|
||||
nSubmit: adoc.nSubmit,
|
||||
nAccept: adoc.nAccept,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await bulk.execute();
|
||||
}
|
||||
|
||||
export const validate = {};
|
||||
|
||||
global.Hydro.script.problemStat = { run, description, validate };
|
||||
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { NumericDictionary } from 'lodash';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { Tdoc, Pdoc } from '../interface';
|
||||
import * as domain from '../model/domain';
|
||||
import * as contest from '../model/contest';
|
||||
import * as problem from '../model/problem';
|
||||
import * as record from '../model/record';
|
||||
import { STATUS } from '../model/builtin';
|
||||
import rating from '../lib/rating';
|
||||
import paginate from '../lib/paginate';
|
||||
|
||||
export const description = 'Calculate rp of a domain, or all domains';
|
||||
|
||||
type ND = NumericDictionary<number>
|
||||
|
||||
async function runProblem(pdoc: Pdoc, udict: ND): Promise<void>
|
||||
async function runProblem(domainId: string, pid: number, udict: ND): Promise<void>
|
||||
async function runProblem(...arg: any[]) {
|
||||
const pdoc: Pdoc = (typeof arg[0] === 'string')
|
||||
? await problem.get(arg[0], arg[1], -1)
|
||||
: arg[0];
|
||||
const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1];
|
||||
const [, nPages] = await paginate(
|
||||
problem.getMultiStatus(
|
||||
pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } },
|
||||
),
|
||||
1,
|
||||
100,
|
||||
);
|
||||
const nAccept = await problem.getMultiStatus(
|
||||
pdoc.domainId, { docId: pdoc.docId, status: STATUS.STATUS_ACCEPTED },
|
||||
).count();
|
||||
const p = (pdoc.difficulty || 5) / (Math.sqrt(Math.sqrt(nAccept)) + 1);
|
||||
for (let page = 1; page <= nPages; page++) {
|
||||
const [psdocs] = await paginate(
|
||||
problem.getMultiStatus(
|
||||
pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } },
|
||||
),
|
||||
page,
|
||||
100,
|
||||
);
|
||||
const rdict = await record.getList(pdoc.domainId, psdocs.map((psdoc) => psdoc.rid), true);
|
||||
for (const psdoc of psdocs) {
|
||||
if (rdict[psdoc.rid]) {
|
||||
const rp = rdict[psdoc.rid].score * p;
|
||||
udict[psdoc.uid] = (udict[psdoc.uid] || 1500) + rp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runContest(tdoc: Tdoc, udict: ND, report: Function): Promise<void>
|
||||
async function runContest(
|
||||
domainId: string, tid: ObjectID, udict: ND, report: Function
|
||||
): Promise<void>
|
||||
async function runContest(...arg: any[]) {
|
||||
const start = new Date().getTime();
|
||||
const tdoc: Tdoc = (typeof arg[0] === 'string')
|
||||
? await contest.get(arg[0], arg[1], -1)
|
||||
: arg[0];
|
||||
const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1];
|
||||
const report = (typeof arg[0] === 'string') ? arg[3] : arg[2];
|
||||
const tsdocs = await contest.getMultiStatus(tdoc.domainId, tdoc.docId, tdoc.docType)
|
||||
.sort(contest.RULES[tdoc.rule].statusSort).toArray();
|
||||
if (!tsdocs.length) return;
|
||||
const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs);
|
||||
const users = [];
|
||||
for (const result of rankedTsdocs) {
|
||||
users.push({ uid: result[1].uid, rank: result[0], old: udict[result[1].uid] || 1500 });
|
||||
}
|
||||
// FIXME sum(rating.new) always less than sum(rating.old)
|
||||
const rated = rating(users);
|
||||
for (const udoc of rated) udict[udoc.uid] = udoc.new;
|
||||
await report({
|
||||
case: {
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
message: `Contest ${tdoc.title} finished`,
|
||||
time: new Date().getTime() - start,
|
||||
memory: 0,
|
||||
score: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function runInDomain(domainId: string, isSub: boolean, report: Function) {
|
||||
const udict: ND = {};
|
||||
const contests = await contest.getMulti(domainId, { rated: true }, -1).sort('endAt', -1).toArray();
|
||||
await report({ message: `Found ${contests.length} contests in ${domainId}` });
|
||||
for (const i in contests) {
|
||||
const tdoc = contests[i];
|
||||
await runContest(tdoc, udict, report);
|
||||
if (!isSub) {
|
||||
await report({
|
||||
progress: Math.floor(((parseInt(i, 10) + 1) / contests.length) * 100),
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO pagination
|
||||
const problems = await problem.getMulti(domainId, { hidden: false }).toArray();
|
||||
await report({ message: `Found ${problems.length} problems in ${domainId}` });
|
||||
for (const i in problems) {
|
||||
const pdoc = problems[i];
|
||||
await runProblem(pdoc, udict);
|
||||
if (!isSub) {
|
||||
await report({
|
||||
progress: Math.floor(((parseInt(i, 10) + 1) / problems.length) * 100),
|
||||
});
|
||||
}
|
||||
}
|
||||
await domain.setMultiUserInDomain(domainId, {}, { rp: 1500 });
|
||||
const tasks = [];
|
||||
for (const uid in udict) {
|
||||
tasks.push(domain.setUserInDomain(domainId, parseInt(uid, 10), { rp: udict[uid] }));
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
export async function run({ domainId }, report: Function) {
|
||||
if (!domainId) {
|
||||
const domains = await domain.getMulti().toArray();
|
||||
await report({ message: `Found ${domains.length} domains` });
|
||||
for (const i in domains) {
|
||||
const start = new Date().getTime();
|
||||
await runInDomain(domains[i]._id, true, report);
|
||||
await report({
|
||||
case: {
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
message: `Domain ${domains[i]._id} finished`,
|
||||
time: new Date().getTime() - start,
|
||||
memory: 0,
|
||||
score: 0,
|
||||
},
|
||||
progress: Math.floor(((parseInt(i, 10) + 1) / domains.length) * 100),
|
||||
});
|
||||
}
|
||||
} else await runInDomain(domainId, false, report);
|
||||
return true;
|
||||
}
|
||||
|
||||
export const validate = {
|
||||
$or: [
|
||||
{ domainId: 'string' },
|
||||
{ domainId: 'undefined' },
|
||||
],
|
||||
};
|
||||
|
||||
global.Hydro.script.rp = { run, description, validate };
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as user from '../model/user';
|
||||
|
||||
export const description = 'Create a new user';
|
||||
|
||||
export async function run({
|
||||
uname, password, mail, uid,
|
||||
}) {
|
||||
if (uid) uid = parseInt(uid, 10);
|
||||
if (uid && Number.isNaN(uid)) throw new Error('uid');
|
||||
uid = await user.create(mail, uname, password, uid);
|
||||
return uid;
|
||||
}
|
||||
|
||||
export const validate = {
|
||||
uname: { $type: 'string' },
|
||||
password: { $type: 'string' },
|
||||
mail: { $type: 'string' },
|
||||
};
|
||||
|
||||
global.Hydro.script.register = { run, description, validate };
|
||||
@@ -0,0 +1,18 @@
|
||||
// TODO add a set_priv interface
|
||||
import * as user from '../model/user';
|
||||
import { PRIV } from '../model/builtin';
|
||||
|
||||
export const description = 'Set a user as superadmin.';
|
||||
|
||||
export async function run({ uid }) {
|
||||
uid = parseInt(uid, 10);
|
||||
if (Number.isNaN(uid)) throw new Error('uid');
|
||||
await user.setPriv(uid, PRIV.PRIV_ALL);
|
||||
return uid;
|
||||
}
|
||||
|
||||
export const validate = {
|
||||
uid: { $type: 'number' },
|
||||
};
|
||||
|
||||
global.Hydro.script.setSuperadmin = { run, description, validate };
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as domain from '../model/domain';
|
||||
|
||||
export const description = 'Upgrade database from 0 to 1';
|
||||
|
||||
export async function run() {
|
||||
const ddoc = await domain.get('system');
|
||||
if (!ddoc) await domain.add('system', 0, 'Hydro', 'Hydro System');
|
||||
// TODO discussion node?
|
||||
}
|
||||
|
||||
export const validate = {};
|
||||
|
||||
global.Hydro.script.upgrade0_1 = { run, description, validate };
|
||||
@@ -0,0 +1,41 @@
|
||||
import cluster from 'cluster';
|
||||
|
||||
const bus = {};
|
||||
|
||||
export function subscribe(events: string[], handler: any) {
|
||||
const id = String.random(16);
|
||||
for (const event of events) {
|
||||
if (!bus[event]) bus[event] = {};
|
||||
bus[event][id] = handler;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function unsubscribe(events: string[], id: string) {
|
||||
for (const event of events) {
|
||||
if (!bus[event]) bus[event] = {};
|
||||
delete bus[event][id];
|
||||
}
|
||||
}
|
||||
|
||||
export function publish(event: string, payload: any, isMaster = true) {
|
||||
// Process forked by pm2 would also have process.send
|
||||
if (isMaster && process.send && !cluster.isMaster) {
|
||||
process.send({
|
||||
event: 'bus',
|
||||
eventName: event,
|
||||
payload,
|
||||
});
|
||||
} else {
|
||||
if (!bus[event]) bus[event] = {};
|
||||
const funcs = Object.keys(bus[event]);
|
||||
Promise.all(funcs.map((func) => bus[event][func]({
|
||||
event,
|
||||
value: payload,
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
global.Hydro.service.bus = {
|
||||
subscribe, unsubscribe, publish,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import mongodb from 'mongodb';
|
||||
import MongoWatcher from 'mongodb-watcher';
|
||||
import * as bus from './bus';
|
||||
import options from '../options';
|
||||
|
||||
const opts = options();
|
||||
|
||||
let mongourl = 'mongodb://';
|
||||
if (opts.username) mongourl += `${opts.username}:${opts.password}@`;
|
||||
mongourl += `${opts.host}:${opts.port}/${opts.name}`;
|
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let db: mongodb.Db = null;
|
||||
|
||||
mongodb.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true })
|
||||
.then((Client) => {
|
||||
db = Client.db(opts.name);
|
||||
global.Hydro.service.db.db = db;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const watcher = new MongoWatcher(db, {
|
||||
longCursorThreshold: 100,
|
||||
largeInsertThreshold: 1024 * 30,
|
||||
largeFetchThreshold: 1024 * 30,
|
||||
});
|
||||
bus.publish('system_database_connected', null);
|
||||
});
|
||||
|
||||
export function collection(c: string) {
|
||||
return db.collection(c);
|
||||
}
|
||||
|
||||
global.Hydro.service.db = { collection, db };
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridFSBucket } from 'mongodb';
|
||||
import { db } from './db';
|
||||
|
||||
export const fs = new GridFSBucket(db);
|
||||
|
||||
global.Hydro.service.gridfs = fs;
|
||||
|
||||
export default fs;
|
||||
@@ -0,0 +1,40 @@
|
||||
import cluster from 'cluster';
|
||||
import * as db from './db';
|
||||
import * as sysinfo from '../lib/sysinfo';
|
||||
|
||||
const coll = db.collection('status');
|
||||
|
||||
export async function update() {
|
||||
const [mid, $set] = await sysinfo.update();
|
||||
await coll.updateOne(
|
||||
{ mid, type: 'server' },
|
||||
{ $set: { ...$set, updateAt: new Date(), reqCount: global.Hydro.stat.reqCount } },
|
||||
{ upsert: true },
|
||||
);
|
||||
global.Hydro.stat.reqCount = 0;
|
||||
}
|
||||
|
||||
export function updateJudger(args) {
|
||||
args.type = 'judger';
|
||||
return coll.updateOne(
|
||||
{ mid: args.mid, type: 'judger' },
|
||||
{ $set: args },
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (cluster.isMaster) {
|
||||
global.Hydro.postInit.push(
|
||||
async () => {
|
||||
const info = await sysinfo.get();
|
||||
await coll.updateOne(
|
||||
{ mid: info.mid, type: 'server' },
|
||||
{ $set: { ...info, updateAt: new Date(), type: 'server' } },
|
||||
{ upsert: true },
|
||||
);
|
||||
setInterval(update, 60 * 1000);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
global.Hydro.service.monitor = { update, updateJudger };
|
||||
@@ -0,0 +1,782 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import assert from 'assert';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import http from 'http';
|
||||
import moment from 'moment-timezone';
|
||||
import { isSafeInteger, Dictionary, filter } from 'lodash';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import Koa from 'koa';
|
||||
import morgan from 'koa-morgan';
|
||||
import Body from 'koa-body';
|
||||
import Router from 'koa-router';
|
||||
import cache from 'koa-static-cache';
|
||||
import sockjs from 'sockjs';
|
||||
import { SetOption } from 'cookies';
|
||||
import serialize, { SerializeJSOptions } from 'serialize-javascript';
|
||||
import { argv } from 'yargs';
|
||||
import { lrucache } from '../utils';
|
||||
import { User, DomainDoc } from '../interface';
|
||||
import {
|
||||
UserNotFoundError, BlacklistedError, PermissionError,
|
||||
UserFacingError, ValidationError, PrivilegeError,
|
||||
CsrfTokenError, InvalidOperationError, MethodNotAllowedError,
|
||||
NotFoundError, HydroError,
|
||||
} from '../error';
|
||||
import hash from '../lib/hash.hydro';
|
||||
import * as misc from '../lib/misc';
|
||||
import * as user from '../model/user';
|
||||
import * as domain from '../model/domain';
|
||||
import * as system from '../model/system';
|
||||
import * as blacklist from '../model/blacklist';
|
||||
import * as token from '../model/token';
|
||||
import * as opcount from '../model/opcount';
|
||||
|
||||
export const app = new Koa();
|
||||
export const server = http.createServer(app.callback());
|
||||
export const router = new Router();
|
||||
|
||||
type MethodDecorator = (target: any, name: string, obj: any) => any;
|
||||
type Converter = (value: any) => any;
|
||||
type Validator = (value: any) => boolean;
|
||||
interface ParamOption {
|
||||
name: string,
|
||||
isOptional?: boolean,
|
||||
convert?: Converter,
|
||||
validate?: Validator,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
export enum Types { String, Int, UnsignedInt, Float, ObjectID, Boolean, Date, Time }
|
||||
|
||||
const Tools: Array<[Converter, Validator, boolean?]> = [
|
||||
[(v) => v.toString(), null],
|
||||
[(v) => parseInt(v, 10), (v) => isSafeInteger(parseInt(v, 10))],
|
||||
[(v) => parseInt(v, 10), (v) => parseInt(v, 10) > 0],
|
||||
[(v) => parseFloat(v), (v) => {
|
||||
const t = parseFloat(v);
|
||||
console.log(t);
|
||||
return t && !Number.isNaN(t) && Number.isFinite(t);
|
||||
}],
|
||||
[(v) => new ObjectID(v), ObjectID.isValid],
|
||||
[(v) => !!v, null, true],
|
||||
[
|
||||
(v) => {
|
||||
const d = v.split('-');
|
||||
assert(d.length === 3);
|
||||
return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
|
||||
},
|
||||
(v) => {
|
||||
const d = v.split('-');
|
||||
assert(d.length === 3);
|
||||
const st = `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
|
||||
return moment(st).isValid();
|
||||
}],
|
||||
[
|
||||
(v) => {
|
||||
const t = v.split(':');
|
||||
assert(t.length === 2);
|
||||
return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`;
|
||||
},
|
||||
(v) => {
|
||||
const t = v.split(':');
|
||||
assert(t.length === 2);
|
||||
return moment(`2020-01-01 ${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`).isValid();
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
export function param(name: string, type: Types, validate: Validator): MethodDecorator;
|
||||
export function param(name: string, type?: Types, isOptional?: boolean): MethodDecorator;
|
||||
export function param(
|
||||
name: string, type: Types, validate: null, convert: Converter
|
||||
): MethodDecorator;
|
||||
export function param(
|
||||
name: string, type: Types, validate?: Validator, convert?: Converter,
|
||||
): MethodDecorator;
|
||||
export function param(
|
||||
name: string, type: Types, isOptional?: boolean, validate?: Validator, convert?: Converter,
|
||||
): MethodDecorator;
|
||||
export function param(
|
||||
name: string, ...args: Array<Types | boolean | Converter | Validator>
|
||||
): MethodDecorator;
|
||||
export function param(name: string, ...args: any): MethodDecorator {
|
||||
let cursor = 0;
|
||||
const v: ParamOption = { name };
|
||||
let isValidate = true;
|
||||
while (cursor < args.length) {
|
||||
if (typeof args[cursor] === 'number') {
|
||||
const type = args[cursor];
|
||||
if (Tools[type]) {
|
||||
if (Tools[type][0]) v.convert = Tools[type][0];
|
||||
if (Tools[type][1]) v.validate = Tools[type][1];
|
||||
if (Tools[type][2]) v.isOptional = Tools[type][2];
|
||||
}
|
||||
} else if (typeof args[cursor] === 'boolean') v.isOptional = args[cursor];
|
||||
else if (isValidate) {
|
||||
if (args[cursor] !== null) v.validate = args[cursor];
|
||||
isValidate = false;
|
||||
} else {
|
||||
const I = args[cursor];
|
||||
v.convert = I;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
return function desc(target: any, funcName: string, obj: any) {
|
||||
if (!target.__param) target.__param = {};
|
||||
if (!target.__param[target.constructor.name]) target.__param[target.constructor.name] = {};
|
||||
if (!target.__param[target.constructor.name][funcName]) {
|
||||
target.__param[target.constructor.name][funcName] = [{ name: 'domainId', type: 'string' }];
|
||||
const originalMethod = obj.value;
|
||||
obj.value = function validate(rawArgs: any) {
|
||||
const c = [];
|
||||
const arglist: ParamOption[] = this.__param[target.constructor.name][funcName];
|
||||
for (const item of arglist) {
|
||||
if (!item.isOptional || rawArgs[item.name]) {
|
||||
if (!rawArgs[item.name]) throw new ValidationError(item.name);
|
||||
if (item.validate) {
|
||||
if (!item.validate(rawArgs[item.name])) {
|
||||
throw new ValidationError(item.name);
|
||||
}
|
||||
}
|
||||
if (item.convert) c.push(item.convert(rawArgs[item.name]));
|
||||
else c.push(rawArgs[item.name]);
|
||||
} else c.push(undefined);
|
||||
}
|
||||
return originalMethod.call(this, ...c);
|
||||
};
|
||||
}
|
||||
target.__param[target.constructor.name][funcName].splice(1, 0, v);
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
export function multipart(maxSizeKiB: number): MethodDecorator {
|
||||
return function multipartDecorator(target: any, funcName: string, obj: any) {
|
||||
const originalMethod = obj.value;
|
||||
obj.value = async function checkCsrfToken(...args: any[]) {
|
||||
const body = Body({
|
||||
multipart: true,
|
||||
formidable: {
|
||||
maxFileSize: maxSizeKiB * 1024,
|
||||
},
|
||||
});
|
||||
return await body.call(this.ctx, this.ctx, () => originalMethod.call(this, ...args));
|
||||
};
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
export function requireCsrfToken(target: any, funcName: string, obj: any) {
|
||||
const originalMethod = obj.value;
|
||||
obj.value = async function checkCsrfToken(...args: any[]) {
|
||||
if (this.getCsrfToken(this.session._id) !== this.args.csrfToken) {
|
||||
throw new CsrfTokenError(this.args.csrfToken);
|
||||
}
|
||||
return await originalMethod.call(this, ...args);
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
|
||||
export async function prepare() {
|
||||
app.keys = await system.get('session.keys');
|
||||
if (argv.public) {
|
||||
app.use(cache(argv.public, {
|
||||
maxAge: 0,
|
||||
}));
|
||||
} else {
|
||||
app.use(cache(path.join(os.tmpdir(), 'hydro', 'public'), {
|
||||
maxAge: 365 * 24 * 60 * 60,
|
||||
}));
|
||||
}
|
||||
app.use(Body());
|
||||
}
|
||||
|
||||
export class Handler {
|
||||
UIContext: any;
|
||||
|
||||
args: any;
|
||||
|
||||
ctx: Koa.Context;
|
||||
|
||||
request: {
|
||||
host: string,
|
||||
hostname: string,
|
||||
ip: string,
|
||||
headers: any,
|
||||
cookies: any,
|
||||
body: any,
|
||||
files: any,
|
||||
query: any,
|
||||
path: string,
|
||||
params: any,
|
||||
referer: string,
|
||||
json: boolean,
|
||||
};
|
||||
|
||||
response: {
|
||||
body: any,
|
||||
type: string,
|
||||
status: number,
|
||||
template?: string,
|
||||
redirect?: string,
|
||||
disposition?: string,
|
||||
attachment: (name: string) => void,
|
||||
};
|
||||
|
||||
session: any;
|
||||
|
||||
csrfToken: string;
|
||||
|
||||
user: User;
|
||||
|
||||
domain: DomainDoc;
|
||||
|
||||
loginMethods: any;
|
||||
|
||||
constructor(ctx: Koa.Context) {
|
||||
this.ctx = ctx;
|
||||
this.request = {
|
||||
host: ctx.request.host,
|
||||
hostname: ctx.request.hostname,
|
||||
ip: ctx.request.ip,
|
||||
headers: ctx.request.headers,
|
||||
cookies: ctx.cookies,
|
||||
body: ctx.request.body,
|
||||
files: ctx.request.files,
|
||||
query: ctx.query,
|
||||
path: ctx.path,
|
||||
params: ctx.params,
|
||||
referer: ctx.request.headers.referer || '/',
|
||||
json: (ctx.request.headers.accept || '').includes('application/json'),
|
||||
};
|
||||
this.response = {
|
||||
body: {},
|
||||
type: '',
|
||||
status: null,
|
||||
template: null,
|
||||
redirect: null,
|
||||
attachment: (name) => ctx.attachment(name),
|
||||
disposition: null,
|
||||
};
|
||||
this.UIContext = {
|
||||
cdn_prefix: '/',
|
||||
url_prefix: '/',
|
||||
};
|
||||
this.session = {};
|
||||
}
|
||||
|
||||
@lrucache
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getCsrfToken(id: string) {
|
||||
return hash('csrf_token', id);
|
||||
}
|
||||
|
||||
async renderHTML(name: string, context: any): Promise<string> {
|
||||
const UserContext = {
|
||||
...this.user,
|
||||
gravatar: misc.gravatar(this.user.gravatar || '', 128),
|
||||
perm: this.user.perm.toString(),
|
||||
};
|
||||
if (!global.Hydro.lib.template) return JSON.stringify(context);
|
||||
const res = await global.Hydro.lib.template.render(name, {
|
||||
handler: this,
|
||||
UserContext,
|
||||
url: this.url.bind(this),
|
||||
_: this.translate.bind(this),
|
||||
...context,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
async limitRate(op: string, periodSecs: number, maxOperations: number) {
|
||||
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
|
||||
}
|
||||
|
||||
translate(str: string) {
|
||||
if (!str) return '';
|
||||
return str.toString().translate(this.user.viewLang, this.session.viewLang);
|
||||
}
|
||||
|
||||
renderTitle(str: string) {
|
||||
return `${this.translate(str)} - Hydro`;
|
||||
}
|
||||
|
||||
checkPerm(...args: bigint[]) {
|
||||
// @ts-ignore
|
||||
if (!this.user.hasPerm(...args)) throw new PermissionError(...args);
|
||||
}
|
||||
|
||||
checkPriv(...args: number[]) {
|
||||
// @ts-ignore
|
||||
if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args);
|
||||
}
|
||||
|
||||
url(name: string, kwargs = {}) {
|
||||
let res = '#';
|
||||
const args: any = { ...kwargs };
|
||||
try {
|
||||
if (this.args.domainId !== 'system' || args.domainId) {
|
||||
name += '_with_domainId';
|
||||
args.domainId = args.domainId || this.args.domainId;
|
||||
}
|
||||
const { anchor, query } = args;
|
||||
if (query) res = router.url(name, args, { query });
|
||||
else res = router.url(name, args);
|
||||
if (anchor) return `${res}#${anchor}`;
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
console.log(name, args);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async render(name: string, context: any) {
|
||||
this.response.body = await this.renderHTML(name, context);
|
||||
this.response.type = 'text/html';
|
||||
}
|
||||
|
||||
back(body?: any) {
|
||||
this.response.body = body || this.response.body || {};
|
||||
this.response.redirect = this.request.headers.referer || '/';
|
||||
}
|
||||
|
||||
binary(data: any, name: string) {
|
||||
this.response.body = data;
|
||||
this.response.template = null;
|
||||
this.response.type = 'application/octet-stream';
|
||||
this.response.disposition = `attachment; filename="${name}"`;
|
||||
}
|
||||
|
||||
async getSession() {
|
||||
const sid = this.request.cookies.get('sid');
|
||||
this.session = await token.get(sid, token.TYPE_SESSION);
|
||||
if (!this.session) this.session = { uid: 0 };
|
||||
}
|
||||
|
||||
async getBdoc() {
|
||||
const bdoc = await blacklist.get(this.request.ip);
|
||||
if (bdoc) throw new BlacklistedError(this.request.ip);
|
||||
}
|
||||
|
||||
async init({ domainId }) {
|
||||
const xff = await system.get('server.xff');
|
||||
if (xff) this.request.ip = this.request.headers[xff.toLowerCase()];
|
||||
[this.domain] = await Promise.all([
|
||||
domain.get(domainId),
|
||||
this.getSession(),
|
||||
this.getBdoc(),
|
||||
]);
|
||||
if (!this.domain) {
|
||||
this.args.domainId = 'system';
|
||||
this.user = await user.getById('system', this.session.uid);
|
||||
if (!this.user) this.user = await user.getById('system', 0);
|
||||
throw new NotFoundError(domainId);
|
||||
}
|
||||
this.user = await user.getById(domainId, this.session.uid);
|
||||
if (!this.user) {
|
||||
this.session.uid = 0;
|
||||
this.user = await user.getById(domainId, this.session.uid);
|
||||
}
|
||||
this.csrfToken = this.getCsrfToken(this.session._id || String.random(32));
|
||||
this.UIContext.csrfToken = this.csrfToken;
|
||||
this.loginMethods = filter(Object.keys(global.Hydro.lib), (str) => str.startsWith('oauth_'))
|
||||
.map((key) => ({
|
||||
id: key.split('_')[1],
|
||||
icon: global.Hydro.lib[key].icon,
|
||||
text: global.Hydro.lib[key].text,
|
||||
}));
|
||||
}
|
||||
|
||||
async finish() {
|
||||
try {
|
||||
await this.renderBody();
|
||||
} catch (error) {
|
||||
this.response.status = error instanceof UserFacingError ? error.code : 500;
|
||||
if (this.request.json) this.response.body = { error };
|
||||
else {
|
||||
try {
|
||||
await this.render(error instanceof UserFacingError ? 'error.html' : 'bsod.html', { error });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// this.response.body.error = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.putResponse();
|
||||
await this.saveCookie();
|
||||
}
|
||||
|
||||
async renderBody() {
|
||||
if (this.response.redirect) {
|
||||
this.response.body = this.response.body || {};
|
||||
this.response.body.url = this.response.redirect;
|
||||
}
|
||||
if (this.response.type) return;
|
||||
if (
|
||||
this.request.json || this.response.redirect
|
||||
|| this.request.query.noTemplate || !this.response.template) {
|
||||
try {
|
||||
this.response.body = JSON.stringify(this.response.body);
|
||||
} catch (e) {
|
||||
const opt: SerializeJSOptions = { ignoreFunction: true };
|
||||
if (this.request.query.noTemplate) opt.space = 2;
|
||||
this.response.body = serialize(this.response.body, opt);
|
||||
}
|
||||
this.response.type = 'application/json';
|
||||
} else if (this.response.body || this.response.template) {
|
||||
const templateName = this.request.query.template || this.response.template;
|
||||
if (templateName) {
|
||||
this.response.body = this.response.body || {};
|
||||
await this.render(templateName, this.response.body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async putResponse() {
|
||||
if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition);
|
||||
if (this.response.redirect && !this.request.json) {
|
||||
this.ctx.response.type = 'application/octet-stream';
|
||||
this.ctx.response.status = 302;
|
||||
this.ctx.redirect(this.response.redirect);
|
||||
} else {
|
||||
if (this.response.body != null) {
|
||||
this.ctx.response.body = this.response.body;
|
||||
this.ctx.response.status = this.response.status || 200;
|
||||
}
|
||||
this.ctx.response.type = this.request.json
|
||||
? 'application/json'
|
||||
: this.response.type
|
||||
? this.response.type
|
||||
: this.ctx.response.type;
|
||||
}
|
||||
}
|
||||
|
||||
async saveCookie() {
|
||||
const expireSeconds = this.session.save
|
||||
? await system.get('session.expire_seconds')
|
||||
: await system.get('session.unsaved_expire_seconds');
|
||||
if (this.session._id) {
|
||||
await token.update(
|
||||
this.session._id,
|
||||
token.TYPE_SESSION,
|
||||
expireSeconds,
|
||||
{
|
||||
...this.session,
|
||||
updateIp: this.request.ip,
|
||||
updateUa: this.request.headers['user-agent'] || '',
|
||||
},
|
||||
);
|
||||
} else {
|
||||
[, this.session] = await token.add(
|
||||
token.TYPE_SESSION,
|
||||
expireSeconds,
|
||||
{
|
||||
...this.session,
|
||||
createIp: this.request.ip,
|
||||
createUa: this.request.headers['user-agent'] || '',
|
||||
updateIp: this.request.ip,
|
||||
updateUa: this.request.headers['user-agent'] || '',
|
||||
},
|
||||
);
|
||||
}
|
||||
const cookie: SetOption = {
|
||||
secure: await system.get('session.secure'),
|
||||
httpOnly: false,
|
||||
};
|
||||
if (this.session.save) {
|
||||
cookie.expires = this.session.expireAt;
|
||||
cookie.maxAge = expireSeconds;
|
||||
}
|
||||
this.ctx.cookies.set('sid', this.session._id, cookie);
|
||||
}
|
||||
|
||||
async onerror(error: HydroError) {
|
||||
if (!error.msg) error.msg = () => error.message;
|
||||
console.error(error.msg(), error.params);
|
||||
console.error(error.stack);
|
||||
this.response.status = error instanceof UserFacingError ? error.code : 500;
|
||||
this.response.template = error instanceof UserFacingError ? 'error.html' : 'bsod.html';
|
||||
this.response.body = {
|
||||
error: { message: error.msg(), params: error.params, stack: error.stack },
|
||||
};
|
||||
await this.finish().catch(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
async function handle(ctx, HandlerClass, checker) {
|
||||
global.Hydro.stat.reqCount++;
|
||||
const args = {
|
||||
domainId: 'system', ...ctx.params, ...ctx.query, ...ctx.request.body,
|
||||
};
|
||||
const h = new HandlerClass(ctx);
|
||||
h.args = args;
|
||||
h.domainId = args.domainId;
|
||||
try {
|
||||
const method = ctx.method.toLowerCase();
|
||||
let operation: string;
|
||||
if (method === 'post' && ctx.request.body.operation) {
|
||||
operation = `_${ctx.request.body.operation}`
|
||||
.replace(/_([a-z])/gm, (s) => s[1].toUpperCase());
|
||||
}
|
||||
|
||||
await h.init(args);
|
||||
if (checker) checker.call(h);
|
||||
if (method === 'post') {
|
||||
if (operation) {
|
||||
if (typeof h[`post${operation}`] !== 'function') {
|
||||
throw new InvalidOperationError(operation);
|
||||
}
|
||||
} else if (typeof h.post !== 'function') {
|
||||
throw new MethodNotAllowedError(method);
|
||||
}
|
||||
} else if (typeof h[method] !== 'function' && typeof h.all !== 'function') {
|
||||
throw new MethodNotAllowedError(method);
|
||||
}
|
||||
|
||||
if (h._prepare) await h._prepare(args);
|
||||
if (h.prepare) await h.prepare(args);
|
||||
|
||||
if (h[method]) await h[method](args);
|
||||
if (operation) await h[`post${operation}`](args);
|
||||
|
||||
if (h.cleanup) await h.cleanup(args);
|
||||
if (h.finish) await h.finish(args);
|
||||
} catch (e) {
|
||||
try {
|
||||
await h.onerror(e);
|
||||
} catch (err) {
|
||||
h.response.code = 500;
|
||||
h.response.body = `${err.message}\n${err.stack}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Checker = (permPrivChecker) => {
|
||||
let perm: bigint;
|
||||
let priv: number;
|
||||
let checker = () => { };
|
||||
for (const item of permPrivChecker) {
|
||||
if (typeof item === 'object') {
|
||||
if (typeof item.call !== 'undefined') {
|
||||
checker = item;
|
||||
} if (typeof item[0] === 'number') {
|
||||
priv = item;
|
||||
} else if (typeof item[0] === 'bigint') {
|
||||
perm = item;
|
||||
}
|
||||
} else if (typeof item === 'number') {
|
||||
priv = item;
|
||||
} else if (typeof item === 'bigint') {
|
||||
perm = item;
|
||||
}
|
||||
}
|
||||
return function check() {
|
||||
checker();
|
||||
if (perm) this.checkPerm(perm);
|
||||
if (priv) this.checkPriv(priv);
|
||||
};
|
||||
};
|
||||
|
||||
export function Route(name: string, route: string, RouteHandler: any, ...permPrivChecker) {
|
||||
const checker = Checker(permPrivChecker);
|
||||
router.all(name, route, (ctx) => handle(ctx, RouteHandler, checker));
|
||||
router.all(`${name}_with_domainId`, `/d/:domainId${route}`, (ctx) => handle(ctx, RouteHandler, checker));
|
||||
}
|
||||
|
||||
export class ConnectionHandler {
|
||||
conn: sockjs.Connection;
|
||||
|
||||
request: {
|
||||
params: any
|
||||
headers: any
|
||||
ip: string
|
||||
}
|
||||
|
||||
session: any
|
||||
|
||||
args: any
|
||||
|
||||
user: any
|
||||
|
||||
domain: DomainDoc
|
||||
|
||||
constructor(conn: sockjs.Connection) {
|
||||
this.conn = conn;
|
||||
this.request = {
|
||||
params: {},
|
||||
headers: conn.headers,
|
||||
ip: this.conn.remoteAddress,
|
||||
};
|
||||
this.session = {};
|
||||
const p: any = (conn.url.split('?')[1] || '').split('&');
|
||||
for (const i in p) p[i] = p[i].split('=');
|
||||
for (const i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]);
|
||||
}
|
||||
|
||||
async renderHTML(name: string, context: any): Promise<string> {
|
||||
const res = await global.Hydro.lib.template.render(name, Object.assign(context, {
|
||||
handler: this,
|
||||
url: this.url.bind(this),
|
||||
_: this.translate.bind(this),
|
||||
}));
|
||||
return res;
|
||||
}
|
||||
|
||||
async limitRate(op: string, periodSecs: number, maxOperations: number) {
|
||||
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
|
||||
}
|
||||
|
||||
translate(str: string) {
|
||||
return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : '';
|
||||
}
|
||||
|
||||
renderTitle(str: string) {
|
||||
return `${this.translate(str)} - Hydro`;
|
||||
}
|
||||
|
||||
checkPerm(...args: bigint[]) {
|
||||
if (!this.user.hasPerm(...args)) throw new PermissionError(...args);
|
||||
}
|
||||
|
||||
checkPriv(...args: number[]) {
|
||||
// @ts-ignore
|
||||
if (!this.user.hasPriv(...args)) throw new PrivilegeError(...args);
|
||||
}
|
||||
|
||||
url(name: string, kwargs = {}) {
|
||||
let res = '#';
|
||||
const args: any = { ...kwargs };
|
||||
try {
|
||||
if (this.args.domainId !== 'system' || args.domainId) {
|
||||
name += '_with_domainId';
|
||||
args.domainId = args.domainId || this.args.domainId;
|
||||
}
|
||||
const { anchor, query } = args;
|
||||
if (query) res = router.url(name, args, { query });
|
||||
else res = router.url(name, args);
|
||||
if (anchor) return `${res}#${anchor}`;
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
console.log(name, args);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
send(data: any) {
|
||||
this.conn.write(JSON.stringify(data));
|
||||
}
|
||||
|
||||
close(code: number, reason: string) {
|
||||
this.conn.close(code.toString(), reason);
|
||||
}
|
||||
|
||||
async message(message: any) { } // eslint-disable-line
|
||||
|
||||
onerror(err: HydroError) {
|
||||
console.error(err);
|
||||
this.send({
|
||||
error: {
|
||||
name: err.name,
|
||||
params: err.params || [],
|
||||
},
|
||||
});
|
||||
this.close(1001, err.toString());
|
||||
}
|
||||
|
||||
async getSession(cookieHeader: string) {
|
||||
const cookies: any = {};
|
||||
const ref = cookieHeader.split(';');
|
||||
for (let j = 0; j < ref.length; j++) {
|
||||
const cookie = ref[j];
|
||||
const parts = cookie.split('=');
|
||||
cookies[parts[0].trim()] = (parts[1] || '').trim();
|
||||
}
|
||||
this.session = await token.get(cookies.sid || '', token.TYPE_SESSION);
|
||||
if (!this.session) this.session = { uid: 0, domainId: 'system' };
|
||||
}
|
||||
|
||||
@param('cookie', Types.String)
|
||||
async init(domainId: string, cookie: string) {
|
||||
[this.domain] = await Promise.all([
|
||||
domain.get(domainId),
|
||||
this.getSession(cookie),
|
||||
]);
|
||||
const bdoc = await blacklist.get(this.request.ip);
|
||||
if (bdoc) throw new BlacklistedError(this.request.ip);
|
||||
this.user = await user.getById(domainId, this.session.uid);
|
||||
if (!this.user) throw new UserNotFoundError(this.session.uid);
|
||||
}
|
||||
}
|
||||
|
||||
export function Connection(
|
||||
name: string, prefix: string,
|
||||
RouteConnHandler: any,
|
||||
...permPrivChecker: Array<number | bigint | Function>
|
||||
) {
|
||||
const log = argv.debug ? console.log : () => { };
|
||||
const sock = sockjs.createServer({ prefix, log });
|
||||
const checker = Checker(permPrivChecker);
|
||||
sock.on('connection', async (conn) => {
|
||||
const h: Dictionary<any> = new RouteConnHandler(conn);
|
||||
try {
|
||||
const args = { domainId: 'system', ...h.request.params };
|
||||
h.args = args;
|
||||
const cookie = await new Promise((resolve) => {
|
||||
conn.once('data', (c) => {
|
||||
resolve(c);
|
||||
});
|
||||
});
|
||||
args.cookie = cookie;
|
||||
await h.init(args);
|
||||
conn.write(JSON.stringify({ event: 'auth' }));
|
||||
checker.call(h);
|
||||
|
||||
if (h._prepare) await h._prepare(args);
|
||||
if (h.prepare) await h.prepare(args);
|
||||
if (h.message) {
|
||||
conn.on('data', (data) => {
|
||||
h.message(JSON.parse(data));
|
||||
});
|
||||
}
|
||||
conn.on('close', async () => {
|
||||
if (h.cleanup) await h.cleanup(args);
|
||||
if (h.finish) await h.finish(args);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
await h.onerror(e);
|
||||
}
|
||||
});
|
||||
sock.installHandlers(server);
|
||||
}
|
||||
|
||||
// TODO use postInit?
|
||||
export async function start() {
|
||||
const port = await system.get('server.port');
|
||||
if (argv.debug) {
|
||||
app.use(morgan(':method :url :status :res[content-length] - :response-time ms', {
|
||||
skip: (req, res) => res.hasHeader('nolog'),
|
||||
}));
|
||||
}
|
||||
app.use(router.routes()).use(router.allowedMethods());
|
||||
Route('notfound_handler', '*', Handler);
|
||||
server.listen(argv.port || port);
|
||||
console.log('Server listening at: %s', argv.port || port);
|
||||
}
|
||||
|
||||
global.Hydro.service.server = {
|
||||
Types,
|
||||
app,
|
||||
server,
|
||||
router,
|
||||
param,
|
||||
multipart,
|
||||
requireCsrfToken,
|
||||
Handler,
|
||||
ConnectionHandler,
|
||||
Route,
|
||||
Connection,
|
||||
prepare,
|
||||
start,
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
import fs from 'fs';
|
||||
import cluster from 'cluster';
|
||||
import path from 'path';
|
||||
import * as superagent from 'superagent';
|
||||
import proxyAgent from 'proxy-agent';
|
||||
import serialize from 'serialize-javascript';
|
||||
import Lru from 'lru-cache';
|
||||
|
||||
declare global {
|
||||
interface StringConstructor {
|
||||
random: (digit?: number) => string;
|
||||
}
|
||||
interface ArrayConstructor {
|
||||
isDiff: (a: any[], b: any[]) => boolean;
|
||||
}
|
||||
interface Date {
|
||||
format: (fmt?: string) => string;
|
||||
}
|
||||
interface Math {
|
||||
sum: (...args: Array<number[] | number>) => number;
|
||||
}
|
||||
interface SetConstructor {
|
||||
isSuperset: (set: Set<any>, subset: Set<any>) => boolean;
|
||||
intersection: <T>(setA: Set<T>, setB: Set<T>) => Set<T>;
|
||||
union: <T>(setA: Set<T>, setB: Set<T>) => Set<T>;
|
||||
}
|
||||
}
|
||||
|
||||
const lru = new Lru(1000);
|
||||
|
||||
export function lrucache(target: any, funcName: string, obj: any) {
|
||||
const originalMethod = obj.value;
|
||||
obj.value = function func(...args: any[]) {
|
||||
const key = serialize({ funcName, args });
|
||||
let val = lru.get(key);
|
||||
if (val) return val;
|
||||
val = originalMethod.call(this, ...args);
|
||||
lru.set(key, val);
|
||||
return val;
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
|
||||
namespace Proxy {
|
||||
function getUrl() {
|
||||
return this._url;
|
||||
}
|
||||
function setUrl(v: string) {
|
||||
this._url = v;
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
proxy.call(this, this._proxyUri);
|
||||
}
|
||||
function setupUrl(req: superagent.Request) {
|
||||
const desc = Object.getOwnPropertyDescriptor(req, 'url');
|
||||
if (desc.get === getUrl && desc.set === setUrl) return;
|
||||
// @ts-ignore
|
||||
req._url = req.url;
|
||||
desc.get = getUrl;
|
||||
desc.set = setUrl;
|
||||
delete desc.value;
|
||||
delete desc.writable;
|
||||
Object.defineProperty(req, 'url', desc);
|
||||
}
|
||||
function proxy(uri: string) {
|
||||
if (!uri) return this;
|
||||
setupUrl(this);
|
||||
const agent = proxyAgent(uri);
|
||||
if (agent) this.agent(agent);
|
||||
this._proxyUri = uri;
|
||||
return this;
|
||||
}
|
||||
export function setup(agent, uri = '') {
|
||||
const Request = agent.Request;
|
||||
if (Request) {
|
||||
Request.prototype.proxy = proxy;
|
||||
return agent;
|
||||
}
|
||||
return proxy.call(agent, uri);
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'superagent' {
|
||||
interface SuperAgentRequest {
|
||||
proxy(url: string): this;
|
||||
}
|
||||
}
|
||||
|
||||
Proxy.setup(superagent);
|
||||
|
||||
if (!cluster.worker) {
|
||||
// @ts-ignore
|
||||
cluster.worker = { id: 0 };
|
||||
}
|
||||
|
||||
const dict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
|
||||
|
||||
String.random = function random(digit = 32) {
|
||||
let str = '';
|
||||
for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * 62)];
|
||||
return str;
|
||||
};
|
||||
|
||||
Array.isDiff = function isDiff(a, b) {
|
||||
if (a.length !== b.length) return true;
|
||||
a.sort();
|
||||
b.sort();
|
||||
for (const i in a) {
|
||||
if (a[i] !== b[i]) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') {
|
||||
let m = this.getMonth() + 1;
|
||||
if (m < 10) m = `0${m}`;
|
||||
let d = this.getDate();
|
||||
if (d < 10) d = `0${d}`;
|
||||
let H = this.getHours();
|
||||
if (H < 10) H = `0${H}`;
|
||||
let M = this.getMinutes();
|
||||
if (M < 10) M = `0${M}`;
|
||||
let S = this.getSeconds();
|
||||
if (S < 10) S = `0${S}`;
|
||||
return fmt
|
||||
.replace('%Y', this.getFullYear())
|
||||
.replace('%m', m)
|
||||
.replace('%d', d)
|
||||
.replace('%H', H)
|
||||
.replace('%M', M)
|
||||
.replace('%S', S);
|
||||
};
|
||||
|
||||
Math.sum = function sum(...args) {
|
||||
let s = 0;
|
||||
for (const i of args) {
|
||||
if (i instanceof Array) {
|
||||
for (const j of i) {
|
||||
s += j;
|
||||
}
|
||||
} else s += i;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
Set.isSuperset = function isSuperset(set, subset) {
|
||||
for (const elem of subset) {
|
||||
if (!set.has(elem)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
Set.union = function Union<T>(setA: Set<T>, setB: Set<T>) {
|
||||
const union = new Set(setA);
|
||||
for (const elem of setB) union.add(elem);
|
||||
return union;
|
||||
};
|
||||
|
||||
Set.intersection = function Intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
|
||||
const intersection = new Set();
|
||||
for (const elem of setB) {
|
||||
if (setA.has(elem)) intersection.add(elem);
|
||||
}
|
||||
// @ts-ignore
|
||||
return intersection;
|
||||
};
|
||||
|
||||
export function folderSize(folderPath: string) {
|
||||
let size = 0;
|
||||
const _next = function a(p: string) {
|
||||
if (p) {
|
||||
const stats = fs.statSync(p);
|
||||
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
||||
if (!stats.isSymbolicLink()) size += stats.size;
|
||||
} else {
|
||||
size += stats.size;
|
||||
const files = fs.readdirSync(p);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach((file) => {
|
||||
_next(path.join(p, file));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
_next(folderPath);
|
||||
return size;
|
||||
}
|
||||
|
||||
const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i;
|
||||
const TIME_UNITS = { '': 1000, m: 1, u: 0.001 };
|
||||
const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i;
|
||||
const MEMORY_UNITS = { k: 0.1, m: 1, g: 1024 };
|
||||
|
||||
export function parseTimeMS(str: string) {
|
||||
const match = TIME_RE.exec(str);
|
||||
if (!match) throw new Error(`${str} error parsing time`);
|
||||
return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]);
|
||||
}
|
||||
|
||||
export function parseMemoryMB(str: string) {
|
||||
const match = MEMORY_RE.exec(str);
|
||||
if (!match) throw new Error(`${str} error parsing memory`);
|
||||
return Math.floor(parseFloat(match[1]) * MEMORY_UNITS[match[2]]);
|
||||
}
|
||||
|
||||
export function isClass(obj: any, strict = false) {
|
||||
if (typeof obj !== 'function') return false;
|
||||
const str = obj.toString();
|
||||
if (obj.prototype === undefined) return false;
|
||||
if (obj.prototype.constructor !== obj) return false;
|
||||
if (str.slice(0, 5) === 'class') return true;
|
||||
if (Object.getOwnPropertyNames(obj.prototype).length >= 2) return true;
|
||||
if (/^function\s+\(|^function\s+anonymous\(/.test(str)) return false;
|
||||
if (strict && /^function\s+[A-Z]/.test(str)) return true;
|
||||
if (/\b\(this\b|\bthis[.[]\b/.test(str)) {
|
||||
if (!strict || /classCallCheck\(this/.test(str)) return true;
|
||||
return /^function\sdefault_\d+\s*\(/.test(str);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# Login-With-Github
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'hydrooj';
|
||||
import superagent from 'superagent';
|
||||
|
||||
async function get() {
|
||||
const { system, token } = global.Hydro.model;
|
||||
const [appid, [state]] = await Promise.all([
|
||||
system.get('login-with-github.id'),
|
||||
token.add(token.TYPE_OAUTH, 600, { redirect: this.request.referer }),
|
||||
]);
|
||||
this.response.redirect = `https://github.com/login/oauth/authorize?client_id=${appid}&state=${state}`;
|
||||
}
|
||||
|
||||
async function callback({ state, code }) {
|
||||
const { system, token } = global.Hydro.model;
|
||||
const { UserFacingError } = global.Hydro.error;
|
||||
const [[appid, secret, proxy, url], s] = await Promise.all([
|
||||
system.getMany([
|
||||
'login-with-github.id',
|
||||
'login-with-github.secret',
|
||||
'login-with-github.proxy',
|
||||
'server.url',
|
||||
]),
|
||||
token.get(state, token.TYPE_OAUTH),
|
||||
]);
|
||||
const res = await superagent.post('https://github.com/login/oauth/access_token')
|
||||
.proxy(proxy)
|
||||
.send({
|
||||
client_id: appid,
|
||||
client_secret: secret,
|
||||
code,
|
||||
redirect_uri: `${url}oauth/github/callback`,
|
||||
state,
|
||||
})
|
||||
.set('accept', 'application/json');
|
||||
if (res.body.error) {
|
||||
throw new UserFacingError(
|
||||
res.body.error, res.body.error_description, res.body.error_uri,
|
||||
);
|
||||
}
|
||||
const t = res.body.access_token;
|
||||
const userInfo = await superagent.get('https://api.github.com/user')
|
||||
.proxy(proxy)
|
||||
.set('User-Agent', 'Hydro-OAuth')
|
||||
.set('Authorization', `token ${t}`);
|
||||
const ret = {
|
||||
email: userInfo.body.email,
|
||||
bio: userInfo.body.bio,
|
||||
uname: [userInfo.body.name, userInfo.body.login],
|
||||
};
|
||||
this.response.redirect = s.redirect;
|
||||
await token.del(s._id, token.TYPE_OAUTH);
|
||||
return ret;
|
||||
}
|
||||
|
||||
global.Hydro.lib.oauth_github = {
|
||||
text: 'Login with Github',
|
||||
callback,
|
||||
get,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@hydrooj/login-with-github",
|
||||
"version": "0.0.2",
|
||||
"main": "package.json",
|
||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
||||
"author": "undefined <masnn0@outlook.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint lib.ts --fix",
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/superagent": "^4.1.8",
|
||||
"hydrooj": "2.12.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hydrooj": "^2.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"superagent": "^5.3.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
id:
|
||||
type: text
|
||||
category: system
|
||||
desc: Github OAuth AppID
|
||||
name: id
|
||||
secret:
|
||||
type: text
|
||||
category: system
|
||||
desc: Github OAuth Secret
|
||||
name: secret
|
||||
proxy:
|
||||
type: text
|
||||
category: system
|
||||
desc: Github OAuth Proxy
|
||||
name: proxy
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": ".",
|
||||
"rootDir": ".",
|
||||
},
|
||||
"include": [
|
||||
"*.ts"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# Login-With-Google
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'hydrooj';
|
||||
import superagent from 'superagent';
|
||||
|
||||
async function get() {
|
||||
const { system, token } = global.Hydro.model;
|
||||
const [appid, url, [state]] = await Promise.all([
|
||||
system.get('login-with-google.id'),
|
||||
system.get('server.url'),
|
||||
token.add(token.TYPE_OAUTH, 600, { redirect: this.request.referer }),
|
||||
]);
|
||||
this.response.redirect = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${appid}&response_type=code&redirect_uri=${url}oauth/google/callback&scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile&state=${state}`;
|
||||
}
|
||||
|
||||
async function callback({
|
||||
state, code, error,
|
||||
}) {
|
||||
const { system, token } = global.Hydro.model;
|
||||
const { UserFacingError } = global.Hydro.error;
|
||||
if (error) throw new UserFacingError(error);
|
||||
const [
|
||||
[appid, secret, url, proxy],
|
||||
s,
|
||||
] = await Promise.all([
|
||||
system.getMany([
|
||||
'login-with-google.id', 'oauth.googlesecret', 'server.url', 'proxy',
|
||||
]),
|
||||
token.get(state, token.TYPE_OAUTH),
|
||||
]);
|
||||
const res = await superagent.post('https://oauth2.googleapis.com/token')
|
||||
.proxy(proxy)
|
||||
.send({
|
||||
client_id: appid,
|
||||
client_secret: secret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: `${url}oauth/google/callback`,
|
||||
});
|
||||
const payload = global.Hydro.lib.jwt.decode(res.body.id_token);
|
||||
await token.del(state, token.TYPE_OAUTH);
|
||||
this.response.redirect = s.redirect;
|
||||
return {
|
||||
email: payload.email,
|
||||
uname: [payload.given_name, payload.name, payload.family_name],
|
||||
viewLang: payload.locale.replace('-', '_'),
|
||||
};
|
||||
}
|
||||
|
||||
global.Hydro.lib.oauth_google = {
|
||||
text: 'Login with Google',
|
||||
callback,
|
||||
get,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@hydrooj/login-with-google",
|
||||
"version": "0.0.1",
|
||||
"main": "package.json",
|
||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
||||
"author": "undefined <masnn0@outlook.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint lib.ts --fix",
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/superagent": "^4.1.8",
|
||||
"hydrooj": "2.12.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hydrooj": "^2.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"superagent": "^5.3.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
id:
|
||||
type: text
|
||||
category: system
|
||||
desc: Google OAuth AppID
|
||||
name: id
|
||||
secret:
|
||||
type: text
|
||||
category: system
|
||||
desc: Google OAuth Secret
|
||||
name: secret
|
||||
proxy:
|
||||
type: text
|
||||
category: system
|
||||
desc: Google OAuth Proxy
|
||||
name: proxy
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": ".",
|
||||
"rootDir": ".",
|
||||
},
|
||||
"include": [
|
||||
"*.ts"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
Reference in new issue
Block a user