@@ -10,8 +10,8 @@ import {
1010} from '@hydrooj/utils/lib/utils' ;
1111import { Context , Service } from '../context' ;
1212import {
13- BadRequestError , ContestNotAttendedError , ContestNotEndedError , ContestNotFoundError , ContestNotLiveError ,
14- ContestScoreboardHiddenError , FileLimitExceededError , FileUploadError ,
13+ BadRequestError , ContestAlreadyAttendedError , ContestNotAttendedError , ContestNotEndedError , ContestNotFoundError ,
14+ ContestNotLiveError , ContestScoreboardHiddenError , FileLimitExceededError , FileUploadError ,
1515 InvalidTokenError , MethodNotAllowedError , NotAssignedError , NotFoundError , PermissionError , ValidationError ,
1616} from '../error' ;
1717import { ContestStatusDoc , FileInfo , ScoreboardConfig , Tdoc } from '../interface' ;
@@ -25,7 +25,7 @@ import problem from '../model/problem';
2525import record from '../model/record' ;
2626import ScheduleModel from '../model/schedule' ;
2727import storage from '../model/storage' ;
28- import user from '../model/user' ;
28+ import user , { collV , deleteUserCache } from '../model/user' ;
2929import {
3030 Handler , param , post , Type , Types ,
3131} from '../service/server' ;
@@ -66,6 +66,7 @@ export class ContestListHandler extends Handler {
6666 const [ tdocs , tpcount ] = await this . paginate ( cursor , page , 'contest' ) ;
6767 const tids = [ ] ;
6868 for ( const tdoc of tdocs ) tids . push ( tdoc . docId ) ;
69+ // FIXME: Team status need to be queried here.
6970 const tsdict = await contest . getListStatus ( domainId , this . user . _id , tids ) ;
7071 const groupsFilter = groups . filter ( ( i ) => ! Number . isSafeInteger ( + i ) ) ;
7172 this . response . template = 'contest_main.html' ;
@@ -78,14 +79,16 @@ export class ContestListHandler extends Handler {
7879export class ContestDetailBaseHandler extends Handler {
7980 tdoc ?: Tdoc ;
8081 tsdoc ?: ContestStatusDoc ;
82+ team ?: number ;
8183
8284 @param ( 'tid' , Types . ObjectId , true )
8385 async __prepare ( domainId : string , tid : ObjectId ) {
8486 if ( ! tid ) return ; // ProblemDetailHandler also extends from ContestDetailBaseHandler
85- [ this . tdoc , this . tsdoc ] = await Promise . all ( [
86- contest . get ( domainId , tid ) ,
87- contest . getStatus ( domainId , tid , this . user . _id ) ,
88- ] ) ;
87+ this . tdoc = await contest . get ( domainId , tid ) ;
88+ if ( this . tdoc . allowTeam ) {
89+ this . team = await contest . getTeamVuser ( domainId , tid , this . user . _id ) || undefined ;
90+ }
91+ this . tsdoc = await contest . getStatus ( domainId , tid , this . team ?? this . user . _id ) ;
8992 if ( this . tdoc . assign ?. length && ! this . user . own ( this . tdoc ) && ! this . user . hasPerm ( PERM . PERM_VIEW_HIDDEN_CONTEST ) ) {
9093 const groups = await user . listGroup ( domainId , this . user . _id ) ;
9194 if ( ! new Set ( this . tdoc . assign ) . intersection ( new Set ( groups . map ( ( i ) => i . name ) ) ) . size ) {
@@ -103,7 +106,11 @@ export class ContestDetailBaseHandler extends Handler {
103106
104107 tsdocAsPublic ( ) {
105108 if ( ! this . tsdoc ) return null ;
106- return pick ( this . tsdoc , [ 'attend' , 'subscribe' , 'startAt' , ...( this . tdoc . duration || this . tsdoc . endAt ? [ 'endAt' ] : [ ] ) ] ) ;
109+ return pick ( this . tsdoc , [
110+ 'attend' , 'subscribe' , 'startAt' ,
111+ 'teamName' , 'members' , // for team
112+ ...( this . tdoc . duration || this . tsdoc . endAt ? [ 'endAt' ] : [ ] ) ,
113+ ] ) ;
107114 }
108115
109116 @param ( 'tid' , Types . ObjectId , true )
@@ -174,19 +181,34 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
174181
175182 @param ( 'tid' , Types . ObjectId )
176183 @param ( 'code' , Types . String , true )
177- async postAttend ( domainId : string , tid : ObjectId , code = '' ) {
184+ @param ( 'vuid' , Types . Int , true )
185+ async postAttend ( domainId : string , tid : ObjectId , code = '' , vuid ?: number ) {
178186 this . checkPerm ( PERM . PERM_ATTEND_CONTEST ) ;
179187 if ( contest . isDone ( this . tdoc ) ) throw new ContestNotLiveError ( domainId , tid ) ;
180188 if ( this . tdoc . _code && code !== this . tdoc . _code ) throw new InvalidTokenError ( 'Contest Invitation' , code ) ;
181- await contest . attend ( domainId , tid , this . user . _id , { subscribe : 1 } ) ;
189+ if ( vuid ) {
190+ if ( ! this . tdoc . allowTeam ) throw new ValidationError ( 'allowTeam' ) ;
191+ const v = await collV . findOne ( { _id : vuid } ) ;
192+ if ( ! v ?. members ?. includes ( this . user . _id ) ) throw new PermissionError ( PERM . PERM_ATTEND_CONTEST ) ;
193+ const conflicts = await Promise . all ( v . members . map ( async ( uid ) => ( { uid, vuser : await contest . getTeamVuser ( domainId , tid , uid ) } ) ) ) ;
194+ const conflict = conflicts . find ( ( c ) => c . vuser ) ;
195+ if ( conflict ) throw new ContestAlreadyAttendedError ( tid , conflict . uid ) ;
196+ await contest . attend ( domainId , tid , vuid , {
197+ subscribe : 1 ,
198+ teamName : v . teamName ,
199+ members : v . members ,
200+ } ) ;
201+ } else {
202+ await contest . attend ( domainId , tid , this . user . _id , { subscribe : 1 } ) ;
203+ }
182204 this . back ( ) ;
183205 }
184206
185207 @param ( 'tid' , Types . ObjectId )
186208 @param ( 'subscribe' , Types . Boolean )
187209 async postSubscribe ( domainId : string , tid : ObjectId , subscribe = false ) {
188210 if ( ! this . tsdoc ?. attend ) throw new ContestNotAttendedError ( domainId , tid ) ;
189- await contest . setStatus ( domainId , tid , this . user . _id , { subscribe : subscribe ? 1 : 0 } ) ;
211+ await contest . setStatus ( domainId , tid , this . team ?? this . user . _id , { subscribe : subscribe ? 1 : 0 } ) ;
190212 this . back ( ) ;
191213 }
192214
@@ -196,7 +218,7 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
196218 if ( ! this . tsdoc ?. attend ) throw new ContestNotAttendedError ( domainId , tid ) ;
197219 if ( ! contest . isOngoing ( this . tdoc , this . tsdoc ) ) throw new ContestNotLiveError ( domainId , tid ) ;
198220 const now = new Date ( ) ;
199- await contest . setStatus ( domainId , tid , this . user . _id , { endAt : now , ...( ! this . tsdoc . startAt ? { startAt : now } : { } ) } ) ;
221+ await contest . setStatus ( domainId , tid , this . team ?? this . user . _id , { endAt : now , ...( ! this . tsdoc . startAt ? { startAt : now } : { } ) } ) ;
200222 this . back ( ) ;
201223 }
202224}
@@ -307,7 +329,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler {
307329 this . response . body . showScore = Object . values ( this . tdoc . score || { } ) . some ( ( i ) => i && i !== 100 ) ;
308330 if ( ! this . tsdoc ) return ;
309331 if ( this . tsdoc . attend && ! this . tsdoc . startAt && contest . isOngoing ( this . tdoc ) ) {
310- await contest . setStatus ( domainId , tid , this . user . _id , { startAt : new Date ( ) } ) ;
332+ await contest . setStatus ( domainId , tid , this . team ?? this . user . _id , { startAt : new Date ( ) } ) ;
311333 this . tsdoc . startAt = new Date ( ) ;
312334 }
313335 this . response . body . tsdoc = this . tsdocAsPublic ( ) ;
@@ -417,13 +439,14 @@ export class ContestEditHandler extends Handler {
417439 @param ( 'allowViewCode' , Types . Boolean )
418440 @param ( 'allowPrint' , Types . Boolean )
419441 @param ( 'keepScoreboardHidden' , Types . Boolean )
442+ @param ( 'allowTeam' , Types . Boolean )
420443 @param ( 'langs' , Types . CommaSeperatedArray , true )
421444 async postUpdate (
422445 domainId : string , tid : ObjectId , beginAtDate : string , beginAtTime : string , duration : number ,
423446 title : string , content : string , rule : string , _pids : string , rated = false ,
424447 _code = '' , autoHide = false , assign : string [ ] = [ ] , lock : number = null ,
425448 contestDuration : number = null , maintainer : number [ ] = [ ] , allowViewCode = false , allowPrint = false ,
426- keepScoreboardHidden = false , langs : string [ ] = [ ] ,
449+ keepScoreboardHidden = false , allowTeam = false , langs : string [ ] = [ ] ,
427450 ) {
428451 if ( ! Object . keys ( contest . RULES ) . includes ( rule ) || contest . RULES [ rule ] . hidden ) throw new ValidationError ( 'rule' ) ;
429452 if ( autoHide ) this . checkPerm ( PERM . PERM_EDIT_PROBLEM ) ;
@@ -464,8 +487,9 @@ export class ContestEditHandler extends Handler {
464487 executeAfter : endAt ,
465488 } ) ;
466489 }
490+ // FIXME: allowTeam cannot be disabled once enabled.
467491 await contest . edit ( domainId , tid , {
468- assign, _code, autoHide, lockAt, maintainer, allowViewCode, allowPrint, keepScoreboardHidden, langs,
492+ assign, _code, autoHide, lockAt, maintainer, allowViewCode, allowPrint, keepScoreboardHidden, allowTeam , langs,
469493 } ) ;
470494 this . response . body = { tid } ;
471495 this . response . redirect = this . url ( 'contest_detail' , { tid } ) ;
@@ -692,7 +716,7 @@ export class ContestFileDownloadHandler extends ContestDetailBaseHandler {
692716 if ( type === 'private' && ! this . user . own ( this . tdoc ) && ! this . user . hasPerm ( PERM . PERM_EDIT_CONTEST ) ) {
693717 if ( ! this . tsdoc ?. attend ) throw new ContestNotAttendedError ( domainId , tid ) ;
694718 if ( ! contest . isOngoing ( this . tdoc ) && ! contest . isDone ( this . tdoc ) ) throw new ContestNotLiveError ( domainId , tid ) ;
695- if ( ! this . tsdoc . startAt ) await contest . setStatus ( domainId , tid , this . user . _id , { startAt : new Date ( ) } ) ;
719+ if ( ! this . tsdoc . startAt ) await contest . setStatus ( domainId , tid , this . team ?? this . user . _id , { startAt : new Date ( ) } ) ;
696720 }
697721 this . response . addHeader ( 'Cache-Control' , 'public' ) ;
698722 const target = `contest/${ domainId } /${ tid } /${ type } /${ filename } ` ;
@@ -917,9 +941,92 @@ declare module 'cordis' {
917941 }
918942}
919943
944+ class ContestTeamHandler extends Handler {
945+ async prepare ( ) {
946+ this . checkPriv ( PRIV . PRIV_USER_PROFILE ) ;
947+ }
948+
949+ async get ( { domainId } ) {
950+ const [ mine , invites ] = await Promise . all ( [
951+ collV . find ( { members : this . user . _id } ) . toArray ( ) ,
952+ collV . find ( { invite : this . user . _id } ) . toArray ( ) ,
953+ ] ) ;
954+ const udict = await user . getList ( domainId , mine . flatMap ( ( t ) => [ ...t . members , ...( t . invite || [ ] ) ] ) ) ;
955+ this . response . template = 'contest_team.html' ;
956+ this . response . body = { mine, invites, udict, page_name : 'contest_team' } ;
957+ }
958+
959+ private async mustMember ( vuid : number ) {
960+ const v = await collV . findOne ( { _id : vuid } ) ;
961+ if ( ! v ?. members ?. includes ( this . user . _id ) ) throw new PermissionError ( PERM . PERM_ATTEND_CONTEST ) ;
962+ return v ;
963+ }
964+
965+ private async mut ( vuid : number , update : any ) {
966+ const v = await collV . findOneAndUpdate ( { _id : vuid } , update , { returnDocument : 'after' } ) ;
967+ deleteUserCache ( v ) ;
968+ return v ;
969+ }
970+
971+ @param ( 'name' , Types . String , true )
972+ async postCreate ( domainId : string , name ?: string ) {
973+ await user . createVuser ( `team:${ this . user . _id } :${ randomstring ( 6 ) } ` , {
974+ teamName : name ?. trim ( ) || this . user . uname ,
975+ members : [ this . user . _id ] ,
976+ } ) ;
977+ this . back ( ) ;
978+ }
979+
980+ @param ( 'vuid' , Types . Int )
981+ @param ( 'name' , Types . String )
982+ async postRename ( domainId : string , vuid : number , name : string ) {
983+ await this . mustMember ( vuid ) ;
984+ await this . mut ( vuid , { $set : { teamName : name . trim ( ) } } ) ;
985+ this . back ( ) ;
986+ }
987+
988+ @param ( 'vuid' , Types . Int )
989+ @param ( 'uid' , Types . Int )
990+ async postInvite ( domainId : string , vuid : number , uid : number ) {
991+ const v = await this . mustMember ( vuid ) ;
992+ if ( v . members . includes ( uid ) || v . invite ?. includes ( uid ) ) throw new ValidationError ( 'uid' ) ;
993+ await this . mut ( vuid , { $addToSet : { invite : uid } } ) ;
994+ await message . send ( this . user . _id , uid ,
995+ `${ this . user . uname } invites you to join team "${ v . teamName } ": ${ this . url ( 'contest_team' ) } ` ,
996+ message . FLAG_RICHTEXT ) ;
997+ this . back ( ) ;
998+ }
999+
1000+ @param ( 'vuid' , Types . Int )
1001+ async postAccept ( domainId : string , vuid : number ) {
1002+ const v = await collV . findOne ( { _id : vuid } ) ;
1003+ if ( ! v ?. invite ?. includes ( this . user . _id ) ) throw new ValidationError ( 'vuid' ) ;
1004+ await this . mut ( vuid , { $pull : { invite : this . user . _id } , $addToSet : { members : this . user . _id } } ) ;
1005+ this . back ( ) ;
1006+ }
1007+
1008+ @param ( 'vuid' , Types . Int )
1009+ async postReject ( domainId : string , vuid : number ) {
1010+ const v = await collV . findOne ( { _id : vuid } ) ;
1011+ if ( ! v ?. invite ?. includes ( this . user . _id ) ) throw new ValidationError ( 'vuid' ) ;
1012+ await this . mut ( vuid , { $pull : { invite : this . user . _id } } ) ;
1013+ this . back ( ) ;
1014+ }
1015+
1016+ @param ( 'vuid' , Types . Int )
1017+ @param ( 'uid' , Types . Int , true )
1018+ async postLeave ( domainId : string , vuid : number , uid ?: number ) {
1019+ // FIXME: remove uid in invite[]
1020+ await this . mustMember ( vuid ) ;
1021+ await this . mut ( vuid , { $pull : { members : uid ?? this . user . _id } } ) ;
1022+ this . back ( ) ;
1023+ }
1024+ }
1025+
9201026export async function apply ( ctx : Context ) {
9211027 ctx . Route ( 'contest_create' , '/contest/create' , ContestEditHandler ) ;
9221028 ctx . Route ( 'contest_main' , '/contest' , ContestListHandler , PERM . PERM_VIEW_CONTEST ) ;
1029+ ctx . Route ( 'contest_team' , '/contest/team' , ContestTeamHandler ) ; // before /contest/:tid: "team" is not a valid ObjectId
9231030 ctx . Route ( 'contest_detail' , '/contest/:tid' , ContestDetailHandler , PERM . PERM_VIEW_CONTEST ) ;
9241031 ctx . Route ( 'contest_problemlist' , '/contest/:tid/problems' , ContestProblemListHandler , PERM . PERM_VIEW_CONTEST ) ;
9251032 ctx . Route ( 'contest_edit' , '/contest/:tid/edit' , ContestEditHandler , PERM . PERM_VIEW_CONTEST ) ;
0 commit comments