rxjs/operators#toArray TypeScript Examples

The following examples show how to use rxjs/operators#toArray. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: emote.component.ts    From App with MIT License 6 votes vote down vote up
/**
	 * Get all sizes of the current emote
	 */
	getSizes(): Observable<EmoteComponent.SizeResult[]> {
		return from([1, 2, 3, 4]).pipe(
			map(s => ({
				scope: s,
				url: this.restService.CDN.Emote(String(this.emote?.getID()), s),
				width: this.emote?.width[s - 1],
				height: this.emote?.height[s - 1]
			} as EmoteComponent.SizeResult)),
			toArray()
		);
	}
Example #2
Source File: rx-operators.spec.ts    From capture-lite with GNU General Public License v3.0 6 votes vote down vote up
describe('rx-operators', () => {
  it('should filter null and undefined values with type guard', done => {
    const expected = [0, 1, 2];
    of(expected[0], expected[1], null, undefined, expected[2])
      .pipe(isNonNullable(), toArray())
      .subscribe(result => {
        expect(result).toEqual(expected);
        done();
      });
  });

  it('should switchTap behaving the same with switchMapTo but ignore the result', done => {
    const expected = 1;
    of(expected)
      .pipe(switchTap(v => of(v + v)))
      .subscribe(result => {
        expect(result).toEqual(expected);
        done();
      });
  });

  it('should switchTapTo behaving the same with switchMapTo but ignore the result', done => {
    const expected = 1;
    of(expected)
      .pipe(switchTapTo(of(2)))
      .subscribe(result => {
        expect(result).toEqual(expected);
        done();
      });
  });
});
Example #3
Source File: RestApi.ts    From majsoul-api with MIT License 6 votes vote down vote up
private getSessions(contest: store.Contest<ObjectId>): Observable<Session> {
		return concat(
			defer(() => from(
				this.mongoStore.sessionsCollection.find(
					{ contestId: contest._id },
					{ sort: { scheduledTime: 1 } }
				).toArray()
			)).pipe(
				mergeAll(),
			),
			of<store.Session<ObjectId>>(null)
		).pipe(
			pairwise(),
			map(([session, nextSession]) =>
				defer(() => from(this.getSessionSummary(contest, session, nextSession)))
					.pipe(
						map(totals => {
							return { ...session, totals, aggregateTotals: totals };
						})
					)
			),
			mergeAll(),
		);
	}
Example #4
Source File: RestApi.ts    From majsoul-api with MIT License 6 votes vote down vote up
private async correctGames(games: store.GameResult<ObjectId>[]): Promise<store.GameResult<ObjectId>[]> {
		const corrections = await this.mongoStore.gameCorrectionsCollection.find({
			gameId: {
				$in: games.map(game => game._id)
			}
		}).toArray();

		if (!corrections.length) {
			return games;
		}

		const gameMap = games.reduce(
			(total, next) => (total[next._id.toHexString()] = next, total),
			{} as Record<string, store.GameResult<ObjectId>>
		);

		for (const correction of corrections) {
			const game = gameMap[correction.gameId.toHexString()];
			for(let i = 0; i < game.finalScore.length; i++) {
				const umaCorrection = correction.finalScore[i].uma
				if (!isNaN(umaCorrection)) {
					game.finalScore[i].uma += umaCorrection;
				}

				const scoreCorrection = correction.finalScore[i].score
				if (!isNaN(scoreCorrection)) {
					game.finalScore[i].score += scoreCorrection;
				}
			}
		}
		return games;
	}
Example #5
Source File: index.ts    From dbm with Apache License 2.0 6 votes vote down vote up
manifest$ = merge(
  ...Object.entries({
    "**/*.scss": stylesheets$,
    "**/*.ts*":  javascripts$
  })
    .map(([pattern, observable$]) => (
      defer(() => process.argv.includes("--watch")
        ? watch(pattern, { cwd: "src" })
        : EMPTY
      )
        .pipe(
          startWith("*"),
          switchMapTo(observable$.pipe(toArray()))
        )
    ))
)
  .pipe(
    scan((prev, mapping) => (
      mapping.reduce((next, [key, value]) => (
        next.set(key, value.replace(`${base}/`, ""))
      ), prev)
    ), new Map<string, string>()),
  )
Example #6
Source File: wardrobe.component.ts    From App with MIT License 6 votes vote down vote up
private parsePaints(): Observable<WardrobeComponent.PaintCosmetic[]> {
		return this.clientService.getCosmetics().pipe(
			mergeAll(),
			filter(c => c.kind === 'PAINT'),
			map(c => ({
				...JSON.parse(c.data),
				...c
			})),
			toArray()
		);
	}
Example #7
Source File: wardrobe.component.ts    From App with MIT License 6 votes vote down vote up
parseBadges(): Observable<WardrobeComponent.BadgeCosmetic[]> {
		return this.clientService.getCosmetics().pipe(
			mergeAll(),
			filter(c => c.kind === 'BADGE'),
			map(c => ({
				...JSON.parse(c.data),
				...c
			})),
			toArray()
		);
	}
Example #8
Source File: user.structure.ts    From App with MIT License 6 votes vote down vote up
getAuditEntries(): Observable<AuditLogEntry[]> {
		return this.dataOnce().pipe(
			switchMap(data => data?.audit_entries ?? []),
			pluck('id'),
			map(entryID => this.dataService.get('audit', { id: entryID } )),
			map(a => a[0]),
			filter(e => !!e),
			toArray()
		);
	}
Example #9
Source File: user.structure.ts    From App with MIT License 6 votes vote down vote up
getEditorIn(): Observable<UserStructure[]> {
		return this.dataOnce().pipe(
			map(data => data?.editor_in ?? []),
			mergeAll(),
			map(u => this.dataService.get('user', { id: u.id })),
			mergeAll(),
			toArray()
		);
	}
Example #10
Source File: user.structure.ts    From App with MIT License 6 votes vote down vote up
/**
	 * Get emote aliases of the user
	 */
	getEmoteAliases(): Observable<UserStructure.EmoteAlias[]> {
		return this.dataOnce().pipe(
			switchMap(data => from(data?.emote_aliases ?? [])),
			map(a => ({
				emoteID: a[0],
				name: a[1]
			} as UserStructure.EmoteAlias)),
			toArray()
		);
	}
Example #11
Source File: user-home.component.ts    From App with MIT License 6 votes vote down vote up
private shouldBlurEmote(emote: EmoteStructure): Observable<boolean> {
		return scheduled([
			emote.hasVisibility('HIDDEN'),
			emote.hasVisibility('PRIVATE')
		], asyncScheduler).pipe(
			concatAll(),
			toArray(),
			mergeMap(b => iif(() => b[0] === true || b[1] === true,
				this.clientService.hasPermission('EDIT_EMOTE_ALL').pipe(
					take(1),
					switchMap(bypass => iif(() => bypass,
						of(false),
						emote.getOwnerID().pipe(
							map(ownerID => ownerID !== this.clientService.id)
						)
					))
				),
				of(false)
			)),
			take(1)
		);
	}
Example #12
Source File: emote-list.component.ts    From App with MIT License 6 votes vote down vote up
getEmotes(page = 0, options?: Partial<RestV2.GetEmotesOptions>): Observable<EmoteStructure[]> {
		this.emotes.next([]);
		this.newPage.next(page);
		const timeout = setTimeout(() => this.loading.next(true), 1000);
		const cancelSpinner = () => {
			this.loading.next(false);
			clearTimeout(timeout);
		};

		const size = this.calculateSizedRows();
		return this.restService.awaitAuth().pipe(
			switchMap(() => this.restService.v2.SearchEmotes(
				(this.pageOptions?.pageIndex ?? 0) + 1,
				Math.max(EmoteListComponent.MINIMUM_EMOTES, size ?? EmoteListComponent.MINIMUM_EMOTES),
				options ?? this.currentSearchOptions
			)),

			takeUntil(this.newPage.pipe(take(1))),
			tap(res => this.totalEmotes.next(res?.total_estimated_size ?? 0)),
			delay(200),
			map(res => res?.emotes ?? []),
			mergeAll(),
			map(data => this.dataService.add('emote', data)[0]),
			toArray(),
			defaultIfEmpty([] as EmoteStructure[]),

			tap(() => cancelSpinner()),
			catchError(() => defer(() => cancelSpinner()))
		) as Observable<EmoteStructure[]>;
	}
Example #13
Source File: RestApi.ts    From majsoul-api with MIT License 5 votes vote down vote up
private async getLeaguePhaseData({
		contest,
		transitions,
		phases
	}: PhaseInfo): Promise<LeaguePhase<ObjectID>[]> {
		const sessions = (await this.getSessions(contest).pipe(toArray()).toPromise())
			.sort((a, b) => a.scheduledTime - b.scheduledTime);
		return from(phases.concat(null)).pipe(
			pairwise(),
			mergeScan((completePhase, [phase, nextPhase]) => {
				const transition = transitions[phase.index];
				const phaseSessions = sessions.filter(
					session =>
						session.scheduledTime >= phase.startTime
						&& (nextPhase == null || session.scheduledTime < nextPhase.startTime)
				);
				const startingTotals = {
					...completePhase.sessions[completePhase.sessions.length - 1]?.aggregateTotals ?? {}
				};

				const rankedTeams = Object.entries(startingTotals)
					.map(([team, score]) => ({ team, score }))
					.sort((a, b) => b.score - a.score);

				const allowedTeams = transition.teams?.top
					? rankedTeams.slice(0, transition.teams.top)
						.reduce((total, next) => (total[next.team] = true, total), {} as Record<string, true>)
					: null;

				for (const team of Object.keys(startingTotals)) {
					if (allowedTeams && !(team in allowedTeams)) {
						delete startingTotals[team];
						continue;
					}

					if (transition.score?.half) {
						startingTotals[team] = Math.floor(startingTotals[team] / 2);
					} else if (transition.score?.nil) {
						startingTotals[team] = 0;
					}
				}

				return of({
					...phase,
					sessions: phaseSessions.reduce((total, next) => {
						const aggregateTotals = { ...(total[total.length - 1]?.aggregateTotals ?? startingTotals) };
						const filteredTotals = Object.entries(next.totals)
							.filter(([key]) => !allowedTeams || key in allowedTeams)
							.reduce((total, [key, value]) => (total[key] = value, total), {} as Record<string, number>);

						for (const team in filteredTotals) {
							if (aggregateTotals[team] == null) {
								aggregateTotals[team] = 0;
							}
							aggregateTotals[team] += filteredTotals[team];
						}

						total.push({
							...next,
							totals: filteredTotals,
							aggregateTotals,
						})
						return total;
					}, [] as Session<ObjectID>[]),
					aggregateTotals: startingTotals,
				} as LeaguePhase<ObjectID>);
			}, {
				sessions: [{
					aggregateTotals: (contest.teams ?? []).reduce(
						(total, next) => (total[next._id.toHexString()] = 0, total),
						{} as Record<string, number>
					)
				}]
			} as LeaguePhase<ObjectID>, 1),
			toArray(),
		).toPromise();
	}
Example #14
Source File: upload.spec.ts    From ngx-operators with MIT License 5 votes vote down vote up
describe('upload', () => {
  it('should transform HTTP upload', done => {
    const request = new Subject<HttpEvent<unknown>>()
    request.pipe(upload(), toArray())
      .subscribe(uploads => {
        expect(uploads).toEqual([
          {progress: 0, state: 'PENDING'},
          {progress: 1, state: 'IN_PROGRESS'},
          {progress: 50, state: 'IN_PROGRESS'},
          {progress: 100, state: 'IN_PROGRESS'},
          {progress: 100, state: 'DONE'}
        ])
        done()
      })
    request.next({type: HttpEventType.Sent})
    request.next({
      type: HttpEventType.User,
    })
    request.next({
      type: HttpEventType.UploadProgress,
      loaded: 12,
      total: 1024
    })
    request.next({
      type: HttpEventType.UploadProgress,
      loaded: 512,
      total: 1024
    })
    request.next({
      type: HttpEventType.User,
    })
    request.next({
      type: HttpEventType.UploadProgress,
      loaded: 1024,
      total: 1024
    })
    request.next({
      type: HttpEventType.UploadProgress,
      loaded: 1024
    })
    request.next(new HttpResponse({
      status: 200,
      url: '/uploads/file.pdf'
    }))
    request.complete()
  })
})
Example #15
Source File: download.spec.ts    From ngx-operators with MIT License 5 votes vote down vote up
describe("download", () => {
  it("should transform HTTP download & save", done => {
    const request = new Subject<HttpEvent<Blob>>();
    const blob = new Blob();
    const saver = createSpy("saver");
    request.pipe(download(saver), toArray()).subscribe(downloads => {
      expect(downloads).toEqual([
        { progress: 0, state: "PENDING", content: null },
        { progress: 1, state: "IN_PROGRESS", content: null },
        { progress: 50, state: "IN_PROGRESS", content: null },
        { progress: 100, state: "IN_PROGRESS", content: null },
        { progress: 100, state: "DONE", content: blob }
      ]);
      expect(saver).toHaveBeenCalledWith(blob);
      done();
    });
    request.next({ type: HttpEventType.Sent });
    request.next({
      type: HttpEventType.User
    });
    request.next({
      type: HttpEventType.DownloadProgress,
      loaded: 12,
      total: 1024
    });
    request.next({
      type: HttpEventType.DownloadProgress,
      loaded: 512,
      total: 1024
    });
    request.next({
      type: HttpEventType.User
    });
    request.next({
      type: HttpEventType.DownloadProgress,
      loaded: 1024,
      total: 1024
    });
    request.next({
      type: HttpEventType.DownloadProgress,
      loaded: 1024
    });
    request.next(
      new HttpResponse({
        body: blob,
        status: 200,
        url: "/downloads/file.pdf"
      })
    );
    request.complete();
  });
});
Example #16
Source File: service.ts    From jetlinks-ui-antd with MIT License 5 votes vote down vote up
public permission = {
        query: (params: any) => defer(() => from(
            request(`/jetlinks/permission/_query/no-paging?paging=false`, {
                method: 'GET',
                params
            })).pipe(
                filter(resp => resp.status === 200),
                map(resp => resp.result as any[]),
                flatMap(data => of(...data)),
                filter((data: any) => (data.properties?.type || []).includes('api')),
                map(item => ({
                    title: item.name,
                    key: item.id,
                    children: item.actions.map((child: any) => ({
                        title: child.name,
                        key: child.action,
                    }))
                })),
                toArray()
            )),
        auth: (params: any) => defer(() => from(
            request(`/jetlinks/autz-setting/_query/no-paging?paging=false`, {
                method: 'GET',
                params
            })).pipe(
                filter(resp => resp.status === 200),
                map(resp => resp.result),
                flatMap(data => of(...data)),
                map(item => ({
                    key: item.permission,
                    actions: item.actions
                })),
                toArray(),
            )),
        save: (data: any) => defer(() => from(
            request(`/jetlinks/autz-setting/detail/_save`, {
                method: 'POST',
                data
            })).pipe(
                filter(resp => resp.status === 200),
            ))
    }
Example #17
Source File: blocking-action.service.spec.ts    From capture-lite with GNU General Public License v3.0 5 votes vote down vote up
describe('BlockingActionService', () => {
  let service: BlockingActionService;
  let htmlIonLoadingElementSpy: jasmine.SpyObj<HTMLIonLoadingElement>;
  const value1 = 8;
  const value2 = 9;
  const action$ = of(value1, value2);
  const errorMessage = 'error!';
  const errorAction$ = action$.pipe(
    map(() => {
      throw new Error(errorMessage);
    })
  );

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [SharedTestingModule],
    });
    service = TestBed.inject(BlockingActionService);
    const loadingController = TestBed.inject(LoadingController);

    htmlIonLoadingElementSpy = jasmine.createSpyObj('HTMLIonLoadingElement', {
      present: new Promise<void>(resolve => resolve()),
      dismiss: new Promise<boolean>(resolve => resolve(true)),
    });

    spyOn(loadingController, 'create').and.resolveTo(htmlIonLoadingElementSpy);
  });

  it('should be created', () => expect(service).toBeTruthy());

  it('should return the same expected value to the action', done => {
    service
      .run$(action$)
      .pipe(toArray())
      .subscribe(result => {
        expect(result).toEqual([value1, value2]);
        done();
      });
  });

  it('should throw error if action has error', done => {
    service.run$(errorAction$).subscribe({
      error: (err: unknown) => {
        if (err instanceof Error) {
          expect(err.message).toEqual(errorMessage);
          done();
        }
      },
    });
  });

  it('should dismiss the blocking dialog when action is completed', done => {
    const subscription = service.run$(action$).subscribe({
      complete: () => {
        subscription.unsubscribe();
        expect(htmlIonLoadingElementSpy.dismiss).toHaveBeenCalled();
        done();
      },
    });
  });

  it('should dismiss the blocking dialog when action throws errors', done => {
    const subscription = service.run$(errorAction$).subscribe({
      error: () => {
        subscription.unsubscribe();
        expect(htmlIonLoadingElementSpy.dismiss).toHaveBeenCalled();
        done();
      },
    });
  });

  // TODO: See #263.
  // it('should block events from physical back button on Android', () => {});
});
Example #18
Source File: index.tsx    From jetlinks-ui-antd with MIT License 5 votes vote down vote up
Time: React.FC<Props> = props => {

    const { time } = props;
    const [data, setData] = useState<any>([]);
    useEffect(() => {
        // 后端提供了distTimeList字段,可直接取。
        new Observable(sink => {
            Object.keys(time || {}).forEach(e => sink.next({ time: e, value: time[e] }));
            sink.complete();
        }).pipe(
            windowCount(2, 1),
            mergeMap(i => i.pipe(toArray())),
            filter(arr => arr.length > 0),
            map((arr: any[]) => ({
                label: arr.length === 1 ?
                    `>=${arr[0].time}` :
                    arr.map(i => i.time).join('~'), value: arr[0].value
            })),
            toArray(),
        ).subscribe((result) => { setData(result) })

    }, [time]);

    const cols = {
        label: {
            min: 0,
            max: 100,
            alias: 'ms'
        },
        value: {
            alias: '次',
            min: 0,
            ticks: [0, 40, 80, 120, 160]
        }
    };
    // x :ms 
    // y :个
    return (
        <Chart height={350} data={data} scale={cols} forceFit>

            <span className='sub-title' style={{ marginLeft: 40, fontWeight: 700 }}>
                连接时间分布
             </span>
            <Axis name="label"
                label={{ autoRotate: false }}
                title={{
                    autoRotate: false,
                    position: 'end',
                    offset: 15,
                    textStyle: {
                        fontSize: '12',
                        textAlign: 'center',
                        fill: '#999',
                        fontWeight: 'bold',
                        // rotate: 0,
                    }
                }} />
            <Axis name="value"
                label={{ autoRotate: false }}
                title={{
                    autoRotate: false,
                    position: 'center',
                    offset: 40,
                    textStyle: {
                        fontSize: '12',
                        textAlign: 'right',
                        fill: '#999',
                        fontWeight: 'bold',
                        rotate: 0,
                    }
                }} />
            <Tooltip
                crosshairs={{
                    type: "y"
                }}
            />
            <Geom type="interval" position="label*value" />
        </Chart>
    )
}
Example #19
Source File: misc-helpers.ts    From s-libs with MIT License 5 votes vote down vote up
export async function pipeAndCollect<I, O>(
  source: I[],
  operator: OperatorFunction<I, O>,
): Promise<O[]> {
  return lastValueFrom(of(...source).pipe(operator, toArray()));
}
Example #20
Source File: emote.component.ts    From App with MIT License 5 votes vote down vote up
readAuditActivity(): Observable<AuditLogEntry[]> {
		return this.emote?.getAuditActivity().pipe(
			toArray()
		) ?? of([]);
	}
Example #21
Source File: user.structure.ts    From App with MIT License 5 votes vote down vote up
getEditors(): Observable<UserStructure[]> {
		return this.dataOnce().pipe(
			map(data => data?.editors ?? []),
			mergeAll(),
			map(u => this.dataService.get('user', { id: u.id })[0]),
			toArray()
		);
	}
Example #22
Source File: RestApi.ts    From majsoul-api with MIT License 5 votes vote down vote up
private async getGames(query: FilterQuery<store.GameResult<ObjectId>>): Promise<store.GameResult<ObjectId>[]> {
		const games = await this.mongoStore.gamesCollection.find(query).toArray();
		return await this.correctGames(games);
	}
Example #23
Source File: notify-menu.component.ts    From App with MIT License 5 votes vote down vote up
ngOnInit(): void {
		this.restService.v2.gql.query<{ user: DataStructure.TwitchUser; }>({
			query: `
				query GetUserNotifications($id: String!) {
					user(id: $id) {
						notifications {
							id, read, title, announcement,
							timestamp,
							read_at,
							users {
								id, login, display_name,
								profile_image_url,
								role { id, color }
							},
							emotes {
								id, name
							},
							message_parts {
								type, data
							}
						}
					}
				}
			`,
			variables: {
				id: this.clientService.impersonating.getValue()?.id ?? '@me'
			},
			auth: true
		}).pipe(
			map(res => res?.body?.data.user.notifications ?? []),
			map(x => this.dataService.add('notification', ...x)),

			switchMap(all => from(all).pipe(
				mergeMap(n => n.isRead().pipe(map(read => ({ n, read })))),
				filter(({ read }) => read === false),
				map(x => x.n),
				toArray(),
				tap(arr => arr.length === 0 ? this.allClear = true : noop())
			)),
			tap(x => this.unreadNotifications.next(x)),
			delay(0)
		).subscribe({
			complete: () => this.loaded = true
		});
	}
Example #24
Source File: user-home.component.ts    From App with MIT License 5 votes vote down vote up
ngOnInit(): void {
		this.user.pipe(
			filter(user => !!user && AppComponent.isBrowser.getValue()),
			takeUntil(this.destroyed),
			tap(user => this.currentUser = user),
			tap(user => this.emoteSlots.next(user.getSnapshot()?.emote_slots ?? 0)),

			switchMap(user => user.getAuditEntries().pipe(
				tap(entries => this.auditEntries = entries),
				mapTo(user)
			)),
			switchMap(user => user.isLive().pipe(
				map(live => this.isLive.next(live)),
				switchMap(() => user.getBroadcast()),
				map(broadcast => this.broadcast.next(broadcast)),
				mapTo(user)
			)),
			switchMap(user => scheduled([
				user.getEmotes().pipe(map(emotes => ({ type: 'channel', emotes }))),
				user.getOwnedEmotes().pipe(map(emotes => ({ type: 'owned', emotes })))
			], asapScheduler).pipe(
				concatAll(),
				mergeMap(s => from(s.emotes).pipe(
					mergeMap(em => this.shouldBlurEmote(em).pipe(map(blur => ({ blur, emote: em })))),
					map(x => x.blur ? this.blurred.add(x.emote.getID()) : noop()),
					toArray(),
					mapTo(s)
				)),
				take(2),
			))
		).subscribe({
			next: set => {
				switch (set.type) {
					case 'channel':
						this.channelEmotes = set.emotes;
						this.channelCount.next(set.emotes.length);
						break;
					case 'owned':
						this.ownedEmotes = set.emotes;
						this.ownedCount.next(set.emotes.length);
						break;
				}
			}
		});
	}
Example #25
Source File: user.structure.ts    From App with MIT License 5 votes vote down vote up
getEmotes(): Observable<EmoteStructure[]> {
		return this.dataOnce().pipe(
			map(data => data?.emotes ?? []),
			mergeAll(),
			map(e => this.dataService.get('emote', { id: e.id })[0]),
			toArray()
		);
	}
Example #26
Source File: RestApi.ts    From majsoul-api with MIT License 4 votes vote down vote up
public async init(root: { username: string, password: string }) {
		const secrets = getSecrets();
		this.oauth2Client = new google.auth.OAuth2(
			secrets.google.clientId,
			secrets.google.clientSecret,
			`${process.env.NODE_ENV === "production" ? "https" : `http`}://${process.env.NODE_ENV === "production" ? "riichi.moe" : `localhost:8080`}/rigging/google`
		);

		if (root?.username != null && root?.password != null) {
			const salt = crypto.randomBytes(24).toString("hex");
			const sha = crypto.createHash("sha256");
			await this.mongoStore.userCollection.findOneAndUpdate(
				{
					nickname: root.username,
				},
				{
					$setOnInsert: {
						password: {
							salt,
							hash: sha.update(`${root.password}:${salt}`).digest("hex")
						},
						scopes: ["root"]
					}
				},
				{ upsert: true }
			);
		}

		this.app.listen(9515, () => console.log(`Express started`));

		let privateKey: Buffer, publicKey: Buffer;
		try {
			privateKey = await RestApi.getKey("riichi.key.pem");
			publicKey = await RestApi.getKey("riichi.crt.pem");
		} catch (err) {
			console.log("Couldn't load keys for auth tokens, disabling rigging");
			console.log(err);
			return;
		}

		this.app.use(
			expressJwt({
				secret: publicKey,
				audience: "riichi.moe",
				issuer: "riichi.moe",
				algorithms: ["RS256"],
				credentialsRequired: true,
			}).unless({
				method: "GET"
			})
		).use(function (err, req, res, next) {
			if (err.name === 'UnauthorizedError') {
				res.status(401).send('token invalid');
				return;
			}
			next();
		})

			.get('/rigging/google',
				query("state").optional(),
				withData<{ state?: string }, any, { authUrl: string }>(async (data, req, res) => {
					const authUrl = this.oauth2Client.generateAuthUrl({
						access_type: 'offline',
						scope: [
							'https://www.googleapis.com/auth/spreadsheets'
						],
						state: data.state
					});
					res.send({
						authUrl
					})
				})
			)

			.patch('/rigging/google',
				body("code").isString().isLength({ min: 1 }),
				withData<{ code: string }, any, void>(async (data, req, res) => {
					const { tokens } = await this.oauth2Client.getToken(data.code);
					this.mongoStore.configCollection.updateMany({}, {
						$set: {
							googleRefreshToken: tokens.refresh_token
						}
					})
					res.send();
				})
			)

			.patch<any, store.Contest<ObjectId>>('/contests/:id',
				param("id").isMongoId(),
				body(nameofContest('majsoulFriendlyId')).not().isString().bail().isInt({ min: 100000, lt: 1000000 }).optional({ nullable: true }),
				body(nameofContest('type')).not().isString().bail().isNumeric().isWhitelisted(Object.keys(store.ContestType)).optional(),
				body(nameofContest('subtype')).not().isString().bail().isNumeric().isWhitelisted(Object.keys(store.TourneyContestPhaseSubtype)).optional(),
				body(nameofContest('anthem')).isString().bail().isLength({ max: 50 }).optional({ nullable: true }),
				body(nameofContest('spreadsheetId')).isString().bail().optional({ nullable: true }),
				body(nameofContest('tagline')).isString().bail().isLength({ max: 200 }).optional({ nullable: true }),
				body(nameofContest('taglineAlternate')).isString().bail().isLength({ max: 200 }).optional({ nullable: true }),
				body(nameofContest('displayName')).isString().bail().isLength({ max: 100 }).optional({ nullable: true }),
				body(nameofContest('initialPhaseName')).isString().bail().isLength({ max: 100 }).optional({ nullable: true }),
				body(nameofContest('maxGames')).not().isString().bail().isInt({ gt: 0, max: 50 }).optional({ nullable: true }),
				body(nameofContest('bonusPerGame')).not().isString().bail().isInt({ min: 0 }).optional({ nullable: true }),
				body(nameofContest('track')).not().isString().bail().isBoolean().optional({ nullable: true }),
				body(nameofContest('adminPlayerFetchRequested')).not().isString().bail().isBoolean().optional({ nullable: true }),
				oneOf([
					body(nameofContest('tourneyType')).not().isString().bail().isNumeric().isWhitelisted(Object.keys(store.TourneyContestScoringType)).optional(),
					body(nameofContest('tourneyType')).not().isString().bail().isArray({ min: 1 }).optional(),
				]),
				body(`${nameofContest('tourneyType')}.*.${nameofTourneyScoringType('type')}`).not().isString().bail().isNumeric().isWhitelisted(Object.keys(store.TourneyContestScoringType)),
				body(`${nameofContest('tourneyType')}.*.${nameofTourneyScoringType('typeDetails')}.${nameofTourneyScoringTypeDetails('findWorst')}`).not().isString().bail().isBoolean().optional({ nullable: true }),
				body(`${nameofContest('tourneyType')}.*.${nameofTourneyScoringType('typeDetails')}.${nameofTourneyScoringTypeDetails('gamesToCount')}`).not().isString().bail().isInt({ gt: 0 }).optional({ nullable: true }),
				body(`${nameofContest('tourneyType')}.*.${nameofTourneyScoringType('places')}`).not().isString().bail().isInt({ gt: 0 }).optional({ nullable: true }),
				body(`${nameofContest('tourneyType')}.*.${nameofTourneyScoringType('reverse')}`).not().isString().bail().isBoolean().optional({ nullable: true }),
				body(`${nameofContest('tourneyType')}.*.${nameofTourneyScoringType('suborder')}`).not().isString().bail().isArray().optional({ nullable: true }),
				body(`${nameofContest('tourneyType')}.*.${nameofTourneyScoringType('suborder')}.*.${nameofTourneyScoringType('type')}`)
					.not().isString().bail().isNumeric().isWhitelisted(Object.keys(store.TourneyContestScoringType)),
				body(`${nameofContest('tourneyType')}.*.${nameofTourneyScoringType('suborder')}.*.${nameofTourneyScoringType('places')}`)
					.not().isString().bail().isInt({ gt: 0 }).optional({ nullable: true }),
				body(`${nameofContest('tourneyType')}.*.${nameofTourneyScoringType('suborder')}.*.${nameofTourneyScoringType('reverse')}`)
					.not().isString().bail().isBoolean().optional({ nullable: true }),
				async (req, res) => {
					const errors = validationResult(req);
					if (!errors.isEmpty()) {
						return res.status(400).json({ errors: errors.array() } as any);
					}
					const update: {
						$set?: {},
						$unset?: {},
					} = {};
					const data: Partial<store.Contest<string>> = matchedData(req, { includeOptionals: true });

					if (data.majsoulFriendlyId != null) {
						try {
							const existingGame = await this.mongoStore.contestCollection.findOne({ majsoulFriendlyId: data.majsoulFriendlyId });
							if (existingGame != null && !existingGame._id.equals(data._id)) {
								res.status(400).send(`Contest #${existingGame._id.toHexString()} already subscribed to majsoul ID ${data.majsoulFriendlyId}` as any);
								return;
							};
						} catch (e) {
							res.status(500).send(e);
							return;
						}
					}

					for (const key in data) {
						if (key === "id") {
							continue;
						}

						if (data[key] === undefined) {
							continue;
						}

						if (key === nameofContest("majsoulFriendlyId")) {
							update.$unset ??= {};
							update.$unset[nameofContest("notFoundOnMajsoul")] = true;
						}

						if (data[key] === null) {
							update.$unset ??= {};
							update.$unset[key] = true;
							continue;
						}

						update.$set ??= {};
						update.$set[key] = data[key];
					}

					if (update.$set == null && update.$unset == null) {
						res.status(400).send("No operations requested" as any);
						return;
					}

					this.mongoStore.contestCollection.findOneAndUpdate(
						{ _id: new ObjectId(req.params.id) },
						update,
						{
							returnOriginal: false,
							projection: {
								teams: false,
								sessions: false,
							}
						}
					).then((contest) => {
						if (contest.value === null) {
							res.status(404).send();
							return;
						}
						res.send(contest.value);
					}).catch((err) => {
						console.log(err);
						res.status(500).send(err);
					})
				}
			)

			.put<any, string>('/games',
				body(nameofGameResult('contestId')).isMongoId().isString(),
				body(nameofGameResult('majsoulId')).isString(),
				logError<any, string>(
					async (req, res) => {
						const errors = validationResult(req);
						if (!errors.isEmpty()) {
							res.status(400).json({ errors: errors.array() } as any);
							return;
						}
						const data: Partial<store.GameResult<string>> = matchedData(req, { includeOptionals: true });
						const contestId = new ObjectId(data.contestId);
						const existingContest = await this.mongoStore.contestCollection.find({ _id: contestId }).toArray();
						if (existingContest.length <= 0) {
							res.status(400).send("Contest Id is invalid." as any);
							return;
						}

						const existingGame = await this.mongoStore.gamesCollection.find({ majsoulId: data.majsoulId }).toArray();

						if (existingGame.length > 0) {
							res.status(400).send(`Game with id ${data.majsoulId} already exists.` as any);
							return;
						}

						const gameResult = await this.mongoStore.gamesCollection.insertOne({
							contestId,
							majsoulId: data.majsoulId
						});

						res.send(JSON.stringify(gameResult.insertedId.toHexString()));
					}
				)
			)

			.patch('/games/:id',
				param("id").isMongoId(),
				body(nameofGameResult("hidden")).isBoolean().not().isString().optional({ nullable: true }),
				withData<{ id: string, hidden?: boolean }, any, Partial<GameResult>>(async (data, req, res) => {
					const gameId = new ObjectId(data.id);
					const [game] = await this.mongoStore.gamesCollection.find({
						_id: gameId
					}).toArray();

					if (!game) {
						res.sendStatus(404);
						return;
					}

					const update: {
						$set?: {},
						$unset?: {},
					} = {};

					for (const key in data) {
						if (data[key] === undefined) {
							continue;
						}

						if (data[key] === null) {
							update.$unset ??= {};
							update.$unset[key] = true;
							continue;
						}

						update.$set ??= {};
						update.$set[key] = data[key];
					}

					if (update.$set == null && update.$unset == null) {
						res.status(400).send("No operations requested" as any);
						return;
					}

					const result = await this.mongoStore.gamesCollection.findOneAndUpdate(
						{
							_id: gameId
						},
						update,
						{
							returnOriginal: false,
							projection: {
								rounds: false
							}
						}
					);

					res.send(result.value);
				})
			)

			.delete<any, void>('/games/:id',
				param("id").isMongoId(),
				logError(async (req, res) => {
					const errors = validationResult(req);
					if (!errors.isEmpty()) {
						res.status(400).json({ errors: errors.array() } as any);
						return;
					}
					const data = matchedData(req, { includeOptionals: true }) as { id: string; };
					const gameId = new ObjectId(data.id);

					const result = await this.mongoStore.gamesCollection.deleteOne({
						_id: gameId
					})

					res.send();
				})
			)

			.put<any, string>('/games/custom',
				body(nameofGameResult('contestId')).isMongoId().isString(),
				logError<any, string>(
					async (req, res) => {
						const errors = validationResult(req);
						if (!errors.isEmpty()) {
							res.status(400).json({ errors: errors.array() } as any);
							return;
						}
						const data: Partial<store.GameResult<string>> = matchedData(req, { includeOptionals: true });
						const contestId = new ObjectId(data.contestId);
						const existingContest = await this.mongoStore.contestCollection.find({ _id: contestId }).toArray();
						if (existingContest.length <= 0) {
							res.status(400).send("Contest Id is invalid." as any);
							return;
						}

						const now = Date.now();

						const gameResult = await this.mongoStore.gamesCollection.insertOne({
							contestId,
							notFoundOnMajsoul: true,
							start_time: now,
							end_time: now,
						});

						res.send(JSON.stringify(gameResult.insertedId.toHexString()));
					}
				)
			)

			.patch('/games/custom/:id',
				param("id").isMongoId(),
				body(nameofGameResult("finalScore")).not().isString().bail().isArray({ max: 4, min: 4 }).optional(),
				body(`${nameofGameResult("finalScore")}.*.score`).isInt().not().isString(),
				body(`${nameofGameResult("finalScore")}.*.uma`).isInt().not().isString(),
				body(nameofGameResult("players")).not().isString().bail().isArray({ max: 4, min: 4 }).optional(),
				body(`${nameofGameResult("players")}.*._id`).isMongoId(),
				withData<{ id: string, hidden?: boolean }, any, Partial<GameResult>>(async (data, req, res) => {
					const gameId = new ObjectId(data.id);
					const [game] = await this.mongoStore.gamesCollection.find({
						_id: gameId,
						majsoulId: {
							$exists: false,
						}
					}).toArray();

					if (!game) {
						res.sendStatus(404);
						return;
					}

					const update: {
						$set?: {},
						$unset?: {},
					} = {};

					for (const key in data) {
						if (key === "id") {
							continue;
						}

						if (data[key] === undefined) {
							continue;
						}

						if (data[key] === null) {
							update.$unset ??= {};
							update.$unset[key] = true;
							continue;
						}

						update.$set ??= {};

						if (key === "players") {
							update.$set[key] = data[key].map(({_id}) => ({
								_id: ObjectID.createFromHexString(_id)
							}))
							continue;
						}

						update.$set[key] = data[key];
					}

					if (update.$set == null && update.$unset == null) {
						res.status(400).send("No operations requested" as any);
						return;
					}

					const result = await this.mongoStore.gamesCollection.findOneAndUpdate(
						{
							_id: gameId
						},
						update,
						{
							returnOriginal: false,
						}
					);

					res.send(result.value);
				})
			)

			.put<any, string>('/corrections',
				body(nameofGameCorrection('gameId')).isMongoId().isString(),
				logError<any, string>(
					async (req, res) => {
						const errors = validationResult(req);
						if (!errors.isEmpty()) {
							res.status(400).json({ errors: errors.array() } as any);
							return;
						}
						const data: Partial<store.GameCorrection<string>> = matchedData(req, { includeOptionals: true });
						const gameId = new ObjectId(data.gameId);
						const game = await this.mongoStore.gamesCollection.find({ _id: gameId }).toArray();
						if (game.length <= 0) {
							res.status(400).send("Game doesn't exist." as any);
							return;
						}

						const existingCorrection = await this.mongoStore.gameCorrectionsCollection.find({ gameId: gameId }).toArray();

						if (existingCorrection.length > 0) {
							res.status(400).send(`Correction for that game id already exists.` as any);
							return;
						}

						const gameResult = await this.mongoStore.gameCorrectionsCollection.insertOne({
							gameId,
						});

						res.send(JSON.stringify(gameResult.insertedId.toHexString()));
					}
				)
			)

			.patch('/corrections/:id',
				param("id").isMongoId(),
				body(nameofGameCorrection("finalScore")).isArray().not().isString().optional({ nullable: true }),
				body(`${nameofGameCorrection("finalScore")}.*.uma`).isInt().not().isString().optional({ nullable: true }),
				body(`${nameofGameCorrection("finalScore")}.*.score`).isInt().not().isString().optional({ nullable: true }),
				withData<{ id: string, hidden?: boolean }, any, Partial<GameResult>>(async (data, req, res) => {
					const correctionId = new ObjectId(data.id);
					const [game] = await this.mongoStore.gameCorrectionsCollection.find({
						_id: correctionId
					}).toArray();

					if (!game) {
						res.sendStatus(404);
						return;
					}

					const update: {
						$set?: {},
						$unset?: {},
					} = {};

					for (const key in data) {
						if (data[key] === undefined) {
							continue;
						}

						if (data[key] === null) {
							update.$unset ??= {};
							update.$unset[key] = true;
							continue;
						}

						update.$set ??= {};
						update.$set[key] = data[key];
					}

					if (update.$set == null && update.$unset == null) {
						res.status(400).send("No operations requested" as any);
						return;
					}

					const result = await this.mongoStore.gameCorrectionsCollection.findOneAndUpdate(
						{
							_id: correctionId
						},
						update,
						{
							returnOriginal: false,
							projection: {
								rounds: false
							}
						}
					);

					res.send(result.value);
				})
			)

			.delete<any, void>('/corrections/:id',
				param("id").isMongoId(),
				logError(async (req, res) => {
					const errors = validationResult(req);
					if (!errors.isEmpty()) {
						res.status(400).json({ errors: errors.array() } as any);
						return;
					}
					const data = matchedData(req, { includeOptionals: true }) as { id: string; };
					const correctionId = new ObjectId(data.id);

					const result = await this.mongoStore.gameCorrectionsCollection.deleteOne({
						_id: correctionId
					})

					res.send();
				})
			)

			.put<any, store.Contest<string>>('/contests', (req, res) => {
				this.mongoStore.contestCollection.insertOne({}).then(result => res.send({ _id: result.insertedId.toHexString() }));
			})

			.delete<any, void>('/contests/:id',
				param("id").isMongoId(),
				logError(async (req, res) => {
					const errors = validationResult(req);
					if (!errors.isEmpty()) {
						res.status(400).json({ errors: errors.array() } as any);
						return;
					}

					const data: Partial<store.Contest<string>> = matchedData(req, { includeOptionals: true });
					const contestId = new ObjectId(data._id);

					await this.mongoStore.configCollection.findOneAndUpdate(
						{ featuredContest: contestId },
						{
							$unset: { featuredContest: true }
						});

					const result = await this.mongoStore.contestCollection.deleteOne({
						_id: contestId
					})

					await this.mongoStore.configCollection.findOneAndUpdate({
						trackedContest: contestId
					}, {
						$unset: {
							trackedContest: true
						}
					})

					res.send();
				})
			)

			.patch<any, store.Config<ObjectId>>('/config',
				body(nameofConfig('featuredContest')).isMongoId().optional({ nullable: true }),
				withData<Partial<store.Config<string>>, any, store.Config<ObjectId>>(async (data, req, res) => {
					if (data.featuredContest != null) {
						const existingContest = await this.mongoStore.contestCollection.findOne({ _id: new ObjectId(data.featuredContest) });
						if (existingContest == null) {
							res.status(400).send(`Featured contest #${data._id} doesn't exist.` as any);
							return;
						};
					}

					const update: {
						$set?: {},
						$unset?: {},
					} = {};

					for (const key in data) {
						if (data[key] === undefined) {
							continue;
						}

						if (data[key] === null) {
							update.$unset ??= {};
							update.$unset[key] = true;
							continue;
						}

						update.$set ??= {};
						update.$set[key] = key === nameofConfig("featuredContest") ? new ObjectId(data[key] as string) : data[key];
					}

					if (update.$set == null && update.$unset == null) {
						res.status(400).send("No operations requested" as any);
						return;
					}

					const [existingConfig] = await this.mongoStore.configCollection.find().toArray();
					if (existingConfig == null) {
						res.status(404).send();
						return;
					}

					const updatedConfig = await this.mongoStore.configCollection.findOneAndUpdate(
						{ _id: existingConfig._id },
						update,
						{
							returnOriginal: false,
							projection: {
								googleRefreshToken: false
							}
						}
					);

					if (updatedConfig.value === null) {
						res.status(404).send();
						return;
					}
					res.send(updatedConfig.value);
				})
			)

			.put('/sessions',
				body(nameofSession("contestId")).isMongoId(),
				withData<Partial<store.Session<string | ObjectId>>, any, store.Session<ObjectId>>(async (data, req, res) => {
					const contestId = await this.contestExists(data.contestId as string);
					if (!contestId) {
						res.status(400).send(`contest #${data.contestId} not found` as any);
						return;
					}

					const [lastSession] = await this.mongoStore.sessionsCollection
						.find()
						.sort(nameofSession("scheduledTime"), -1)
						.limit(1)
						.toArray();

					const session = await this.mongoStore.sessionsCollection.insertOne(
						{
							scheduledTime: (lastSession?.scheduledTime ?? Date.now()) + (24 * 60 * 60 * 1000),
							contestId,
							plannedMatches: [],
						},
					);

					res.send(session.ops[0]);
				})
			)

			.patch('/sessions/:id',
				param("id").isMongoId(),
				body(nameofSession("scheduledTime")).not().isString().bail().isInt({ min: 0 }).optional(),
				body(nameofSession("name")).isString().optional({ nullable: true }),
				body(nameofSession("isCancelled")).not().isString().bail().isBoolean().optional({ nullable: true }),
				body(nameofSession("plannedMatches")).not().isString().bail().isArray().optional(),
				body(`${nameofSession("plannedMatches")}.*.teams`).not().isString().bail().isArray({ max: 4, min: 4 }),
				body(`${nameofSession("plannedMatches")}.*.teams.*._id`).isMongoId(),
				withData<{
					id: string;
				} & Partial<store.Session<string | ObjectId>>, any, store.Session<ObjectId>>(async (data, req, res) => {
					if (data.plannedMatches && data.plannedMatches.length > 0) {
						const teamIds = data.plannedMatches.map(match => match.teams.map(team => team._id as string)).flat();
						const uniqueTeams = new Set(teamIds.map(id => id));

						const sessionId = new ObjectId(data.id);

						const [session] = await this.mongoStore.sessionsCollection.find({
							_id: sessionId
						}).toArray();

						if (!session) {
							res.status(404).send();
							return;
						}

						const [contest] = await this.mongoStore.contestCollection.find({
							_id: session.contestId,
							"teams._id": {
								$all: Array.from(uniqueTeams).map(id => new ObjectId(id))
							}
						}).toArray();

						if (!contest) {
							res.status(400).send(`One of team ids ${teamIds.map(id => `#${id}`).join(", ")} doesn't exist.` as any);
							return;
						}
					}

					const update: {
						$set?: {},
						$unset?: {},
					} = {};

					for (const key in data) {
						if (key === "id") {
							continue;
						}

						if (data[key] === undefined) {
							continue;
						}

						if (data[key] === null) {
							update.$unset ??= {};
							update.$unset[key] = true;
							continue;
						}

						update.$set ??= {};
						update.$set[key] = data[key];
					}

					if (update.$set == null && update.$unset == null) {
						res.status(400).send("No operations requested" as any);
						return;
					}

					const session = await this.mongoStore.sessionsCollection.findOneAndUpdate(
						{ _id: new ObjectId(data.id) },
						update,
						{
							returnOriginal: false,

						}
					);

					if (!session.value) {
						res.status(404).send();
						return;
					}

					res.send(session.value);
				})
			)

			.delete('/sessions/:id',
				param("id").isMongoId(),
				withData<{ id: string }, any, store.Session<ObjectId>>(async (data, req, res) => {
					const result = await this.mongoStore.sessionsCollection.deleteOne(
						{
							_id: new ObjectId(data.id)
						}
					);

					if (result.deletedCount <= 0) {
						res.sendStatus(404);
					}
					res.send();
				})
			)

			.patch('/contests/:id/teams/:teamId',
				param("id").isMongoId(),
				param("teamId").isMongoId(),
				body(nameofTeam('image')).isString().optional({ nullable: true }),
				body(nameofTeam('imageLarge')).isString().optional({ nullable: true }),
				body(nameofTeam('name')).isString().optional({ nullable: true }),
				body(nameofTeam('players')).isArray().optional(),
				body(`${nameofTeam('players')}.*._id`).isMongoId(),
				body(nameofTeam('anthem')).isString().optional({ nullable: true }),
				body(nameofTeam('color')).isString().matches(/^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/).optional({ nullable: true }),
				body(nameofTeam("contrastBadgeFont")).isBoolean().not().isString().optional({ nullable: true }),
				withData<
					{
						id: string;
						teamId: string;
					} & Partial<store.ContestTeam<ObjectId | string>>,
					any,
					store.ContestTeam<ObjectId>
				>(async (data, req, res) => {
					const update: {
						$set?: {},
						$unset?: {},
					} = {};

					const id = new ObjectId(data.id);
					const teamId = new ObjectId(data.teamId);

					if (data.players) {
						for (const player of data.players) {
							player._id = new ObjectId(player._id);
						}
						const players = await this.mongoStore.playersCollection.find({
							_id: { $in: data.players.map(player => player._id as ObjectId) }
						}).toArray();
						if (players.length !== data.players.length) {
							res.status(400).send(
								`Players ${data.players
									.filter(player => !players.find(p => p._id.equals(player._id)))
									.map(player => `#${player._id}`)
									.join(", ")
								} not found.` as any
							);
							return;
						}
					}

					for (const key in data) {
						if (data[key] === undefined) {
							continue;
						}

						if (key === "id" || key === "teamId") {
							continue;
						}

						const updateKey = `teams.$.${key}`;

						if (data[key] === null) {
							update.$unset ??= {};
							update.$unset[updateKey] = true;
							continue;
						}

						update.$set ??= {};
						update.$set[updateKey] = data[key];
					}

					if (update.$set == null && update.$unset == null) {
						res.status(400).send("No operations requested" as any);
						return;
					}

					this.mongoStore.contestCollection.findOneAndUpdate(
						{
							_id: id,
							teams: { $elemMatch: { _id: teamId } }
						},
						update,
						{ returnOriginal: false, projection: { teams: true } }
					).then((contest) => {
						res.send(contest.value.teams.find(team => team._id.equals(teamId)));
					}).catch((err) => {
						console.log(err);
						res.status(500).send(err);
					})
				}
				))

			.put('/contests/:id/teams/',
				param("id").isMongoId(),
				withData<
					{
						id: string;
					},
					any,
					store.ContestTeam<ObjectId>
				>(async (data, req, res) => {
					const contestId = await this.contestExists(data.id);
					if (!contestId) {
						res.sendStatus(404);
						return;
					}

					const team = {
						_id: new ObjectId()
					};

					await this.mongoStore.contestCollection.findOneAndUpdate(
						{
							_id: contestId,
						},
						{
							$push: {
								teams: team
							}
						},
						{ returnOriginal: false, projection: { teams: true } }
					)

					res.send(team);
				}
				))

			.delete('/contests/:id/teams/:teamId',
				param("id").isMongoId(),
				param("teamId").isMongoId(),
				withData<
					{
						id: string;
						teamId: string;
					},
					any,
					store.ContestTeam<ObjectId>
				>(async (data, req, res) => {
					const [contest] = await this.mongoStore.contestCollection.find(
						{
							_id: new ObjectId(data.id),
							teams: { $elemMatch: { _id: new ObjectId(data.teamId) } }
						},
					).toArray();

					if (contest == null) {
						res.sendStatus(404);
						return;
					}

					const teamId = new ObjectId(data.teamId);

					await this.mongoStore.contestCollection.findOneAndUpdate(
						{
							_id: contest._id,
						},
						{
							$pull: {
								teams: {
									_id: teamId
								}
							}
						},
						{ returnOriginal: false, projection: { teams: true } }
					)

					res.send();
				}
				))

			.put("/contests/:id/transitions",
				param("id").isMongoId(),
				body(nameofTransition("startTime")).isInt({ min: 0 }).not().isString(),
				body(nameofTransition("name")).isString(),
				body(`${nameofTransition("score")}.half`).isBoolean().not().isString().optional(),
				body(`${nameofTransition("score")}.nil`).isBoolean().not().isString().optional(),
				body(`${nameofTransition("teams")}.top`).isInt({ min: 4 }).not().isString().optional(),
				withData<
					Partial<store.ContestPhaseTransition> & {
						id: string,
					},
					any,
					Pick<store.ContestPhaseTransition<ObjectId>, "_id">
				>(async (data, req, res) => {
					const contest = await this.findContest(data.id);
					if (!contest) {
						res.status(404).send();
						return;
					}

					const transition: ContestPhaseTransition<ObjectId> = {
						_id: new ObjectId(),
						startTime: data.startTime,
						name: data.name,
						score: data.score,
						teams: data.teams,
					}

					if (contest.transitions) {
						this.mongoStore.contestCollection.findOneAndUpdate(
							{ _id: new ObjectId(data.id) },
							{
								$push: {
									transitions: transition,
								}
							}
						);
					} else {
						this.mongoStore.contestCollection.findOneAndUpdate(
							{ _id: new ObjectId(data.id) },
							{
								$set: {
									transitions: [transition],
								}
							}
						);
					}
					res.send({ _id: transition._id });
				})
			)

			.delete("/contests/:contestId/transitions/:id",
				param("contestId").isMongoId(),
				param("id").isMongoId(),
				withData<{ contestId: string; id: string }, any, void>(async (data, req, res) => {
					const contest = await this.findContest(data.contestId);
					if (!contest) {
						res.sendStatus(404);
						return;
					}

					this.mongoStore.contestCollection.findOneAndUpdate({ _id: ObjectId.createFromHexString(data.contestId) },
						{
							$pull: {
								transitions: {
									_id: ObjectId.createFromHexString(data.id)
								}
							}
						}
					);

					res.send();
				})
			)

			.patch('/players/:id',
				param("id").isMongoId(),
				body(nameofPlayer("displayName")).isString().optional({nullable: true}),
				withData<Partial<store.Player<string | ObjectId> & {id: string}>, any, Store.Player<ObjectId>>(async (data, req, res) => {
					const player = await this.mongoStore.playersCollection.findOneAndUpdate(
						{
							_id: ObjectId.createFromHexString(data.id as string)
						},
						data.displayName == null
							? {
								$unset: {
									displayName: true
								}
							}
							: {
								$set: {
									displayName: data.displayName
								}
							},
						{
							returnOriginal: false
						}
					);

					if (!player.ok) {
						res.status(404).send();
					}

					res.send(player.value);
				})
			)

			.put('/players/',
				body(nameofPlayer("majsoulFriendlyId")).not().isString().bail().isNumeric(),
				withData<Partial<store.Player<string | ObjectId>>, any, Store.Player<ObjectId>>(async (data, req, res) => {
					const result = await this.mongoStore.playersCollection.insertOne({
						majsoulFriendlyId: data.majsoulFriendlyId
					});
					res.send(result.ops[0]);
				})
			)

			.get("/rigging/token", async (req, res) => {
				const user = await this.mongoStore.userCollection.findOne({
					nickname: req.header("Username") as string,
				});

				if (!user) {
					res.sendStatus(401);
					return;
				}

				const sha = crypto.createHash("sha256");
				if (user.password.hash !== sha.update(`${req.header("Password") as string}:${user.password.salt}`).digest("hex")) {
					res.sendStatus(401);
					return;
				}

				jwt.sign(
					{
						name: user.nickname,
						roles: user.scopes
					},
					privateKey,
					{
						algorithm: 'RS256',
						issuer: "riichi.moe",
						audience: "riichi.moe",
						expiresIn: "1d",
						notBefore: 0,
					},
					(err, token) => {
						if (err) {
							console.log(err);
							res.status(500).send(err);
							return;
						}
						res.send(token);
					});
			});
	}
Example #27
Source File: RestApi.ts    From majsoul-api with MIT License 4 votes vote down vote up
constructor(private readonly mongoStore: store.Store) {
		this.app = express();
		this.app.use(cors());
		this.app.use(express.json({ limit: "1MB" }));

		this.app.get<any, store.Contest<ObjectId>[]>('/contests', (req, res) => {
			this.mongoStore.contestCollection
				.find()
				.project({
					majsoulFriendlyId: true,
					name: true,
					displayName: true,
				})
				.toArray()
				.then(contests => res.send(contests))
				.catch(error => res.status(500).send(error));
		});

		this.app.get<any, store.Contest<ObjectId>>('/contests/featured', logError(async (req, res) => {
			const [config] = await this.mongoStore.configCollection.find()
				.project({
					googleRefreshToken: false
				}).limit(1)
				.toArray();
			const query: FilterQuery<store.Contest<ObjectId>> = {};
			if (config.featuredContest != null) {
				query._id = config.featuredContest;
			}

			this.mongoStore.contestCollection
				.find(query)
				.sort({ _id: -1 })
				.limit(1)
				.project({
					_id: true
				})
				.toArray()
				.then(contests => res.send(contests[0]))
				.catch(error => res.status(500).send(error));
		}));

		this.app.get('/contests/:id',
			param("id").isMongoId(),
			withData<{ id: string }, any, Contest<ObjectId>>(async (data, req, res) => {
				const contest = await this.findContest(data.id);
				if (contest === null) {
					res.status(404).send();
					return;
				}

				const { phases } = await this.getPhases(data.id);

				res.send({ ...contest, phases });
			})
		);

		this.app.get('/contests/:id/images',
			param("id").isMongoId(),
			query("large").isBoolean().optional({nullable: false}),
			query("teams").optional({nullable: false}),
			withData<{ id: string, large: "true" | "false", teams: string }, any, store.Contest<ObjectId>>(async (data, req, res) => {
				const contest = await this.findContest(data.id, {
					projection: {
						[`teams._id`]: true,
						[`teams.image${data.large === "true" ? "Large" : ""}`]: true,
					}
				});

				if (contest === null) {
					res.status(404).send();
					return;
				}

				if (data.teams) {
					const teams = data.teams.split(' ');
					contest.teams = contest.teams.filter(team => teams.find(id => team._id.toHexString() === id))
				}

				res.send(contest);
			}));

		this.app.get<any, store.GameResult<ObjectId>>('/games/:id',
			param("id").isMongoId(),
			withData<{ id: string }, any, store.GameResult<ObjectId>>(async (data, req, res) => {
				const gameId = new ObjectId(data.id);
				const games = await this.getGames({
					_id: gameId
				});

				if (games.length < 1) {
					res.status(404).send();
					return;
				}
				res.send(games[0]);
			})
		)

		this.app.get('/contests/:id/pendingGames',
			param("id").isMongoId(),
			withData<{ id: string }, any, store.GameResult<ObjectId>[]>(async (data, req, res) => {
				const games = await this.getGames({
					contestId: new ObjectId(data.id),
					notFoundOnMajsoul: { $ne: false },
					contestMajsoulId: { $exists: false }
				});
				res.send(games);
			})
		);

		this.app.get('/contests/:id/phases',
			param("id").isMongoId(),
			withData<{ id: string }, any, Phase<ObjectId>[]>(async (data, req, res) => {
				const phaseInfo = await this.getPhases(data.id);

				if (!phaseInfo.contest) {
					res.sendStatus(404);
					return;
				}

				if (phaseInfo.contest.type === ContestType.League) {
					const phases = await this.getLeaguePhaseData(phaseInfo);
					res.send(phases);
					return;
				}

				res.sendStatus(500);
			})
		);

		this.app.get('/contests/:id/phases/active',
			param("id").isMongoId(),
			withData<{ id: string, phaseIndex: string }, any, Phase<ObjectId>>(async (data, req, res) => {
				const phaseInfo = await this.getPhases(data.id);

				if (!phaseInfo.contest) {
					res.sendStatus(404);
					return;
				}

				const now = Date.now();

				if (phaseInfo.contest.type === ContestType.League) {
					const phases = await this.getLeaguePhaseData(phaseInfo);
					res.send(phases.reverse().find(phase => phase.startTime < now));
					return;
				}

				if (phaseInfo.contest.tourneyType === TourneyContestScoringType.Cumulative) {
					res.status(500).send("Tourney subtype is not supported" as any);
					return
				}

				const phases = await this.getTourneyPhaseData(phaseInfo);
				res.send(phases.reverse().find(phase => phase.startTime < now) ?? phases[0]);
			})
		);

		this.app.get('/contests/:id/phases/:phaseIndex',
			param("id").isMongoId(),
			param("phaseIndex").isInt({ min: 0 }),
			withData<{ id: string, phaseIndex: string }, any, Phase<ObjectId>>(async (data, req, res) => {
				const phaseInfo = await this.getPhases(data.id);

				if (!phaseInfo.contest) {
					res.sendStatus(404);
					return;
				}

				const index = parseInt(data.phaseIndex);
				if (index >= phaseInfo.phases.length) {
					res.sendStatus(400);
					return;
				}

				const phases = await this.getLeaguePhaseData(phaseInfo);
				res.send(phases.find(phase => phase.index === index));
			})
		);

		this.app.get('/contests/:id/sessions',
			param("id").isMongoId(),
			withData<{ id: string }, any, Session<ObjectId>[]>(async (data, req, res) => {
				const phaseInfo = await this.getPhases(data.id);

				if (!phaseInfo.contest) {
					res.sendStatus(404);
					return;
				}

				const phases = await this.getLeaguePhaseData(phaseInfo);
				res.send(phases.reduce((total, next) =>
					total.concat(next.sessions), [])
				);
			})
		);

		this.app.get('/contests/:id/sessions/active',
			param("id").isMongoId(),
			withData<{ id: string }, any, Session<ObjectId>>(async (data, req, res) => {
				const phaseInfo = await this.getPhases(data.id);

				if (!phaseInfo.contest) {
					res.sendStatus(404);
					return;
				}

				const now = Date.now();

				const phases = await this.getLeaguePhaseData(phaseInfo);
				res.send(
					phases
						.reduce((total, next) => total.concat(next.sessions), [] as Session<ObjectId>[])
						.filter(session => session.scheduledTime < now)
						.reverse()[0]
				);
			})
		);

		this.app.get<any, store.Config<ObjectId>>('/config', (req, res) => {
			this.mongoStore.configCollection.find()
				.project({
					googleRefreshToken: false
				}).toArray()
				.then((config) => {
					if (config[0] == null) {
						res.sendStatus(404);
						return;
					}
					res.send(config[0]);
				})
				.catch(error => {
					console.log(error);
					res.status(500).send(error)
				});
		});

		this.app.get<any, GameResult<ObjectId>[]>('/games', async (req, res) => {
			const filter: FilterQuery<store.GameResult<ObjectId>> = {
				$and: [{
					$or: [
						{
							notFoundOnMajsoul: false,
						},
						{
							contestMajsoulId: { $exists: true },
						},
						{
							majsoulId: { $exists: false },
						},
					]
				}]
			};

			const contestIds = (req.query.contests as string)?.split(' ');
			if (contestIds) {
				const contests = await this.mongoStore.contestCollection.find(
					{
						$or: [
							{ majsoulFriendlyId: { $in: contestIds.map(id => parseInt(id)) } },
							{ _id: { $in: contestIds.map(id => ObjectId.isValid(id) ? ObjectId.createFromHexString(id) : null) } },
						]
					}
				).toArray();

				filter.$and.push(
					{
						$or: contestIds.map(string => ({
							contestId: { $in: contests.map(p => p._id) }
						}))
					}
				);
			}

			const sessionIds = (req.query?.sessions as string)?.split(' ');
			let sessionMap: {
				startSession: store.Session,
				endSession: store.Session
			}[] = [];
			if (sessionIds) {
				const sessions = await this.mongoStore.sessionsCollection.find({
					_id: { $in: sessionIds.map(id => new ObjectId(id)) }
				}).toArray();

				const sessionOr = [];
				for (const session of sessions) {
					let [startSession, endSession] = await this.mongoStore.sessionsCollection.find(
						{
							contestId: session.contestId,
							scheduledTime: { $gte: session.scheduledTime }
						}
					).sort({ scheduledTime: 1 }).limit(2).toArray();

					sessionMap.push({
						startSession,
						endSession
					});

					const end_time: Condition<number> = {
						$gte: startSession.scheduledTime
					}

					if (endSession != null) {
						end_time.$lt = endSession.scheduledTime;
					}

					sessionOr.push({ end_time });
				}

				filter.$and.push({ $or: sessionOr });
			}

			const cursor = this.mongoStore.gamesCollection.find(filter);

			if (!req.query?.stats) {
				cursor.project({
					rounds: false,
				});
			}

			if (req.query?.last) {
				const last = parseInt(req.query.last as string);
				if (last && !isNaN(last)) {
					cursor.sort({ end_time: -1 })
						.limit(Math.min(last, 64));
				}
			} else {
				cursor.limit(64);
			}

			try {
				const games = await this.correctGames(await cursor.toArray());
				const contests = await this.mongoStore.contestCollection.find(
					{ majsoulId: { $in: [...new Set(games.map(g => g.contestMajsoulId))] } }
				).toArray();

				res.send(
					games.map(game => ({
						...game,
						sessionId: sessionMap.find((session) =>
							game.end_time >= session.startSession.scheduledTime
							&& (session.endSession == null || game.end_time < session.endSession.scheduledTime)
						)?.startSession?._id
					})).map(game => {
						if (req.query?.stats) {
							(game as any).stats = collectStats(game, minimumVersion(game), game.players.reduce((total, next) => (total[next._id.toHexString()] = true, total), {})).map(stats => stats?.stats);
							delete game.rounds;
						}
						return game;
					})
				);
			} catch (error) {
				console.log(error);
				res.status(500).send(error)
			}
		});

		this.app.get<any, GameCorrection<ObjectId>[]>('/corrections', async (req, res) => {
			const corrections = await this.mongoStore.gameCorrectionsCollection.find({}).toArray();
			res.send(corrections);
		});

		this.app.get<any, GameResult[]>('/contests/:contestId/players/:playerId/games', async (req, res) => {
			try {
				const contestId = await this.contestExists(req.params.contestId);
				if (!contestId) {
					res.sendStatus(404);
					return;
				}

				const games = await this.getGames({
					contestId: contestId,
					hidden: { $ne: true },
					$or: [
						{ notFoundOnMajsoul: false },
						{ contestMajsoulId: { $exists: true } },
						{ majsoulId: { $exists: false } },
					],
					"players._id": ObjectId.createFromHexString(req.params.playerId)
				});

				res.send(games.map(game => ({
					...game,
					contestId: contestId
				})));
			} catch (error) {
				console.log(error);
				res.status(500).send(error)
			}
		});

		this.app.get<any, YakumanInformation[]>('/contests/:contestId/yakuman', async (req, res) => {
			try {
				const contestId = await this.contestExists(req.params.contestId);
				if (!contestId) {
					res.sendStatus(404);
					return;
				}

				const games = await this.getGames({
					contestId: contestId,
					$or: [
						{ notFoundOnMajsoul: false },
						{ contestMajsoulId: { $exists: true } }
					],
					hidden: { $ne: true }
				});

				const yakumanGames = games
					.map(game => {
						return {
							game,
							yakumanAgari: game.rounds.map(({tsumo, round, rons}) => {
								if (isAgariYakuman(
									game,
									round,
									tsumo
								)) {
									return [tsumo] as AgariInfo[];
								}

								return rons?.filter(ron => isAgariYakuman(
									game,
									round,
									ron
								)) || [] as AgariInfo[];
							}).flat()
						}
					});

				const playerMap = (await this.mongoStore.playersCollection.find(
					{
						_id: {
							$in: yakumanGames.map(({ game, yakumanAgari }) => yakumanAgari.map(agari => game.players[agari.winner]._id)).flat()
						},
					},
					{
						projection: {
							_id: true,
							nickname: true,
							displayName: true,
							majsoulId: true,
						}
					}
				).toArray()).reduce((total, next) => (total[next._id.toHexString()] = next, total), {} as Record<string, store.Player>)

				res.send(
					yakumanGames
						.map(({ game, yakumanAgari }) => yakumanAgari.map(agari => {
							const player = playerMap[game.players[agari.winner]._id.toHexString()];
							return {
								han: agari.han,
								player: {
									nickname: player.displayName ?? player.nickname,
									_id: player._id.toHexString(),
									zone: Majsoul.Api.getPlayerZone(player.majsoulId)
								},
								game: {
									endTime: game.end_time,
									majsoulId: game.majsoulId,
								}
							}
						})).flat()
				);
			} catch (error) {
				console.log(error);
				res.status(500).send(error)
			}
		});

		this.app.get('/contests/:id/players',
			param("id").isMongoId(),
			query("gameLimit").isInt({ min: 0 }).optional(),
			query("ignoredGames").isInt({ min: 0 }).optional(),
			query("teamId").isMongoId().optional(),
			withData<{
				id: string;
				teamId?: string;
				gameLimit?: string;
				ignoredGames?: string;
			}, any, ContestPlayer[]>(async (data, req, res) => {
				const contest = await this.findContest(data.id, {
					projection: {
						_id: true,
						'teams._id': true,
						'teams.players._id': true,
						majsoulFriendlyId: true,
						bonusPerGame: true,
					}
				});

				if (contest == null) {
					res.sendStatus(404);
					return;
				}

				const contestMajsoulFriendlyId = contest.majsoulFriendlyId?.toString() ?? "";

				const team = data.teamId && contest.teams.find(team => team._id.equals(data.teamId));
				if (data.teamId && !team) {
					res.status(400).send(`Team ${data.teamId} doesn't exist` as any);
					return;
				}

				const playerIds = team?.players?.map(player => player._id) ?? [];

				const gameQuery: FilterQuery<store.GameResult<ObjectId>> = {
					contestId: contest._id,
					hidden: { $ne: true },
					$or: [
						{ notFoundOnMajsoul: false },
						{ contestMajsoulId: { $exists: true } },
						{ majsoulId: { $exists: false } }
					],
				}

				if (data.teamId) {
					gameQuery["players._id"] = {
						$in: playerIds
					}
				}

				const games = await this.getGames(gameQuery);

				let gameLimit = parseInt(data.gameLimit);
				if (isNaN(gameLimit)) {
					gameLimit = Infinity;
				}

				let ignoredGames = parseInt(data.ignoredGames);
				if (isNaN(ignoredGames)) {
					ignoredGames = 0;
				}

				const playerGameInfo = games.reduce<Record<string, ContestPlayer>>((total, game) => {
					game.players.forEach((player, index) => {
						if (player == null) {
							return;
						}

						if (data.teamId && !playerIds.find(id => id.equals(player._id))) {
							return;
						}

						const id = player._id.toHexString();
						if (!(id in total)) {
							total[id] = {
								...player,
								tourneyScore: 0,
								tourneyRank: undefined,
								gamesPlayed: 0,
								team: undefined
							};
						}

						total[id].gamesPlayed++;
						if (total[id].gamesPlayed <= ignoredGames || total[id].gamesPlayed > (gameLimit + ignoredGames)) {
							return;
						}
						total[id].tourneyScore += game.finalScore[index].uma + (contest.bonusPerGame ?? 0);
					});
					return total;
				}, {});

				const seededPlayersForContest = seededPlayerNames[contestMajsoulFriendlyId] ?? [];

				const seededPlayers = await this.mongoStore.playersCollection.find(
					{ nickname: { $in: seededPlayersForContest } }
				).toArray();

				for (const seededPlayer of seededPlayers) {
					const id = seededPlayer._id.toHexString();
					if (id in playerGameInfo) {
						continue;
					}
					playerGameInfo[id] = {
						...seededPlayer,
						tourneyScore: 0,
						tourneyRank: undefined,
						gamesPlayed: 0,
						team: undefined
					};
				}

				const players = await this.mongoStore.playersCollection.find(
					{ _id: { $in: Object.values(playerGameInfo).map(p => p._id).concat(playerIds) } },
					{ projection: { majsoulId: 0 } }
				).toArray();

				res.send(
					players.map(player => ({
						...playerGameInfo[player._id.toHexString()],
						...player,
						team: {
							teams: Object.entries(sakiTeams[contestMajsoulFriendlyId] ?? {})
								.filter(([team, players]) => players.indexOf(player.nickname) >= 0)
								.map(([team, _]) => team),
							seeded: seededPlayersForContest.indexOf(player.nickname) >= 0,
						}
					}))
						.filter(player => ignoredGames == 0 || player.gamesPlayed > ignoredGames || player.team.seeded)
						.sort((a, b) => b.tourneyScore - a.tourneyScore)
						.map((p, i) => ({ ...p, tourneyRank: i }))
				);
			}));

		this.app.get('/contests/:id/stats',
			param("id").isMongoId(),
			oneOf([
				query("team").isMongoId(),
				query("player").isMongoId(),
				query("players").isEmpty(),
			]),
			withData<{
				id?: string;
				team?: string;
				player?: string;
				players?: "";
			}, any, Record<string, Stats>>(async (data, req, res) => {
				const contest = await this.findContest(data.id);
				if (!contest) {
					res.sendStatus(404);
					return;
				}

				if ([data.team, data.player, data.players].filter(option => option != null).length !== 1) {
					res.status(401).send("Only one query allowed at a time." as any);
					return;
				}

				const query: FilterQuery<Store.GameResult<ObjectId>> = {
					contestId: contest._id,
					hidden: { $ne: true }
				};

				let playerMap: Record<string, ObjectId | boolean> = null;

				if (data.team) {
					const teamId = new ObjectId(data.team);
					const team = contest.teams.find(team => team._id.equals(teamId));
					if (team == null) {
						res.status(401).send(`Team #${teamId} not found` as any);
						return;
					}

					playerMap = (team.players ?? []).reduce((total, next) => (total[next._id.toHexString()] = teamId, total), {} as Record<string, ObjectId | boolean>)
				} else if (data.player != null) {
					const playerId = new ObjectId(data.player);
					const [player] = await this.mongoStore.playersCollection.find({
						_id: playerId
					}).toArray();

					if (player === null) {
						res.status(401).send(`Player #${playerId} not found!` as any);
						return;
					}

					playerMap = {
						[data.player]: true
					}
				}

				if (playerMap) {
					query.players = {
						$elemMatch: {
							_id: {
								$in: Object.keys(playerMap).map(ObjectId.createFromHexString)
							}
						}
					}
				}

				const games = await this.getGames(query);
				const [commonVersion, latestVersion] = games.reduce(
					([common, latest], next) => (
						[
							Math.min(common, minimumVersion(next)) as StatsVersion,
							Math.max(latest, minimumVersion(next)) as StatsVersion,
						]
					),
					[latestStatsVersion, StatsVersion.Undefined]
				);
				const gameStats = games.map(game => collectStats(game, minimumVersion(game), playerMap));

				if (data.team != null) {
					res.send({
						[data.team]: mergeStats(gameStats.flat(), latestVersion)
					});
					return;
				}

				const gamesByPlayer = gameStats.reduce((total, next) => {
					for (const stats of next) {
						const id = stats.playerId.toHexString();
						total[id] ??= [];
						total[id].push(stats);
					}
					return total;
				}, {} as Record<string, Stats[]>);

				res.send(Object.entries(gamesByPlayer).reduce((total, [key, value]) => (total[key] = mergeStats(value, latestVersion), total), {}));
			})
		);

		this.app.get('/players',
			query("name").optional(),
			query("limit").isInt({ gt: 0 }).optional(),
			withData<{
				name?: string;
				limit?: string;
			}, any, store.Player<ObjectId>[]>(async (data, req, res) => {
				const regex = new RegExp(`^${escapeRegexp(data.name)}.*$`);
				const cursor = this.mongoStore.playersCollection.find(
					{
						$or: [
							{
								displayName: { $regex: regex, $options: "i" }
							},
							{
								nickname: { $regex: regex, $options: "i" }
							}
						],
					},
					{
						projection: {
							_id: true,
							nickname: true,
							displayName: true,
						},
						sort: {
							nickname: 1,
						}
					}
				)

				if (data.limit) {
					cursor.limit(parseInt(data.limit))
				}

				res.send(await cursor.toArray());
			})
		);
	}
Example #28
Source File: RestApi.ts    From majsoul-api with MIT License 4 votes vote down vote up
private async getTourneyPhaseData({
		contest,
		transitions,
		phases
	}: PhaseInfo): Promise<TourneyPhase<ObjectID>[]> {
		const contestTypes: (TourneyScoringInfo & {id:string})[] = (
			Array.isArray(contest.tourneyType)
				? contest.tourneyType
				: [ {type: contest.tourneyType == null ? TourneyContestScoringType.Cumulative : contest.tourneyType } ]
		).map(type => ({...type, id: this.generateScoringTypeId(type)}));

		const games = await this.correctGames(
			await this.mongoStore.gamesCollection.find(
				{
					contestId: contest._id,
				},
				{
					sort: {
						end_time: 1
					}
				}
			).toArray()
		);

		const scoreTypeSet: Record<string, TourneyContestScoringDetailsWithId> = {};
		const scoreTypeLevels = [...contestTypes];
		while (scoreTypeLevels.length > 0) {
			const scoreTypeLevel = scoreTypeLevels.pop();
			const id = this.generateScoringTypeId(scoreTypeLevel);
			if (!(id in scoreTypeSet)) {
				scoreTypeSet[id] = {
					type: scoreTypeLevel.type,
					typeDetails: scoreTypeLevel.typeDetails,
					id,
				}
			};

			if (scoreTypeLevel.suborder) {
				scoreTypeLevels.push(
					...scoreTypeLevel.suborder?.map(type => ({...type, id: this.generateScoringTypeId(type)}))
				);
			}
		}

		const scoreTypes = Object.values(scoreTypeSet);
		const resultsByType = {} as Record<string, Record<string, PlayerContestTypeResults>>;
		for (const type of scoreTypes) {
			switch (type.type) {
				case TourneyContestScoringType.Consecutive: {
					resultsByType[type.id] = this.getConsectutiveResults(type, games, contest);
					break;
				} case TourneyContestScoringType.Cumulative: {
					resultsByType[type.id] = this.getCumulativeResults(games, contest);
					break;
				} case TourneyContestScoringType.Kans: {
					resultsByType[type.id] = this.getKanResults(games, contest);
					break;
				}
			}
		}

		let players = await this.mongoStore.playersCollection.find({
			_id: { $in: Object.keys(resultsByType[contestTypes[0].id]).map(ObjectId.createFromHexString) }
		}).toArray();

		const playerResults = players.map<PlayerTourneyStandingInformation>(player => ({
			player: {
				_id: player._id.toHexString(),
				nickname: player.displayName ?? player.nickname,
				zone: Majsoul.Api.getPlayerZone(player.majsoulId),
			},
			rank: 0,
			totalMatches: resultsByType[contestTypes[0].id][player._id.toHexString()].totalMatches,
			qualificationType: contestTypes[0].id,
			rankingDetails: {
				type: PlayerRankingType.Score,
				details: scoreTypes.reduce((total, type) => {
					const result = resultsByType[type.id][player._id.toHexString()];
					total[type.id] = {
						score: result.score,
						highlightedGameIds: result.highlightedGameIds,
						rank: result.rank,
					};
					return total;
				}, {} as PlayerScoreTypeRanking['details'])
			}
		})).reduce(
			(total, next) => (total[next.player._id] = next, total),
			{} as Record<string, PlayerTourneyStandingInformation>
		)

		this.rankPlayersUsingContestRules(
			Object.values(playerResults).map(player => ({
				_id: player.player._id,
				player: player
			})),
			contestTypes,
			resultsByType
		);

		if (contest.subtype === store.TourneyContestPhaseSubtype.TeamQualifier && contest.teams) {
			const freeAgents = players.filter(player => !contest.teams.find(team => team.players?.find(teamPlayer => player._id.equals(teamPlayer._id))))
				.map(player => playerResults[player._id.toHexString()]);

			const scoreRankings = {} as Record<string, PlayerScoreTypeRanking>;
			const teams = [
				{
					id: null,
					playerIds: players.map(player => player._id.toHexString())
				},
				...contest.teams.map(team => ({
					id: team._id.toHexString(),
					playerIds: team.players?.map(player => player._id.toHexString())
				}))
			];

			for (const team of teams) {
				const teamPlayerResults = [
					...team.playerIds?.map(player => playerResults[player]),
					...freeAgents
				].filter(player => player) ?? [];
				for (const result of teamPlayerResults) {
					if (result.rankingDetails.type !== PlayerRankingType.Team) {
						scoreRankings[result.player._id] = result.rankingDetails;
						result.rankingDetails = {
							type: PlayerRankingType.Team,
							details: {}
						}
					}

					result.rankingDetails.details[team.id] = {
						rank: 0,
						qualificationType: null,
						scoreRanking: this.copyScoreRanking(scoreRankings[result.player._id])
					}
				}

				for (const scoreType of [...scoreTypes]) {
					teamPlayerResults.sort((a, b) => scoreRankings[a.player._id].details[scoreType.id].rank - scoreRankings[b.player._id].details[scoreType.id].rank);
					let rank = 1;
					for (const player of teamPlayerResults) {
						if (player.rankingDetails.type !== PlayerRankingType.Team) {
							continue;
						}

						player.rankingDetails.details[team.id].scoreRanking.details[scoreType.id].rank = rank;
						rank++;
					}
				}

				this.rankPlayersUsingContestRules(
					Object.values(teamPlayerResults).map(player => ({
						_id: player.player._id,
						player: (player.rankingDetails as PlayerTeamRanking).details[team.id]
					})),
					contestTypes,
					resultsByType
				);
			}
		}

		for (const result of Object.values(playerResults)) {
			if (result.hasMetRequirements) {
				continue;
			}

			const type = scoreTypeSet[result.qualificationType];

			const targetGames = type.type === TourneyContestScoringType.Consecutive
				? (type.typeDetails?.gamesToCount ?? 5)
				: contest.maxGames;

			if (Number.isNaN(targetGames)) {
				continue;
			}

			if (result.totalMatches < contest.maxGames) {
				continue;
			}

			result.hasMetRequirements = true;
		}

		return [{
			index: 0,
			subtype: contest.subtype,
			name: contest?.initialPhaseName ?? "予選",
			startTime: contest.startTime,
			scoringTypes: scoreTypes,
			standings: Object.values(playerResults)
				.sort((a, b) => a.rank - b.rank)
		}];
	}
Example #29
Source File: index.tsx    From jetlinks-ui-antd with MIT License 4 votes vote down vote up
Save = (props: Props) => {
  const {  close, data } = props;

  const [types, setTypes] = useState<Type[]>([]);

  const queryType = () => {
    service
      .type()
      .pipe(
        mergeMap((data: Type[]) => from(data)),
        map((i: Type) => ({ label: i.name, value: i.id })),
        toArray(),
      )
      .subscribe((data: any) => {
        setTypes([...data])
      });
  };
  useEffect(() => {
    queryType();
  }, [data]);

  const { onFieldValueChange$ } = FormEffectHooks;

  const effects = () => {
    const { setFieldState } = actions;

    onFieldValueChange$('typeId').subscribe(({ value }) => {
      setFieldState(
        `*(shareConfig.adminUrl,shareConfig.addresses,shareConfig.virtualHost,shareConfig.username,shareConfig.password)`,
        state => {
          state.visible = value === 'rabbitmq';
          state.value = undefined;
        },
      );
      setFieldState(
        `*(shareConfig.url,shareConfig.username,shareConfig.password, shareConfig.schema, button)`,
        state => {
          state.visible = value === 'rdb';
          state.value = undefined;
        },
      );
      setFieldState(`*(shareConfig.bootstrapServers)`, state => {
        state.visible = value === 'kafka';
        state.value = undefined;
      });
    });
  };

  const schema: ISchema = {
    type: 'object',
    properties: {
      NO_NAME_FIELD_$0: {
        type: 'object',
        'x-component': 'mega-layout',
        'x-component-props': {
          grid: true,
          autoRow: true,
          full: true,
          responsive: {
            lg: 4,
            m: 2,
            s: 1,
          },
        },
        properties: {
          name: {
            title: '名称',
            'x-component': 'Input',
            'x-mega-props': {
              span: 2,
              labelCol: 6,
            },
            'x-rules': [{ required: true, message: '此字段必填' }],
          },
          typeId: {
            title: '类型',
            'x-component': 'Select',
            'x-component-props': {
              disabled: !!data.typeId,
              showSearch:true,
              optionFilterProp:'children'
            },
            'x-mega-props': {
              span: 2,
              labelCol: 6,
            },
            enum: types
          },
          'shareConfig.adminUrl': {
            title: '管理地址',
            'x-mega-props': {
              span: 4,
              labelCol: 3,
            },
            required: true,
            default: 'http://localhost:15672',
            visible: false,
            'x-component': 'Input',
          },
          'shareConfig.addresses': {
            title: '链接地址',
            'x-mega-props': {
              span: 2,
              labelCol: 6,
            },
            required: true,
            default: 'localhost:5672',
            visible: false,
            'x-component': 'Input',
          },
          'shareConfig.virtualHost': {
            title: '虚拟域',
            'x-mega-props': {
              span: 2,
              labelCol: 6,
            },
            required: true,
            visible: false,
            default: '/',
            'x-component': 'Input',
          },
          'shareConfig.url': {
            title: 'URL',
            'x-mega-props': {
              span: 2,
              labelCol: 6,
            },
            visible: false,
            'x-rules': [
              {
                required: true,
                message: 'URL必填',
              },
            ],
            'x-component': 'Input',
            "x-component-props": {
              placeholder: '请输入r2dbc或者jdbc连接地址'
            }
          },
          'shareConfig.username': {
            title: '用户名',
            'x-mega-props': {
              span: 2,
              labelCol: 6,
            },
            visible: false,
            'x-rules': [
              {
                required: true,
                message: '用户名必填',
              },
            ],
            'x-component': 'Input',
          },
          'shareConfig.password': {
            title: '密码',
            'x-mega-props': {
              span: 2,
              labelCol: 6,
            },
            visible: false,
            'x-rules': [
              {
                required: true,
                message: '密码必填',
              },
            ],
            'x-component': 'Input',
          },
          'shareConfig.schema': {
            title: 'schema',
            'x-mega-props': {
              span: 2,
              labelCol: 6,
            },
            visible: false,
            'x-rules': [
              {
                required: true,
                message: 'schema必填',
              },
            ],
            'x-component': 'Input',
          },
          'shareConfig.bootstrapServers': {
            title: '地址',
            'x-mega-props': {
              span: 4,
              labelCol: 3,
            },
            'x-rules': [
              {
                required: true,
                message: '地址必填',
              },
            ],
            visible: false,
            'x-component': 'Select',
            'x-component-props': {
              mode: 'tags',
            },
          },
          description: {
            title: '说明',
            'x-mega-props': {
              span: 4,
              labelCol: 3,
            },
            'x-component': 'TextArea',
            'x-component-props': {
              rows: 4,
            },
          },
        },
      },
    },
  };

  const save = (data: any) => {
    service.saveOrUpdate(data).subscribe(() => {
      message.success('保存成功');
      close();
    });
  };

  return (
    <Modal title="编辑" onCancel={close} visible={true} width={1000} onOk={() => actions.submit()}>
      <SchemaForm
        initialValues={data}
        className={styles.save}
        onSubmit={save}
        actions={actions}
        effects={effects}
        schema={schema}
        components={{
          Input,
          Select,
          ArrayTable,
          TextArea: Input.TextArea
        }}
      />
    </Modal>
  );
}