[^\/.]+)/requirement",
10 | views.ClassRequirementViewSet,
11 | basename="requirement",
12 | )
13 |
14 | urlpatterns = [path("", include(router.urls))]
15 |
--------------------------------------------------------------------------------
/deploy/galaxy/outputs.tf:
--------------------------------------------------------------------------------
1 | output "run_service_name" {
2 | value = module.siarnaq.run_service_name
3 | }
4 |
5 | output "sql_instance_ip" {
6 | value = module.siarnaq.sql_instance_ip
7 | }
8 |
9 | output "storage_public_name" {
10 | value = google_storage_bucket.public.name
11 | }
12 |
13 | output "storage_secure_name" {
14 | value = google_storage_bucket.secure.name
15 | }
16 |
17 | output "storage_frontend_name" {
18 | value = var.create_website ? google_storage_bucket.frontend[0].name : null
19 | }
20 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/user/migrations/0005_user_email_verified.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2025-11-30 17:04
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("user", "0004_alter_user_managers"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="user",
15 | name="email_verified",
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/frontend/src/components/elements/FormLabel.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | const FormLabel: React.FC<{
4 | label?: string;
5 | required?: boolean;
6 | className?: string;
7 | }> = ({ label, required = false, className }) => (
8 |
13 | {label}
14 | {required && * }
15 |
16 | );
17 |
18 | export default FormLabel;
19 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/teams/migrations/0007_teamprofile_has_report.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-27 23:05
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("teams", "0006_classrequirement"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="teamprofile",
15 | name="has_report",
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/backend/siarnaq/bracket/__init__.py:
--------------------------------------------------------------------------------
1 | from .challonge import (
2 | bulk_add_teams,
3 | create_round_objects,
4 | create_tournament,
5 | get_match_and_participant_objects_for_round,
6 | get_tournament_data,
7 | start_tournament,
8 | update_match,
9 | )
10 |
11 | __all__ = [
12 | "bulk_add_teams",
13 | "create_round_objects",
14 | "create_tournament",
15 | "get_match_and_participant_objects_for_round",
16 | "get_tournament_data",
17 | "start_tournament",
18 | "update_match",
19 | ]
20 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/compete/permissions.py:
--------------------------------------------------------------------------------
1 | from rest_framework import permissions
2 |
3 | from siarnaq.api.teams.models import Team
4 |
5 |
6 | class HasTeamSubmission(permissions.BasePermission):
7 | """Allows access to users whose team has a submission in the current episode."""
8 |
9 | def has_permission(self, request, view):
10 | return (
11 | Team.objects.filter(episode=view.kwargs["episode_id"], members=request.user)
12 | .with_active_submission()
13 | .exists()
14 | )
15 |
--------------------------------------------------------------------------------
/titan/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.18-buster AS go
2 |
3 | ENV BUILD_HOME /build
4 | WORKDIR $BUILD_HOME
5 |
6 | COPY go.mod go.sum ./
7 | RUN go mod download
8 |
9 | COPY . .
10 | RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /titan -ldflags="-s -w" ./cmd/titan/main.go
11 |
12 |
13 | FROM clamav/clamav:0.105.1_base
14 |
15 | ENV APP_HOME /app
16 | WORKDIR $APP_HOME
17 |
18 | ARG REVISION_ARG=nightly
19 | ENV TITAN_REVISION=$REVISION_ARG
20 |
21 | COPY --from=go /titan .
22 | COPY bootstrap.sh .
23 | ENTRYPOINT ["./bootstrap.sh"]
24 |
--------------------------------------------------------------------------------
/backend/siarnaq/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for siarnaq project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "siarnaq.settings")
13 | os.environ.setdefault("DJANGO_CONFIGURATION", "Local")
14 |
15 | from configurations.asgi import get_asgi_application # noqa: E402
16 |
17 | application = get_asgi_application()
18 |
--------------------------------------------------------------------------------
/backend/siarnaq/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for siarnaq project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "siarnaq.settings")
13 | os.environ.setdefault("DJANGO_CONFIGURATION", "Local")
14 |
15 | from configurations.wsgi import get_wsgi_application # noqa: E402
16 |
17 | application = get_wsgi_application()
18 |
--------------------------------------------------------------------------------
/frontend/src/api/cookies.ts:
--------------------------------------------------------------------------------
1 | import Cookies from "js-cookie";
2 |
3 | export const removeApiTokens = (): void => {
4 | Cookies.remove("access");
5 | Cookies.remove("refresh");
6 | };
7 |
8 | export const setApiTokens = ({
9 | access,
10 | refresh,
11 | }: {
12 | access: string;
13 | refresh: string;
14 | }): void => {
15 | Cookies.set("access", access);
16 | Cookies.set("refresh", refresh);
17 | };
18 |
19 | export const doApiTokensExist = (): boolean =>
20 | Cookies.get("access") !== undefined && Cookies.get("refresh") !== undefined;
21 |
--------------------------------------------------------------------------------
/frontend/src/views/NoContentFound.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { PageContainer } from "components/elements/BattlecodeStyle";
3 |
4 | const NoContentFound: React.FC = () => (
5 |
6 |
7 | {`No content found for this episode. If you have any questions, please reach out to Battlecode staff at `}
8 |
9 | {`battlecode@mit.edu`}
10 | {" "}
11 |
12 |
13 | );
14 |
15 | export default NoContentFound;
16 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0010_episode_is_allowed_ranked_scrimmage.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2025-01-13 21:54
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("episodes", "0009_alter_episode_language"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="episode",
15 | name="is_allowed_ranked_scrimmage",
16 | field=models.BooleanField(default=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/user/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include, path
2 | from rest_framework.routers import DefaultRouter
3 |
4 | from siarnaq.api.user import views
5 |
6 | router = DefaultRouter()
7 | router.register("u", views.UserViewSet, basename="user")
8 | router.register("verify_email", views.EmailVerificationViewSet, basename="verify_email")
9 |
10 | urlpatterns = [
11 | path("", include(router.urls)),
12 | path(
13 | "password_reset/",
14 | include("django_rest_passwordreset.urls", namespace="password_reset"),
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/backend/siarnaq/templates/password_reset.html:
--------------------------------------------------------------------------------
1 | Hello, {{ username|safe }}!
2 |
3 |
4 | We received a request to reset your password for
5 | Battlecode .
6 |
7 |
8 | If you did not make this request, then please ignore this email. Otherwise,
9 | click here to reset your password. The
10 | link will expire in 24 hours.
11 |
12 | Link: {{reset_password_url}}
13 |
14 | Best,
15 | -Teh Devs
16 |
17 |
--------------------------------------------------------------------------------
/frontend/src/views/EpisodeNotFound.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { Link } from "react-router-dom";
3 | import { DEFAULT_EPISODE } from "utils/constants";
4 | import { PageContainer } from "components/elements/BattlecodeStyle";
5 |
6 | const EpisodeNotFound: React.FC = () => (
7 |
8 |
9 | That episode was not found.{" "}
10 |
11 | Go Home?
12 |
13 |
14 |
15 | );
16 |
17 | export default EpisodeNotFound;
18 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM condaforge/mambaforge:22.9.0-1 AS conda
2 | COPY environment.yml environment.yml
3 | RUN mamba env create -p /env -f environment.yml && conda clean -afy
4 |
5 |
6 | FROM ubuntu:22.10
7 | COPY --from=conda /env /env
8 |
9 | ENV PYTHONUNBUFFERED 1
10 | ENV APP_HOME /app
11 | WORKDIR $APP_HOME
12 |
13 | ARG REVISION_ARG=nightly
14 | ENV SIARNAQ_REVISION=$REVISION_ARG
15 |
16 | COPY manage.py manage.py
17 | COPY siarnaq siarnaq
18 | CMD /env/bin/gunicorn --bind=:$PORT --workers=4 --threads=4 --pid=/run/gunicorn.pid --timeout=0 siarnaq.wsgi:application
19 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0012_eligibilitycriterion_is_private.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2025-11-04 21:18
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("episodes", "0011_episode_ranked_scrimmage_hourly_limit_and_more"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="eligibilitycriterion",
15 | name="is_private",
16 | field=models.BooleanField(default=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/user/migrations/0003_alter_userprofile_country.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2022-11-28 17:18
2 |
3 | from django.db import migrations
4 | import django_countries.fields
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("user", "0002_add_superuser_and_groups"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="userprofile",
16 | name="country",
17 | field=django_countries.fields.CountryField(max_length=2),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/saturn/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 |
--------------------------------------------------------------------------------
/titan/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0008_episode_release_version_client.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2024-12-23 20:58
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("episodes", "0007_remove_episode_pass_requirement_out_of_and_more"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="episode",
15 | name="release_version_client",
16 | field=models.CharField(blank=True, max_length=32),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/user/migrations/0004_alter_user_managers.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-16 00:53
2 |
3 | from django.db import migrations
4 | import siarnaq.api.user.managers
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("user", "0003_alter_userprofile_country"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelManagers(
15 | name="user",
16 | managers=[
17 | ("objects", siarnaq.api.user.managers.UserManager()),
18 | ],
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/user/permissions.py:
--------------------------------------------------------------------------------
1 | from rest_framework.permissions import BasePermission
2 |
3 |
4 | class IsEmailVerified(BasePermission):
5 | """Allows access only to users with verified email addresses."""
6 |
7 | message = "Please verify your email address to access this resource."
8 |
9 | def has_permission(self, request, view):
10 | if request.user and request.user.is_staff:
11 | return True
12 | return (
13 | request.user
14 | and request.user.is_authenticated
15 | and request.user.email_verified
16 | )
17 |
--------------------------------------------------------------------------------
/backend/siarnaq/templates/email_verification.html:
--------------------------------------------------------------------------------
1 | Hello, {{ username|safe }}!
2 |
3 |
4 | Thank you for registering for
5 | Battlecode .
6 | Please verify your email address to activate your account.
7 |
8 |
9 | Click here to verify your email. The
10 | link will expire in 24 hours.
11 |
12 | Link: {{verify_email_url}}
13 |
14 | If you didn't create an account, you can safely ignore this email.
15 |
16 |
17 | Best,
18 | -Teh Devs
19 |
20 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Battlecode",
3 | "name": "MIT Battlecode",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/backend/mypy_django_configurations_plugin.py:
--------------------------------------------------------------------------------
1 | # Django Configurations requires its import hooks to be installed before it can be used
2 | # properly. This is a short Mypy plugin to do this, so that Mypy will run successfully.
3 | # See also: https://github.com/typeddjango/django-stubs/pull/180
4 |
5 | import os
6 |
7 | from configurations import importer
8 | from mypy_django_plugin import main
9 |
10 |
11 | def plugin(version):
12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_project.config")
13 | os.environ.setdefault("DJANGO_CONFIGURATION", "Local")
14 | importer.install()
15 | return main.plugin(version)
16 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0004_tournament_display_date.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-17 20:34
2 |
3 | import datetime
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("episodes", "0003_remove_tournament_challonge_private_and_more"),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name="tournament",
16 | name="display_date",
17 | field=models.DateField(default=datetime.date(2023, 1, 1)),
18 | preserve_default=False,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/tournamentsLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import type { LoaderFunction } from "react-router-dom";
3 | import { tournamentListFactory } from "../episode/episodeFactories";
4 | import { safeEnsureQueryData } from "../helpers";
5 |
6 | export const tournamentsLoader =
7 | (queryClient: QueryClient): LoaderFunction =>
8 | ({ params }) => {
9 | const { episodeId } = params;
10 | if (episodeId === undefined) return null;
11 |
12 | // Tournament list
13 | safeEnsureQueryData({ episodeId }, tournamentListFactory, queryClient);
14 |
15 | return null;
16 | };
17 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/compete/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include, path
2 | from rest_framework.routers import DefaultRouter
3 |
4 | from siarnaq.api.compete import views
5 |
6 | router = DefaultRouter()
7 | router.register(
8 | r"(?P[^\/.]+)/submission",
9 | views.SubmissionViewSet,
10 | basename="submission",
11 | )
12 | router.register(r"(?P[^\/.]+)/match", views.MatchViewSet, basename="match")
13 | router.register(
14 | r"(?P[^\/.]+)/request",
15 | views.ScrimmageRequestViewSet,
16 | basename="request",
17 | )
18 |
19 | urlpatterns = [
20 | path("", include(router.urls)),
21 | ]
22 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0007_remove_episode_pass_requirement_out_of_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-27 03:39
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("episodes", "0006_tournamentround_display_order"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name="episode",
15 | name="pass_requirement_out_of",
16 | ),
17 | migrations.RemoveField(
18 | model_name="episode",
19 | name="pass_requirement_win",
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/deploy/cpw/output.tf:
--------------------------------------------------------------------------------
1 | output "vm_ip" {
2 | description = "The external IP address of the VM"
3 | value = google_compute_address.static.address
4 | }
5 |
6 | # Output the command to retrieve the SSH key
7 | output "ssh_command" {
8 | description = "Command to get the SSH private key from Secret Manager"
9 | value = "gcloud secrets versions access latest --secret=${google_secret_manager_secret.ssh_private_key.secret_id} && chmod 600 private_key.pem"
10 | }
11 |
12 | output "dns_records" {
13 | value = {
14 | type = "A",
15 | subdomain = "cpw.",
16 | rrdatas = [google_compute_address.static.address],
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | import formsPlugin from "@tailwindcss/forms";
3 | import headlessPlugin from "@headlessui/tailwindcss";
4 | import tailwind from "tailwindcss/defaultConfig";
5 |
6 | module.exports = {
7 | content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
8 | theme: {
9 | ...tailwind.theme,
10 | extend: {
11 | container: {
12 | center: true,
13 | },
14 | fontFamily: {
15 | sans: ["Inter", "sans-serif"],
16 | display: ['"Josefin Sans"', "sans-serif"],
17 | },
18 | },
19 | },
20 | plugins: [formsPlugin, headlessPlugin],
21 | };
22 |
--------------------------------------------------------------------------------
/backend/siarnaq/gcloud/tasks.py:
--------------------------------------------------------------------------------
1 | import google.cloud.tasks_v2 as tasks_v2
2 | import structlog
3 | from django.conf import settings
4 |
5 | logger = structlog.get_logger(__name__)
6 |
7 |
8 | class NullClient:
9 | def create_task(self, *args, **kwargs) -> None:
10 | return None
11 |
12 | def queue_path(self, *args, **kwargs) -> str:
13 | return "nothing"
14 |
15 |
16 | def get_task_client() -> tasks_v2.CloudTasksClient | NullClient:
17 | if not settings.GCLOUD_ENABLE_ACTIONS:
18 | logger.warn("tasks_disabled", message="Async task queue is disabled.")
19 | return NullClient()
20 |
21 | return tasks_v2.CloudTasksClient()
22 |
--------------------------------------------------------------------------------
/frontend/src/contexts/EpisodeContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 |
3 | interface EpisodeIdContextType {
4 | episodeId: string;
5 | setEpisodeId: (episodeId: string) => void;
6 | }
7 |
8 | export const EpisodeIdContext = createContext(
9 | null,
10 | );
11 |
12 | // Use this function to retrieve and update the episodeId.
13 | export const useEpisodeId = (): EpisodeIdContextType => {
14 | const episodeIdContext = useContext(EpisodeIdContext);
15 | if (episodeIdContext === null) {
16 | throw new Error("useEpisodeId has to be used within ");
17 | }
18 | return episodeIdContext;
19 | };
20 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0006_tournamentround_display_order.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-27 02:51
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | (
10 | "episodes",
11 | "0005_remove_tournamentround_round-unique-tournament-challonge_and_more",
12 | ),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name="tournamentround",
18 | name="display_order",
19 | field=models.PositiveSmallIntegerField(default=0),
20 | preserve_default=False,
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/deploy/releases/variables.tf:
--------------------------------------------------------------------------------
1 | variable "name" {
2 | description = "Name of this service"
3 | type = string
4 | }
5 |
6 | variable "gcp_project" {
7 | description = "Google Cloud project where all resources are located"
8 | type = string
9 | }
10 |
11 | variable "gcp_region" {
12 | description = "Google Cloud region where all resources are located"
13 | type = string
14 | }
15 |
16 | variable "gcp_zone" {
17 | description = "Google Cloud zone where all zonal resources are located"
18 | type = string
19 | }
20 |
21 | variable "labels" {
22 | description = "The labels to attach to resources created by this module"
23 | type = map(string)
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/docs/local-setup.md:
--------------------------------------------------------------------------------
1 | # Local Setup Instructions
2 |
3 | ## One-Time Setup
4 |
5 | First, make sure you have **prepared your Conda environment, according to the top-level readme.**
6 |
7 | Then, run
8 |
9 | ```
10 | npm install
11 | ```
12 |
13 | ## Run the frontend
14 |
15 | Make sure that the backend is running. (You can hook up to a deployed backend, or run it locally. Then make sure `.env.development` points to that backend.)
16 |
17 | In **this directory**, run:
18 |
19 | ```
20 | npm run start
21 | ```
22 |
23 | This automatically reloads the page on changes.
24 |
25 | For more info of what you can do to set up or run the frontend, see [create-react-app.md](create-react-app.md).
26 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "es2017",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": true,
18 | "types": ["vite/client", "vite-plugin-svgr/client"],
19 | "jsx": "react-jsx"
20 | },
21 | "include": ["src", "*.js"]
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import svgr from "vite-plugin-svgr";
3 | import tsconfigPaths from "vite-tsconfig-paths";
4 | import react from "@vitejs/plugin-react";
5 | import eslint from "vite-plugin-eslint";
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | base: "/",
10 | plugins: [
11 | react(),
12 | svgr(),
13 | tsconfigPaths(),
14 | // Show ESLint warnings/errors in overlay without blocking interaction
15 | eslint({ failOnError: false, failOnWarning: false }),
16 | ],
17 | server: {
18 | port: 3000,
19 | open: true,
20 | },
21 | build: {
22 | outDir: "build",
23 | sourcemap: true,
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/accountLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import { scrimmagingRecordFactory } from "api/compete/competeFactories";
3 | import { safeEnsureQueryData } from "api/helpers";
4 | import type { LoaderFunction } from "react-router-dom";
5 |
6 | export const accountLoader =
7 | (queryClient: QueryClient): LoaderFunction =>
8 | ({ params }) => {
9 | const { episodeId } = params;
10 |
11 | if (episodeId === undefined) return null;
12 |
13 | // User team scrimmage record
14 | safeEnsureQueryData(
15 | {
16 | episodeId,
17 | },
18 | scrimmagingRecordFactory,
19 | queryClient,
20 | );
21 |
22 | return null;
23 | };
24 |
--------------------------------------------------------------------------------
/frontend/src/components/elements/BattlecodeStyle.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | interface PageTitleProps {
4 | children?: React.ReactNode;
5 | }
6 |
7 | export const PageTitle: React.FC = ({
8 | children,
9 | }: PageTitleProps) => (
10 |
11 | {children}
12 |
13 | );
14 |
15 | interface PageContainerProps {
16 | children?: React.ReactNode;
17 | className?: string;
18 | }
19 |
20 | export const PageContainer: React.FC = ({
21 | children,
22 | className = "",
23 | }: PageContainerProps) => (
24 |
25 | {children}
26 |
27 | );
28 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/compete/migrations/0002_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2022-11-26 20:29
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ("teams", "0001_initial"),
13 | ("compete", "0001_initial"),
14 | ]
15 |
16 | operations = [
17 | migrations.AddField(
18 | model_name="submission",
19 | name="team",
20 | field=models.ForeignKey(
21 | on_delete=django.db.models.deletion.PROTECT,
22 | related_name="submissions",
23 | to="teams.team",
24 | ),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0009_alter_episode_language.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2025-01-06 16:53
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("episodes", "0008_episode_release_version_client"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="episode",
14 | name="language",
15 | field=models.CharField(
16 | choices=[
17 | ("java8", "Java 8"),
18 | ("java21", "Java 21"),
19 | ("py3", "Python 3"),
20 | ],
21 | max_length=8,
22 | ),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/compete/migrations/0004_match_challonge_id_matchparticipant_challonge_id.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-17 06:57
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("compete", "0003_initial"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="match",
15 | name="challonge_id",
16 | field=models.IntegerField(blank=True, null=True, unique=True),
17 | ),
18 | migrations.AddField(
19 | model_name="matchparticipant",
20 | name="challonge_id",
21 | field=models.CharField(blank=True, max_length=64, null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include, path
2 | from rest_framework.routers import DefaultRouter
3 |
4 | from siarnaq.api.episodes import views
5 |
6 | router = DefaultRouter()
7 | router.register("e", views.EpisodeViewSet, basename="episode")
8 | router.register(
9 | r"(?P[^\/.]+)/map",
10 | views.MapViewSet,
11 | basename="map",
12 | )
13 | router.register(
14 | r"(?P[^\/.]+)/tournament",
15 | views.TournamentViewSet,
16 | basename="tournament",
17 | )
18 | router.register(
19 | r"(?P[^\/.]+)/tournament/(?P[^\/.]+)/round",
20 | views.TournamentRoundViewSet,
21 | basename="tournament-round",
22 | )
23 |
24 | urlpatterns = [
25 | path("", include(router.urls)),
26 | ]
27 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/teams/migrations/0005_alter_teamprofile_auto_accept_ranked.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-22 17:40
2 |
3 | from django.db import migrations, models
4 |
5 | def set_all_no_autoaccept(apps, schema_editor):
6 | TeamProfile = apps.get_model("teams", "TeamProfile")
7 | TeamProfile.objects.update(auto_accept_ranked=False)
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ("teams", "0004_alter_teamprofile_team"),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterField(
17 | model_name="teamprofile",
18 | name="auto_accept_ranked",
19 | field=models.BooleanField(default=False),
20 | ),
21 | migrations.RunPython(set_all_no_autoaccept),
22 | ]
23 |
--------------------------------------------------------------------------------
/titan/bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -x -o errexit
4 |
5 | # The Docker image has configured clamd to use a socket at /tmp/clamd.sock. However,
6 | # Titan expects it to be at /run/clamav/clamd.sock instead. So update the config.
7 | sed -i 's/\/tmp\/clamd\.sock/\/run\/clamav\/clamd\.sock/' /etc/clamav/clamd.conf
8 |
9 | # I solemnly swear that I am up to no good.
10 | # We need to start the clamd daemon, and conveniently the image provides an init script
11 | # to do this. Less conveniently, that init script also ends with a blocking command to
12 | # "wait forever". So, let's drop that command and run everything else...
13 |
14 | grep -v 'tail -f "/dev/null"' /init | sh
15 | # Mischief managed. I am a terrible person.
16 |
17 | # Run the Titan server process.
18 | exec $APP_HOME/titan "$@"
19 |
--------------------------------------------------------------------------------
/frontend/public/number_4.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0011_episode_ranked_scrimmage_hourly_limit_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2025-01-23 08:04
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("episodes", "0010_episode_is_allowed_ranked_scrimmage"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="episode",
15 | name="ranked_scrimmage_hourly_limit",
16 | field=models.PositiveSmallIntegerField(default=10),
17 | ),
18 | migrations.AddField(
19 | model_name="episode",
20 | name="unranked_scrimmage_hourly_limit",
21 | field=models.PositiveSmallIntegerField(default=10),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/teams/migrations/0004_alter_teamprofile_team.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2022-12-30 04:33
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("teams", "0003_alter_team_name_alter_teamprofile_quote"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="teamprofile",
16 | name="team",
17 | field=models.OneToOneField(
18 | on_delete=django.db.models.deletion.CASCADE,
19 | primary_key=True,
20 | related_name="profile",
21 | serialize=False,
22 | to="teams.team",
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/rankingsLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import type { LoaderFunction } from "react-router-dom";
3 | import { episodeInfoFactory } from "../episode/episodeFactories";
4 | import { safeEnsureQueryData } from "../helpers";
5 | import { searchTeamsFactory } from "../team/teamFactories";
6 |
7 | export const rankingsLoader =
8 | (queryClient: QueryClient): LoaderFunction =>
9 | ({ params }) => {
10 | const { episodeId } = params;
11 |
12 | if (episodeId === undefined) return null;
13 |
14 | // Episode info
15 | safeEnsureQueryData({ id: episodeId }, episodeInfoFactory, queryClient);
16 |
17 | // Team list (rankings)
18 | safeEnsureQueryData({ episodeId }, searchTeamsFactory, queryClient);
19 |
20 | return null;
21 | };
22 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/teamProfileLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import type { LoaderFunction } from "react-router-dom";
3 | import { safeEnsureQueryData } from "../helpers";
4 | import { otherTeamInfoFactory } from "../team/teamFactories";
5 |
6 | // loader for other team's public profile pages
7 | export const teamProfileLoader =
8 | (queryClient: QueryClient): LoaderFunction =>
9 | ({ params }) => {
10 | const { teamId, episodeId } = params;
11 | if (teamId === undefined || episodeId === undefined) return null;
12 |
13 | // Load the team's info
14 | safeEnsureQueryData(
15 | {
16 | episodeId,
17 | id: teamId,
18 | },
19 | otherTeamInfoFactory,
20 | queryClient,
21 | );
22 |
23 | return null;
24 | };
25 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/userProfileLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import type { LoaderFunction } from "react-router-dom";
3 | import { safeEnsureQueryData } from "../helpers";
4 | import { otherUserTeamsFactory } from "../user/userFactories";
5 |
6 | // loader for other team's public profile pages
7 | export const userProfileLoader =
8 | (queryClient: QueryClient): LoaderFunction =>
9 | ({ params }) => {
10 | const { episodeId, userId } = params;
11 | if (userId === undefined || episodeId === undefined) return null;
12 | const id = parseInt(userId, 10);
13 |
14 | // All teams
15 | safeEnsureQueryData(
16 | {
17 | id,
18 | },
19 | otherUserTeamsFactory,
20 | queryClient,
21 | );
22 |
23 | return null;
24 | };
25 |
--------------------------------------------------------------------------------
/backend/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "siarnaq.settings")
10 | os.environ.setdefault("DJANGO_CONFIGURATION", "Local")
11 | try:
12 | from configurations.management import execute_from_command_line
13 | except ImportError as exc:
14 | raise ImportError(
15 | "Couldn't import Django. Are you sure it's installed and "
16 | "available on your PYTHONPATH environment variable? Did you "
17 | "forget to activate a virtual environment?"
18 | ) from exc
19 | execute_from_command_line(sys.argv)
20 |
21 |
22 | if __name__ == "__main__":
23 | main()
24 |
--------------------------------------------------------------------------------
/deploy/cd/variables.tf:
--------------------------------------------------------------------------------
1 | variable "name" {
2 | description = "Name of this service"
3 | type = string
4 | }
5 |
6 | variable "gcp_project" {
7 | description = "Google Cloud project where all resources are located"
8 | type = string
9 | }
10 |
11 | variable "gcp_region" {
12 | description = "Google Cloud region where all resources are located"
13 | type = string
14 | }
15 |
16 | variable "gcp_zone" {
17 | description = "Google Cloud zone where all zonal resources are located"
18 | type = string
19 | }
20 |
21 | variable "labels" {
22 | description = "The labels to attach to resources created by this module"
23 | type = map(string)
24 | }
25 |
26 | variable "storage_frontend_name" {
27 | description = "Name of Google Cloud Storage bucket resource for website frontend"
28 | type = string
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/contexts/EpisodeProvider.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { useState, useMemo } from "react";
3 | import { EpisodeIdContext } from "./EpisodeContext";
4 | import { DEFAULT_EPISODE } from "../utils/constants";
5 |
6 | export const EpisodeProvider: React.FC<{ children: React.ReactNode }> = ({
7 | children,
8 | }) => {
9 | // episodeId is set by the EpisodeLayout component to the episode in the url
10 | const [episodeId, setEpisodeId] = useState(DEFAULT_EPISODE);
11 |
12 | // avoid recreating context value if episode id hasn't changed
13 | const episodeIdContextValue = useMemo(
14 | () => ({ episodeId, setEpisodeId }),
15 | [episodeId, setEpisodeId],
16 | );
17 |
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/saturn/pkg/run/protocol.go:
--------------------------------------------------------------------------------
1 | package run
2 |
3 | type Submission struct {
4 | Source FileSpecification `mapstructure:"source"`
5 | Binary FileSpecification `mapstructure:"binary"`
6 | TeamName string `mapstructure:"team-name"`
7 | Package string `mapstructure:"package"`
8 | }
9 |
10 | type CompileRequest struct {
11 | Submission `mapstructure:"submission,squash"`
12 | }
13 |
14 | type ExecuteRequest struct {
15 | A Submission `mapstructure:"a"`
16 | B Submission `mapstructure:"b"`
17 | Maps []string `mapstructure:"maps"`
18 | Replay FileSpecification `mapstructure:"replay"`
19 | AlternateOrder bool `mapstructure:"alternate-order"`
20 | }
21 |
22 | type FileSpecification struct {
23 | Bucket string `mapstructure:"bucket"`
24 | Name string `mapstructure:"name"`
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/myTeamLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import type { LoaderFunction } from "react-router-dom";
3 | import { myTeamFactory } from "../team/teamFactories";
4 | import { safeEnsureQueryData } from "../helpers";
5 | import { scrimmagingRecordFactory } from "api/compete/competeFactories";
6 |
7 | export const myTeamLoader =
8 | (queryClient: QueryClient): LoaderFunction =>
9 | ({ params }) => {
10 | const { episodeId } = params;
11 |
12 | if (episodeId === undefined) return null;
13 |
14 | // My team info
15 | safeEnsureQueryData({ episodeId }, myTeamFactory, queryClient);
16 |
17 | // Ranked and Unranked Scrimmage Record
18 | safeEnsureQueryData(
19 | {
20 | episodeId,
21 | },
22 | scrimmagingRecordFactory,
23 | queryClient,
24 | );
25 |
26 | return null;
27 | };
28 |
--------------------------------------------------------------------------------
/frontend/src/components/PrivateRoute.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { useEffect } from "react";
3 | import { AuthStateEnum, useCurrentUser } from "../contexts/CurrentUserContext";
4 | import { Outlet, useNavigate } from "react-router-dom";
5 | import Spinner from "./Spinner";
6 |
7 | const PrivateRoute: React.FC = () => {
8 | const { authState } = useCurrentUser();
9 | const navigate = useNavigate();
10 |
11 | useEffect(() => {
12 | if (authState === AuthStateEnum.NOT_AUTHENTICATED) {
13 | navigate("/login");
14 | }
15 | }, [navigate, authState]);
16 |
17 | if (authState === AuthStateEnum.AUTHENTICATED) {
18 | return ;
19 | } else {
20 | return (
21 |
22 |
23 |
24 | );
25 | }
26 | };
27 |
28 | export default PrivateRoute;
29 |
--------------------------------------------------------------------------------
/titan/cmd/titan/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 | "time"
7 |
8 | "github.com/battlecode/galaxy/titan/pkg/titan"
9 | "github.com/rs/zerolog"
10 | "github.com/rs/zerolog/log"
11 | )
12 |
13 | func main() {
14 | zerolog.DefaultContextLogger = &log.Logger
15 | zerolog.LevelFieldName = "severity"
16 | zerolog.TimeFieldFormat = time.RFC3339Nano
17 |
18 | ctx := context.Background()
19 |
20 | titan, err := titan.New(ctx)
21 | if err != nil {
22 | log.Ctx(ctx).Fatal().Err(err).Msg("Could not initialize Titan.")
23 | }
24 |
25 | port := os.Getenv("PORT")
26 | if port == "" {
27 | port = "8080"
28 | }
29 | log.Ctx(ctx).Info().Msgf("Listening on port %s.", port)
30 |
31 | if err := titan.Start(ctx, ":"+port); err != nil {
32 | log.Ctx(ctx).Fatal().Err(err).Msg("Server closed abnormally.")
33 | }
34 | log.Ctx(ctx).Info().Msg("Server closed normally.")
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/homeIfLoggedIn.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import { buildKey } from "api/helpers";
3 | import { loginTokenVerifyFactory } from "api/user/userFactories";
4 | import { redirect, type LoaderFunction } from "react-router-dom";
5 | import { DEFAULT_EPISODE } from "utils/constants";
6 |
7 | export const homeIfLoggedIn =
8 | (queryClient: QueryClient): LoaderFunction =>
9 | async () => {
10 | // Check if user is logged in
11 | const loggedIn = await queryClient.ensureQueryData({
12 | queryKey: buildKey(loginTokenVerifyFactory.queryKey, { queryClient }),
13 | queryFn: async () =>
14 | await loginTokenVerifyFactory.queryFn({ queryClient }),
15 | });
16 |
17 | if (loggedIn) {
18 | // If user is logged in, redirect to home
19 | return redirect(`/${DEFAULT_EPISODE}/home`);
20 | }
21 | return null;
22 | };
23 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/compete/migrations/0005_rename_challonge_id_match_external_id_private_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-21 18:05
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("compete", "0004_match_challonge_id_matchparticipant_challonge_id"),
10 | ]
11 |
12 | operations = [
13 | migrations.RenameField(
14 | model_name="match",
15 | old_name="challonge_id",
16 | new_name="external_id_private",
17 | ),
18 | migrations.RemoveField(
19 | model_name="matchparticipant",
20 | name="challonge_id",
21 | ),
22 | migrations.AddField(
23 | model_name="matchparticipant",
24 | name="external_id",
25 | field=models.CharField(blank=True, max_length=64, null=True),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/frontend/src/api/README.md:
--------------------------------------------------------------------------------
1 | This is the README for the **new** API. I would greatly recommend reading at least some of this blog before working in here as it will de-mystify a lot of the decisions made in building it out: https://tkdodo.eu/blog/practical-react-query.
2 |
3 | - \_autogen: This contains **ONLY** auto-generated code, created by calling "./generate_types.sh"! This contains types & and auto-generated Fetch APIs. We wrap these auto-generated api functions in each endpoint's sub-folder. The only thing that FE code should import from here are models and types!
4 |
5 | - endpoint subfolders: Each endpoint's subfolder contains three files. Below uses _episode_ as an example:
6 | - episodeApi.ts: exposes wrapper functions of an instance of EpisodeApi.
7 | - episodeKeys.ts: contains key factories for episode Query Hooks and Mutation Hooks.
8 | - useEpisode.ts: contains wrapper hooks for each episode api function, mutations and queries.
9 |
--------------------------------------------------------------------------------
/frontend/src/components/OptionalSectionCardMarkdown.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import type { Key } from "react";
3 | import Markdown from "./elements/Markdown";
4 | import SectionCard from "./SectionCard";
5 |
6 | interface OptionalSectionCardMarkdownProps {
7 | children?: React.ReactNode;
8 | title: T;
9 | textRecord: Partial>;
10 | }
11 |
12 | const OptionalSectionCardMarkdown: React.FC<
13 | OptionalSectionCardMarkdownProps
14 | > = ({
15 | children,
16 | title,
17 | textRecord,
18 | }: OptionalSectionCardMarkdownProps) => {
19 | if (textRecord[title] !== undefined) {
20 | return (
21 |
22 |
23 | {children}
24 |
25 | );
26 | } else {
27 | return <>>;
28 | }
29 | };
30 |
31 | export default OptionalSectionCardMarkdown;
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report-or-feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report or Feature Request
3 | about: General Issue template for Galaxy.
4 | title: ""
5 | labels: triage
6 | assignees: ""
7 | ---
8 |
9 | # Before you submit this issue...
10 |
11 | **If you are not a Battlecode dev, please keep the `triage` label.**
12 |
13 | If you are a Battlecode dev, the following would be great, but optional.
14 |
15 | - [ ] If you know the priority, assign a label for it. (the red labels)
16 | - [ ] If you know the module(s), assign a label (or labels) for it. (the green labels)
17 | - [ ] If you know the type(s), assign a label (or labels) for it. (the yellow labels)
18 | - [ ] If you know this is a simple issue or fix, assign `good first issue`. (the blue label)
19 | - [ ] If you did not fill any of the above, please keep the `triage` label.
20 |
21 | # Description
22 |
23 | Please include a description of the issue below, including steps to reproduce.
24 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/SidebarItem.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { NavLink } from "react-router-dom";
3 | import Icon, { type IconName } from "../elements/Icon";
4 |
5 | interface SidebarItemProps {
6 | iconName: IconName;
7 | text: string;
8 | linkTo: string;
9 | }
10 |
11 | const SidebarItem: React.FC = ({
12 | iconName,
13 | text,
14 | linkTo,
15 | }) => {
16 | const colorVariants = {
17 | gray: "text-gray-800 hover:text-gray-100 hover:bg-gray-500",
18 | color: "text-cyan-600",
19 | };
20 | return (
21 |
23 | "flex items-center gap-3 rounded-lg py-2 pl-1.5 pr-8 text-base " +
24 | (isActive ? colorVariants.color : colorVariants.gray)
25 | }
26 | to={linkTo}
27 | >
28 |
29 | {text}
30 |
31 | );
32 | };
33 |
34 | export default SidebarItem;
35 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Josefin+Sans:wght@100;300;400;500;700&display=swap");
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
6 | @layer base {
7 | h1 {
8 | @apply pb-4 text-3xl font-semibold text-gray-900;
9 | }
10 | h2 {
11 | @apply pb-4 pt-6 text-2xl font-semibold text-gray-900;
12 | }
13 | h3 {
14 | @apply pb-4 text-xl font-semibold text-gray-900;
15 | }
16 | h4 {
17 | @apply pb-4 text-lg font-semibold text-gray-700;
18 | }
19 | p {
20 | @apply pb-4 text-gray-900;
21 | }
22 | code {
23 | @apply rounded bg-gray-100 px-1 py-0.5 text-sm text-cyan-900;
24 | }
25 | pre code {
26 | @apply p-0;
27 | }
28 | pre {
29 | @apply rounded bg-gray-100 px-4 py-3;
30 | }
31 | ul {
32 | @apply ml-6 list-outside list-disc pb-4;
33 | }
34 | ol {
35 | @apply ml-6 list-outside list-decimal pb-4;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/backend/environment.yml:
--------------------------------------------------------------------------------
1 | name: siarnaq
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - ca-certificates=2022.9.24
6 | - django=4.1.2
7 | - django-cors-headers=3.13.0
8 | - djangorestframework=3.14.0
9 | - drf-spectacular=0.24.2
10 | - google-auth=2.13.0
11 | - google-cloud-pubsub=2.13.10
12 | - google-cloud-secret-manager=2.12.6
13 | - google-cloud-storage=2.5.0
14 | - gunicorn=20.1.0
15 | - openssl=3.0.5
16 | - pillow=9.0.1
17 | - pip=22.3
18 | - psycopg2=2.9.3
19 | - python=3.10
20 | - pip:
21 | - django-anymail==8.6
22 | - django-configurations==2.4
23 | - django-countries==7.4.2
24 | - django-import-export==3.0.2
25 | - django-sortedm2m==4.0.0
26 | - django-storages[google]==1.13.1
27 | - django-structlog==4.0.1
28 | - django_rest_passwordreset==1.3.0
29 | - djangorestframework-simplejwt==5.2.2
30 | - google-cloud-scheduler==2.7.3
31 | - google-cloud-tasks==2.12.0
32 | - requests==2.28.1
33 |
--------------------------------------------------------------------------------
/frontend/src/contexts/CurrentUserProvider.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { AuthStateEnum, CurrentUserContext } from "./CurrentUserContext";
3 | import { useCurrentUserInfo, useIsLoggedIn } from "../api/user/useUser";
4 | import _ from "lodash";
5 | import { useQueryClient } from "@tanstack/react-query";
6 |
7 | export const CurrentUserProvider: React.FC<{ children: React.ReactNode }> = ({
8 | children,
9 | }) => {
10 | const queryClient = useQueryClient();
11 | const isLoggedIn = useIsLoggedIn(queryClient);
12 | const authState =
13 | !_.isNil(isLoggedIn.data) && isLoggedIn.data
14 | ? AuthStateEnum.AUTHENTICATED
15 | : isLoggedIn.isLoading
16 | ? AuthStateEnum.LOADING
17 | : AuthStateEnum.NOT_AUTHENTICATED;
18 |
19 | const userData = useCurrentUserInfo();
20 |
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/StyleEnum.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | /**
16 | *
17 | * @export
18 | * @enum {string}
19 | */
20 | export enum StyleEnum {
21 | Se = 'SE',
22 | De = 'DE'
23 | }
24 |
25 |
26 | export function StyleEnumFromJSON(json: any): StyleEnum {
27 | return StyleEnumFromJSONTyped(json, false);
28 | }
29 |
30 | export function StyleEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): StyleEnum {
31 | return json as StyleEnum;
32 | }
33 |
34 | export function StyleEnumToJSON(value?: StyleEnum | null): any {
35 | return value as any;
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/frontend/src/contexts/CurrentUserContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 | import type { UserPrivate } from "../api/_autogen";
3 | import type { UseQueryResult } from "@tanstack/react-query";
4 |
5 | export enum AuthStateEnum {
6 | LOADING = "loading",
7 | AUTHENTICATED = "authenticated",
8 | NOT_AUTHENTICATED = "not_authenticated",
9 | }
10 |
11 | export type AuthState = `${AuthStateEnum}`;
12 |
13 | interface CurrentUserContextType {
14 | authState: AuthState;
15 | user: UseQueryResult;
16 | }
17 |
18 | export const CurrentUserContext = createContext(
19 | null,
20 | );
21 |
22 | export const useCurrentUser = (): CurrentUserContextType => {
23 | const currentUserContext = useContext(CurrentUserContext);
24 |
25 | if (currentUserContext === null) {
26 | throw new Error(
27 | "useCurrentUser has to be used within ",
28 | );
29 | }
30 |
31 | return currentUserContext;
32 | };
33 |
--------------------------------------------------------------------------------
/frontend/src/utils/loadTeams.ts:
--------------------------------------------------------------------------------
1 | import { searchTeams } from "../api/team/teamApi";
2 | import type { PaginatedTeamPublicList } from "../api/_autogen";
3 |
4 | /**
5 | * Search for teams by name. Often used for the AsyncSelectMenu component.
6 | * @param episodeId
7 | * @param search Team name search string.
8 | * @param page (Optional) Page number (default: 1).
9 | * @returns An array of value/label pairs, which is expected by the AsyncSelectMenu component.
10 | */
11 | export const loadTeamOptions = async (
12 | episodeId: string,
13 | search: string,
14 | page?: number,
15 | ): Promise> => {
16 | try {
17 | const result: PaginatedTeamPublicList = await searchTeams({
18 | episodeId,
19 | search,
20 | page,
21 | });
22 | return (
23 | result.results?.map((t) => ({
24 | value: t.id,
25 | label: t.name,
26 | })) ?? []
27 | );
28 | } catch (err) {
29 | console.error(err);
30 | return [];
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/titan/README.md:
--------------------------------------------------------------------------------
1 | # Titan
2 |
3 | Our competitors can upload many files that are viewable by other users. For example,
4 | they may upload arbitrary files as their resumes, and these files may be viewed by
5 | authorized representatives from sponsors. In order to keep our stakeholders safe, we
6 | scan these files for malware.
7 |
8 | Titan is our antivirus module that performs asynchronous malware scanning on-demand.
9 |
10 | ## How to request a scan
11 |
12 | Titan is configured (by Terraform) to respond to scan requests for the `public` and
13 | `secure` storage buckets. To request a scan, tag the object with the following metadata
14 | after uploading it:
15 |
16 | ```
17 | Titan-Status: Unverified
18 | ```
19 |
20 | Titan will detect this and scan the file. Once the file is scanned, the status will be
21 | replaced with either of `Verified` or `Malicious`.
22 |
23 | ## System architecture
24 |
25 | Titan receives file metadata update events via Google EventArc. These events are pushed
26 | to the server and the file is scanned.
27 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/compete/migrations/0006_rename_external_id_matchparticipant_external_id_private_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-23 10:07
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("compete", "0005_rename_challonge_id_match_external_id_private_and_more"),
10 | ]
11 |
12 | operations = [
13 | migrations.RenameField(
14 | model_name="matchparticipant",
15 | old_name="external_id",
16 | new_name="external_id_private",
17 | ),
18 | migrations.AddField(
19 | model_name="match",
20 | name="external_id_public",
21 | field=models.IntegerField(blank=True, null=True, unique=True),
22 | ),
23 | migrations.AddField(
24 | model_name="matchparticipant",
25 | name="external_id_public",
26 | field=models.CharField(blank=True, max_length=64, null=True),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/compete/migrations/0007_alter_match_tournament_round.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-24 15:10
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | (
11 | "episodes",
12 | "0005_remove_tournamentround_round-unique-tournament-challonge_and_more",
13 | ),
14 | (
15 | "compete",
16 | "0006_rename_external_id_matchparticipant_external_id_private_and_more",
17 | ),
18 | ]
19 |
20 | operations = [
21 | migrations.AlterField(
22 | model_name="match",
23 | name="tournament_round",
24 | field=models.ForeignKey(
25 | blank=True,
26 | null=True,
27 | on_delete=django.db.models.deletion.CASCADE,
28 | related_name="matches",
29 | to="episodes.tournamentround",
30 | ),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/frontend/src/components/elements/Pill.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import Icon from "./Icon";
3 |
4 | interface PillProps {
5 | label: string | React.JSX.Element;
6 | deletable?: boolean;
7 | onDelete?: (ev?: React.MouseEvent) => void;
8 | className?: string;
9 | }
10 |
11 | const Pill: React.FC = ({
12 | label,
13 | deletable = false,
14 | onDelete,
15 | className = "",
16 | }) => (
17 |
20 |
{label}
21 | {deletable && (
22 |
{
24 | onDelete?.(ev);
25 | ev.stopPropagation();
26 | }}
27 | className="cursor-pointer items-center rounded hover:text-cyan-950"
28 | >
29 |
30 |
31 | )}
32 |
33 | );
34 |
35 | export default Pill;
36 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/managers.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils import timezone
3 |
4 |
5 | class EpisodeQuerySet(models.QuerySet):
6 | def visible_to_user(self, *, is_staff):
7 | """Filter the queryset for only episodes that are visible to the user."""
8 | if is_staff:
9 | # Staff can see everything
10 | return self.all()
11 | else:
12 | # Other users can only see those that have been released
13 | return self.filter(registration__lte=timezone.now())
14 |
15 |
16 | class TournamentQuerySet(models.QuerySet):
17 | def visible_to_user(self, *, is_staff):
18 | """
19 | Filter the queryset for only tournaments that are visible to the user. Does not
20 | consider episode visibility.
21 | """
22 | if is_staff:
23 | # Staff can see everything
24 | return self.all()
25 | else:
26 | # Other users can only see those that have been released
27 | return self.filter(is_public=True)
28 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/LanguageEnum.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | /**
16 | *
17 | * @export
18 | * @enum {string}
19 | */
20 | export enum LanguageEnum {
21 | Java8 = 'java8',
22 | Java21 = 'java21',
23 | Py3 = 'py3'
24 | }
25 |
26 |
27 | export function LanguageEnumFromJSON(json: any): LanguageEnum {
28 | return LanguageEnumFromJSONTyped(json, false);
29 | }
30 |
31 | export function LanguageEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): LanguageEnum {
32 | return json as LanguageEnum;
33 | }
34 |
35 | export function LanguageEnumToJSON(value?: LanguageEnum | null): any {
36 | return value as any;
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/Status526Enum.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | /**
16 | *
17 | * @export
18 | * @enum {string}
19 | */
20 | export enum Status526Enum {
21 | R = 'R',
22 | X = 'X',
23 | S = 'S',
24 | O = 'O'
25 | }
26 |
27 |
28 | export function Status526EnumFromJSON(json: any): Status526Enum {
29 | return Status526EnumFromJSONTyped(json, false);
30 | }
31 |
32 | export function Status526EnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): Status526Enum {
33 | return json as Status526Enum;
34 | }
35 |
36 | export function Status526EnumToJSON(value?: Status526Enum | null): any {
37 | return value as any;
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/GenderEnum.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | /**
16 | *
17 | * @export
18 | * @enum {string}
19 | */
20 | export enum GenderEnum {
21 | F = 'F',
22 | M = 'M',
23 | N = 'N',
24 | Star = '*',
25 | QuestionMark = '?'
26 | }
27 |
28 |
29 | export function GenderEnumFromJSON(json: any): GenderEnum {
30 | return GenderEnumFromJSONTyped(json, false);
31 | }
32 |
33 | export function GenderEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): GenderEnum {
34 | return json as GenderEnum;
35 | }
36 |
37 | export function GenderEnumToJSON(value?: GenderEnum | null): any {
38 | return value as any;
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/deploy/.gitignore:
--------------------------------------------------------------------------------
1 | # Local .terraform directories
2 | **/.terraform/*
3 |
4 | # .tfstate files
5 | *.tfstate
6 | *.tfstate.*
7 |
8 | # Crash log files
9 | crash.log
10 | crash.*.log
11 |
12 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as
13 | # password, private keys, and other secrets. These should not be part of version
14 | # control as they are data points which are potentially sensitive and subject
15 | # to change depending on the environment.
16 | *.tfvars
17 | *.tfvars.json
18 |
19 | # Ignore override files as they are usually used to override resources locally and so
20 | # are not checked in
21 | override.tf
22 | override.tf.json
23 | *_override.tf
24 | *_override.tf.json
25 |
26 | # Include override files you do wish to add to version control using negated pattern
27 | # !example_override.tf
28 |
29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
30 | # example: *tfplan*
31 |
32 | # Ignore CLI configuration files
33 | .terraformrc
34 | terraform.rc
35 |
36 | #private key for cpw vm ssh
37 | *.pem
38 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/PlayerOrderEnum.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | /**
16 | *
17 | * @export
18 | * @enum {string}
19 | */
20 | export enum PlayerOrderEnum {
21 | Plus = '+',
22 | Minus = '-',
23 | QuestionMark = '?'
24 | }
25 |
26 |
27 | export function PlayerOrderEnumFromJSON(json: any): PlayerOrderEnum {
28 | return PlayerOrderEnumFromJSONTyped(json, false);
29 | }
30 |
31 | export function PlayerOrderEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): PlayerOrderEnum {
32 | return json as PlayerOrderEnum;
33 | }
34 |
35 | export function PlayerOrderEnumToJSON(value?: PlayerOrderEnum | null): any {
36 | return value as any;
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/saturn/pkg/saturn/secret.go:
--------------------------------------------------------------------------------
1 | package saturn
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 |
8 | secretmanager "cloud.google.com/go/secretmanager/apiv1"
9 | "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
10 | )
11 |
12 | type Secret struct {
13 | GitToken string `json:"git-token"`
14 | }
15 |
16 | func ReadSecret(ctx context.Context, projectID, name string) (*Secret, error) {
17 | client, err := secretmanager.NewClient(ctx)
18 | if err != nil {
19 | return nil, fmt.Errorf("secretmanager.NewClient: %v", err)
20 | }
21 | defer client.Close()
22 |
23 | request := &secretmanagerpb.AccessSecretVersionRequest{
24 | Name: fmt.Sprintf("projects/%s/secrets/%s/versions/latest", projectID, name),
25 | }
26 | resp, err := client.AccessSecretVersion(ctx, request)
27 | if err != nil {
28 | return nil, fmt.Errorf("client.AccessSecretVersionRequest: %v", err)
29 | }
30 |
31 | var result Secret
32 | if err := json.Unmarshal(resp.Payload.Data, &result); err != nil {
33 | return nil, fmt.Errorf("json.Unmarshal: %v", err)
34 | }
35 | return &result, nil
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/adminTournamentLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import { buildKey } from "api/helpers";
3 | import { myUserInfoFactory } from "api/user/userFactories";
4 | import { type LoaderFunction, redirect } from "react-router-dom";
5 |
6 | export const adminTournamentLoader =
7 | (queryClient: QueryClient): LoaderFunction =>
8 | async ({ params }) => {
9 | const { episodeId, tournamentId } = params;
10 | if (episodeId === undefined) return null;
11 | if (tournamentId === undefined) return redirect(`/${episodeId}/home`);
12 |
13 | // Ensure the user is a staff member
14 | const user = queryClient.ensureQueryData({
15 | queryKey: buildKey(myUserInfoFactory.queryKey, {}),
16 | queryFn: async () => await myUserInfoFactory.queryFn({}),
17 | });
18 |
19 | try {
20 | if (!(await user).is_staff)
21 | return redirect(`/${episodeId}/tournament/${tournamentId}`);
22 | } catch (_) {
23 | return redirect(`/${episodeId}/tournament/${tournamentId}`);
24 | }
25 |
26 | return null;
27 | };
28 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/ReleaseStatusEnum.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | /**
16 | *
17 | * @export
18 | * @enum {string}
19 | */
20 | export enum ReleaseStatusEnum {
21 | NUMBER_0 = 0,
22 | NUMBER_1 = 1,
23 | NUMBER_2 = 2
24 | }
25 |
26 |
27 | export function ReleaseStatusEnumFromJSON(json: any): ReleaseStatusEnum {
28 | return ReleaseStatusEnumFromJSONTyped(json, false);
29 | }
30 |
31 | export function ReleaseStatusEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): ReleaseStatusEnum {
32 | return json as ReleaseStatusEnum;
33 | }
34 |
35 | export function ReleaseStatusEnumToJSON(value?: ReleaseStatusEnum | null): any {
36 | return value as any;
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/deploy/titan/variables.tf:
--------------------------------------------------------------------------------
1 | variable "name" {
2 | description = "Name of this service"
3 | type = string
4 | }
5 |
6 | variable "gcp_project" {
7 | description = "Google Cloud project where all resources are located"
8 | type = string
9 | }
10 |
11 | variable "gcp_region" {
12 | description = "Google Cloud region where all resources are located"
13 | type = string
14 | }
15 |
16 | variable "gcp_zone" {
17 | description = "Google Cloud zone where all zonal resources are located"
18 | type = string
19 | }
20 |
21 | variable "labels" {
22 | description = "The labels to attach to resources created by this module"
23 | type = map(string)
24 | }
25 |
26 | variable "image" {
27 | description = "Image for the Docker container to be run"
28 | type = string
29 | }
30 |
31 | variable "storage_names" {
32 | description = "Name of Google Cloud Storage buckets to be scanned"
33 | type = list(string)
34 | }
35 |
36 | variable "trigger_tag_pattern" {
37 | description = "Regex pattern for git tags that trigger Titan builds"
38 | type = string
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/ScrimmageStatusEnum.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | /**
16 | *
17 | * @export
18 | * @enum {string}
19 | */
20 | export enum ScrimmageStatusEnum {
21 | P = 'P',
22 | Y = 'Y',
23 | N = 'N'
24 | }
25 |
26 |
27 | export function ScrimmageStatusEnumFromJSON(json: any): ScrimmageStatusEnum {
28 | return ScrimmageStatusEnumFromJSONTyped(json, false);
29 | }
30 |
31 | export function ScrimmageStatusEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): ScrimmageStatusEnum {
32 | return json as ScrimmageStatusEnum;
33 | }
34 |
35 | export function ScrimmageStatusEnumToJSON(value?: ScrimmageStatusEnum | null): any {
36 | return value as any;
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0002_rename_release_version_episode_release_version_public_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-05 02:34
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("episodes", "0001_initial"),
10 | ]
11 |
12 | operations = [
13 | migrations.RenameField(
14 | model_name="episode",
15 | old_name="release_version",
16 | new_name="release_version_public",
17 | ),
18 | migrations.AddField(
19 | model_name="episode",
20 | name="artifact_name",
21 | field=models.CharField(blank=True, max_length=32),
22 | ),
23 | migrations.AddField(
24 | model_name="episode",
25 | name="release_version_saturn",
26 | field=models.CharField(blank=True, max_length=32),
27 | ),
28 | migrations.AddField(
29 | model_name="episode",
30 | name="scaffold",
31 | field=models.URLField(blank=True),
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/frontend/src/utils/utilTypes.ts:
--------------------------------------------------------------------------------
1 | import { PlayerOrderEnum } from "api/_autogen";
2 |
3 | export type Maybe = T | undefined;
4 |
5 | /**
6 | * Helper function to remove nothing types from a type.
7 | * @param val Item to check if is present.
8 | * @returns Whether val is not null or undefined.
9 | */
10 | export function isPresent(val: T | undefined | null): val is T {
11 | return val !== undefined && val !== null;
12 | }
13 |
14 | /**
15 | * Helper function to stringify a PlayerOrderEnum.
16 | * @param order PlayerOrderEnum to stringify.
17 | * @returns Description of the player order.
18 | */
19 | export const stringifyPlayerOrder = (order: PlayerOrderEnum): string => {
20 | switch (order) {
21 | case PlayerOrderEnum.QuestionMark:
22 | return "Alternating";
23 | case PlayerOrderEnum.Plus:
24 | return "Requestor First";
25 | case PlayerOrderEnum.Minus:
26 | return "Requestor Last";
27 | }
28 | };
29 |
30 | export const MILLIS_SECOND = 1000;
31 | export const SECONDS_MINUTE = 60;
32 | export const MINUTES_HOUR = 60;
33 | export const HOURS_DAY = 24;
34 | export const DAYS_YEAR = 365;
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 MIT Battlecode
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/StatusBccEnum.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | /**
16 | *
17 | * @export
18 | * @enum {string}
19 | */
20 | export enum StatusBccEnum {
21 | New = 'NEW',
22 | Que = 'QUE',
23 | Run = 'RUN',
24 | Try = 'TRY',
25 | Ok = 'OK!',
26 | Err = 'ERR',
27 | Can = 'CAN'
28 | }
29 |
30 |
31 | export function StatusBccEnumFromJSON(json: any): StatusBccEnum {
32 | return StatusBccEnumFromJSONTyped(json, false);
33 | }
34 |
35 | export function StatusBccEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): StatusBccEnum {
36 | return json as StatusBccEnum;
37 | }
38 |
39 | export function StatusBccEnumToJSON(value?: StatusBccEnum | null): any {
40 | return value as any;
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/backend/siarnaq/gcloud/saturn.py:
--------------------------------------------------------------------------------
1 | from concurrent.futures import Future
2 |
3 | import google.cloud.pubsub as pubsub
4 | import structlog
5 | from django.conf import settings
6 |
7 | logger = structlog.get_logger(__name__)
8 |
9 |
10 | class NullPublisher:
11 | def publish(self, *args, **kwargs) -> Future:
12 | f: Future = Future()
13 | f.set_result("none")
14 | return f
15 |
16 | def resume_publish(self, *args, **kwargs) -> None:
17 | pass
18 |
19 | def topic_path(self, *args, **kwargs) -> str:
20 | return "nowhere"
21 |
22 |
23 | def get_publish_client() -> pubsub.PublisherClient | NullPublisher:
24 | if not settings.GCLOUD_ENABLE_ACTIONS:
25 | logger.warn("saturn_disabled", message="Saturn queue is disabled.")
26 | return NullPublisher()
27 |
28 | return pubsub.PublisherClient(
29 | credentials=settings.GCLOUD_CREDENTIALS,
30 | publisher_options=pubsub.types.PublisherOptions(
31 | enable_message_ordering=True,
32 | ),
33 | client_options={
34 | "api_endpoint": "us-east1-pubsub.googleapis.com:443",
35 | },
36 | )
37 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/permissions.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import get_object_or_404
2 | from rest_framework import permissions
3 |
4 | from siarnaq.api.episodes.models import Episode
5 |
6 |
7 | class IsEpisodeAvailable(permissions.BasePermission):
8 | """
9 | Allows access to visible episodes. Episodes that are not visible will raise a 404.
10 | """
11 |
12 | def has_permission(self, request, view):
13 | get_object_or_404(
14 | Episode.objects.visible_to_user(is_staff=request.user.is_staff),
15 | pk=view.kwargs["episode_id"],
16 | )
17 | return True
18 |
19 |
20 | class IsEpisodeMutable(permissions.BasePermission):
21 | """
22 | Allows mutation access to visible episodes iff it is not frozen. Episodes that are
23 | not visible will raise a 404.
24 | """
25 |
26 | def has_permission(self, request, view):
27 | episode = get_object_or_404(
28 | Episode.objects.visible_to_user(is_staff=request.user.is_staff),
29 | pk=view.kwargs["episode_id"],
30 | )
31 | return request.method in permissions.SAFE_METHODS or not episode.frozen()
32 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/.openapi-generator-ignore:
--------------------------------------------------------------------------------
1 | # OpenAPI Generator Ignore
2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator
3 |
4 | # Use this file to prevent files from being overwritten by the generator.
5 | # The patterns follow closely to .gitignore or .dockerignore.
6 |
7 | # As an example, the C# client generator defines ApiClient.cs.
8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
9 | #ApiClient.cs
10 |
11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*):
12 | #foo/*/qux
13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
14 |
15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**):
16 | #foo/**/qux
17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
18 |
19 | # You can also negate patterns with an exclamation (!).
20 | # For example, you can ignore all files in a docs folder with the file extension .md:
21 | #docs/*.md
22 | # Then explicitly reverse the ignore rule for a single file:
23 | #!docs/README.md
24 |
--------------------------------------------------------------------------------
/frontend/src/components/team/SearchTeamsMenu.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import AsyncSelectMenu from "../elements/AsyncSelectMenu";
3 | import { useSearchTeams } from "../../api/team/useTeam";
4 | import type { TeamPublic } from "../../api/_autogen";
5 |
6 | interface SearchMenuProps {
7 | onChange: (selection: { value: number; label: string } | null) => void;
8 | selected: { value: number; label: string } | null;
9 | label?: string;
10 | required?: boolean;
11 | placeholder?: string;
12 | className?: string;
13 | }
14 |
15 | const SearchTeamsMenu: React.FC = ({
16 | onChange,
17 | selected,
18 | label,
19 | required,
20 | placeholder,
21 | className,
22 | }) => (
23 |
24 | useQueryResult={useSearchTeams}
25 | resultToOptions={(result) =>
26 | result.map((team) => ({
27 | value: team.id,
28 | label: team.name,
29 | }))
30 | }
31 | onChange={onChange}
32 | selected={selected}
33 | label={label}
34 | required={required}
35 | placeholder={placeholder}
36 | className={className}
37 | />
38 | );
39 |
40 | export default SearchTeamsMenu;
41 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/compete/migrations/0008_alter_matchparticipant_match_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-24 16:58
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("compete", "0007_alter_match_tournament_round"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="matchparticipant",
16 | name="match",
17 | field=models.ForeignKey(
18 | on_delete=django.db.models.deletion.CASCADE,
19 | related_name="participants",
20 | to="compete.match",
21 | ),
22 | ),
23 | migrations.AlterField(
24 | model_name="matchparticipant",
25 | name="previous_participation",
26 | field=models.OneToOneField(
27 | blank=True,
28 | null=True,
29 | on_delete=django.db.models.deletion.RESTRICT,
30 | related_name="next_participation",
31 | to="compete.matchparticipant",
32 | ),
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/homeLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import type { LoaderFunction } from "react-router-dom";
3 | import { safeEnsureQueryData } from "../helpers";
4 | import {
5 | episodeInfoFactory,
6 | nextTournamentFactory,
7 | } from "../episode/episodeFactories";
8 | import {
9 | userRatingHistoryFactory,
10 | scrimmagingRecordFactory,
11 | } from "api/compete/competeFactories";
12 |
13 | export const homeLoader =
14 | (queryClient: QueryClient): LoaderFunction =>
15 | ({ params }) => {
16 | const { episodeId } = params;
17 | if (episodeId === undefined) return null;
18 |
19 | // Episode Info
20 | safeEnsureQueryData({ id: episodeId }, episodeInfoFactory, queryClient);
21 |
22 | // Next Tournament
23 | safeEnsureQueryData({ episodeId }, nextTournamentFactory, queryClient);
24 |
25 | // User Team Rating History
26 | safeEnsureQueryData({ episodeId }, userRatingHistoryFactory, queryClient);
27 |
28 | // User Team Scrimmage Record
29 | safeEnsureQueryData(
30 | {
31 | episodeId,
32 | },
33 | scrimmagingRecordFactory,
34 | queryClient,
35 | );
36 |
37 | return null;
38 | };
39 |
--------------------------------------------------------------------------------
/frontend/src/components/compete/MatchStatus.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { type Match, StatusBccEnum } from "../../api/_autogen/models";
3 | import Tooltip from "../elements/Tooltip";
4 |
5 | interface StatusProps {
6 | match: Match;
7 | }
8 |
9 | const MatchStatusDisplays: Record = {
10 | [StatusBccEnum.New]: "Created",
11 | [StatusBccEnum.Que]: "Queued",
12 | [StatusBccEnum.Run]: "Running",
13 | [StatusBccEnum.Try]: "Will be retried",
14 | [StatusBccEnum.Ok]: "Success",
15 | [StatusBccEnum.Err]: "Failed",
16 | [StatusBccEnum.Can]: "Cancelled",
17 | };
18 |
19 | const MatchStatus: React.FC = ({ match }: StatusProps) => {
20 | const { [match.status]: displayStatus } = MatchStatusDisplays;
21 |
22 | return (
23 | <>
24 | {match.status === StatusBccEnum.Err ? (
25 |
30 | {displayStatus}
31 |
32 | ) : (
33 | <>{displayStatus}>
34 | )}
35 | >
36 | );
37 | };
38 |
39 | export default MatchStatus;
40 |
--------------------------------------------------------------------------------
/titan/pkg/titan/file.go:
--------------------------------------------------------------------------------
1 | package titan
2 |
3 | import (
4 | "context"
5 | "io"
6 | )
7 |
8 | // FileStatus is the Titan status of a file, indicating whether it has been scanned and
9 | // whether it is safe. Unverified files are yet to be scanned, and untracked files are
10 | // ignored by the Titan system.
11 | type FileStatus string
12 |
13 | const (
14 | TitanStatusUntracked FileStatus = "Untracked"
15 | TitanStatusUnverified FileStatus = "Unverified"
16 | TitanStatusVerified FileStatus = "Verified"
17 | TitanStatusMalicious FileStatus = "Malicious"
18 | )
19 |
20 | // StorageClient is a client to a file storage service.
21 | type StorageClient interface {
22 | // GetFile retrieves the file specified by an event.
23 | GetFile(context.Context, *EventPayload) (File, error)
24 | }
25 |
26 | // File is a handle to manage a file stored in a file storage service.
27 | type File interface {
28 | // NewReader creates a new reader to read the contents of the file.
29 | NewReader(context.Context) (io.ReadCloser, error)
30 |
31 | // Status retrieves the Titan status of the file.
32 | Status() FileStatus
33 |
34 | // SetStatus updates the Titan status of the file.
35 | SetStatus(context.Context, FileStatus) error
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/ScrimmageRequestAcceptRejectEnum.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | /**
16 | *
17 | * @export
18 | * @enum {string}
19 | */
20 | export enum ScrimmageRequestAcceptRejectEnum {
21 | A = 'A',
22 | R = 'R',
23 | M = 'M'
24 | }
25 |
26 |
27 | export function ScrimmageRequestAcceptRejectEnumFromJSON(json: any): ScrimmageRequestAcceptRejectEnum {
28 | return ScrimmageRequestAcceptRejectEnumFromJSONTyped(json, false);
29 | }
30 |
31 | export function ScrimmageRequestAcceptRejectEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): ScrimmageRequestAcceptRejectEnum {
32 | return json as ScrimmageRequestAcceptRejectEnum;
33 | }
34 |
35 | export function ScrimmageRequestAcceptRejectEnumToJSON(value?: ScrimmageRequestAcceptRejectEnum | null): any {
36 | return value as any;
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/frontend/generate_types.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Can't run the script? You may need to run `chmod +x generate_types.sh` to make this script executable.
4 | # This script should be run from the frontend folder.
5 |
6 | # Exit the script if an error occurs
7 | set -e
8 |
9 | echo "Running generate_types.sh"
10 |
11 | cd ../backend
12 |
13 | echo "Generating OpenAPI 3.0 backend schema from siarnaq.."
14 | # `conda run -n galaxy` runs the command inside the conda environment.
15 | conda run -n galaxy python manage.py spectacular --file ../frontend/schema.yml --validate
16 |
17 | cd ../frontend
18 |
19 | echo "Formatting schema.yml with prettier.."
20 | npx prettier --write schema.yml
21 |
22 | # Delete the types folder before regenerating it.
23 | echo "Deleting the old types folder.."
24 | rm -rf ./src/api/_autogen
25 |
26 | # Generate types folder
27 | echo "Generating typescript types from schema, placing them in frontend/src/api/_autogen.."
28 | mkdir src/api/_autogen
29 | conda run -n galaxy npx @openapitools/openapi-generator-cli generate -i schema.yml -o src/api/_autogen -g typescript-fetch --additional-properties=modelPropertyNaming=original --additional-properties=disallowAdditionalPropertiesIfNotPresent=false --additional-properties=stringEnums=true
30 |
--------------------------------------------------------------------------------
/frontend/src/components/elements/Markdown.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import ReactMarkdown from "react-markdown";
3 | import { Link } from "react-router-dom";
4 |
5 | interface MarkdownProps {
6 | text: string;
7 | className?: string;
8 | }
9 |
10 | const isInternalLink = (to: string): boolean => {
11 | const url = new URL(to, window.location.origin);
12 | return url.hostname === window.location.hostname;
13 | };
14 |
15 | const Markdown: React.FC = ({ text, className = "" }) => (
16 | {
20 | const target = href ?? "";
21 | if (isInternalLink(target)) {
22 | return (
23 |
28 | );
29 | } else {
30 | return (
31 |
36 | );
37 | }
38 | },
39 | }}
40 | >
41 | {text}
42 |
43 | );
44 |
45 | export default Markdown;
46 |
--------------------------------------------------------------------------------
/environment-dev.yml:
--------------------------------------------------------------------------------
1 | name: galaxy
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - black=22.10.0
6 | - ca-certificates=2022.9.24
7 | - click<8.1.0
8 | - coverage=6.5.0
9 | - django=4.1.2
10 | - django-cors-headers=3.13.0
11 | - django-stubs<1.12.0
12 | - djangorestframework=3.14.0
13 | - drf-spectacular=0.24.2
14 | - flake8=5.0.4
15 | - go=1.18.5
16 | - google-auth=2.13.0
17 | - google-cloud-pubsub=2.13.10
18 | - google-cloud-secret-manager=2.12.6
19 | - google-cloud-storage=2.5.0
20 | - isort=5.10.1
21 | - mypy=1.8.0
22 | - nodejs=20
23 | - openjdk=11
24 | - openssl>=3.0.15
25 | - pillow=9.0.1
26 | - pip=22.3
27 | - pre_commit=2.20.0
28 | - psycopg2=2.9.3
29 | - python=3.10
30 | - pyupgrade=3.1.0
31 | - terraform=1.3.4
32 | - pip:
33 | - django-anymail==8.6
34 | - django-configurations==2.4
35 | - django-countries==7.4.2
36 | - django-import-export==3.0.2
37 | - django-sortedm2m==4.0.0
38 | - django-storages[google]==1.13.1
39 | - django-structlog==4.0.1
40 | - django_rest_passwordreset==1.3.0
41 | - djangorestframework-simplejwt==5.2.2
42 | - google-cloud-scheduler==2.7.3
43 | - google-cloud-tasks==2.12.0
44 | - requests==2.28.1
45 | - types-requests==2.28.11.7
46 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/teams/migrations/0003_alter_team_name_alter_teamprofile_quote.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2022-11-28 19:56
2 |
3 | import django.core.validators
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("teams", "0002_initial"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="team",
16 | name="name",
17 | field=models.CharField(
18 | max_length=32,
19 | validators=[
20 | django.core.validators.RegexValidator(
21 | "^[ -~]*$", message="Use ASCII characters only."
22 | )
23 | ],
24 | ),
25 | ),
26 | migrations.AlterField(
27 | model_name="teamprofile",
28 | name="quote",
29 | field=models.CharField(
30 | blank=True,
31 | max_length=80,
32 | validators=[
33 | django.core.validators.RegexValidator(
34 | "^[ -~]*$", message="Use ASCII characters only."
35 | )
36 | ],
37 | ),
38 | ),
39 | ]
40 |
--------------------------------------------------------------------------------
/frontend/src/components/elements/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import Icon from "./Icon";
3 | import { Switch } from "@headlessui/react";
4 | import FormLabel from "./FormLabel";
5 |
6 | interface CheckboxProps {
7 | disabled?: boolean;
8 | checked?: boolean;
9 | onChange: (checked: boolean) => void;
10 | label?: string;
11 | }
12 |
13 | const Checkbox: React.FC = ({
14 | disabled = false,
15 | checked = false,
16 | onChange,
17 | label,
18 | }) => (
19 |
20 | {label !== undefined && (
21 |
22 |
23 |
24 | )}
25 |
33 |
38 |
39 |
40 | );
41 |
42 | export default Checkbox;
43 |
--------------------------------------------------------------------------------
/frontend/src/components/SectionCard.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import Spinner from "./Spinner";
3 |
4 | interface SectionCardProps {
5 | loading?: boolean;
6 | children?: React.ReactNode;
7 | title?: string;
8 | className?: string;
9 | allowOverflow?: boolean;
10 | }
11 |
12 | const SectionCard: React.FC = ({
13 | children,
14 | title,
15 | loading = false,
16 | className = "",
17 | allowOverflow = false,
18 | }) => (
19 |
20 |
25 | {title !== undefined && (
26 |
27 |
28 | {title}
29 |
30 |
31 | )}
32 | {loading ?
: children}
33 |
34 |
35 | );
36 |
37 | const LoadingContent: React.FC = () => (
38 |
39 | Loading
40 |
41 |
42 | );
43 |
44 | export default SectionCard;
45 |
--------------------------------------------------------------------------------
/frontend/src/components/DocumentationPage.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import ReactMarkdown from "react-markdown";
3 | import { Link } from "react-router-dom";
4 |
5 | const isInternalLink = (to: string): boolean => {
6 | const url = new URL(to, window.location.origin);
7 | return url.hostname === window.location.hostname;
8 | };
9 |
10 | interface DocumentationPageProps {
11 | text: string;
12 | }
13 |
14 | const DocumentationPage: React.FC = ({ text }) => (
15 |
16 |
{
19 | const target = href ?? "";
20 | if (isInternalLink(target)) {
21 | return (
22 |
27 | );
28 | } else {
29 | return (
30 |
35 | );
36 | }
37 | },
38 | }}
39 | >
40 | {text}
41 |
42 |
43 | );
44 |
45 | export default DocumentationPage;
46 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/teams/migrations/0008_remove_teamprofile_auto_accept_ranked_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2025-01-23 07:56
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("teams", "0007_teamprofile_has_report"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name="teamprofile",
15 | name="auto_accept_ranked",
16 | ),
17 | migrations.RemoveField(
18 | model_name="teamprofile",
19 | name="auto_accept_unranked",
20 | ),
21 | migrations.AddField(
22 | model_name="teamprofile",
23 | name="auto_accept_reject_ranked",
24 | field=models.CharField(
25 | choices=[("A", "Auto Accept"), ("R", "Auto Reject"), ("M", "Manual")],
26 | default="M",
27 | max_length=1,
28 | ),
29 | ),
30 | migrations.AddField(
31 | model_name="teamprofile",
32 | name="auto_accept_reject_unranked",
33 | field=models.CharField(
34 | choices=[("A", "Auto Accept"), ("R", "Auto Reject"), ("M", "Manual")],
35 | default="M",
36 | max_length=1,
37 | ),
38 | ),
39 | ]
40 |
--------------------------------------------------------------------------------
/titan/pkg/titan/scan.go:
--------------------------------------------------------------------------------
1 | package titan
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/baruwa-enterprise/clamd"
8 | )
9 |
10 | type Scanner interface {
11 | // Scan scans a file for malware, returning all malware signatures that are found.
12 | // An empty slice indicates that the file is safe.
13 | Scan(ctx context.Context, file File) ([]string, error)
14 | }
15 |
16 | // ClamdClient is a client to clamd, the ClamAV daemon.
17 | type ClamdClient struct {
18 | client *clamd.Client
19 | }
20 |
21 | // NewClamdClient creates a new client to clamd.
22 | func NewClamdClient(ctx context.Context) (*ClamdClient, error) {
23 | client, err := clamd.NewClient("", "")
24 | if err != nil {
25 | return nil, fmt.Errorf("clamd.NewClient: %v", err)
26 | }
27 | return &ClamdClient{client}, nil
28 | }
29 |
30 | func (c *ClamdClient) Scan(ctx context.Context, file File) ([]string, error) {
31 | reader, err := file.NewReader(ctx)
32 | if err != nil {
33 | return nil, fmt.Errorf("file.NewReader: %v", err)
34 | }
35 | defer reader.Close()
36 |
37 | responses, err := c.client.ScanReader(ctx, reader)
38 | if err != nil {
39 | return nil, fmt.Errorf("client.ScanReader: %v", err)
40 | }
41 |
42 | var signatures []string
43 | for _, r := range responses {
44 | if r.Status != "OK" {
45 | signatures = append(signatures, r.Signature)
46 | }
47 | }
48 | return signatures, nil
49 | }
50 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/user/migrations/0002_add_superuser_and_groups.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2022-11-26 20:37
2 |
3 | from django.conf import settings
4 | from django.contrib.auth.management import create_permissions
5 | from django.db import migrations
6 |
7 | def create_superuser(apps, schema_editor):
8 | User = apps.get_model("user", "User")
9 | User.objects.create_superuser(
10 | username="admin",
11 | password=settings.SUPERUSER_PASSWORD,
12 | first_name="Guardian",
13 | last_name="of the Galaxy",
14 | email="battlecode@mit.edu",
15 | )
16 |
17 | def create_dev_group(apps, schema_editor):
18 | # Permissions are not created yet, so we force them to exist.
19 | for app_config in apps.get_app_configs():
20 | app_config.models_module = True
21 | create_permissions(app_config, verbosity=0)
22 |
23 | Permission = apps.get_model("auth", "Permission")
24 | Group = apps.get_model("auth", "Group")
25 | dev_administrator, _ = Group.objects.get_or_create(name="dev_administrators")
26 | dev_administrator.permissions.set(Permission.objects.all())
27 |
28 | class Migration(migrations.Migration):
29 |
30 | dependencies = [
31 | ("user", "0001_initial"),
32 | ]
33 |
34 | operations = [
35 | migrations.RunPython(create_superuser),
36 | migrations.RunPython(create_dev_group),
37 | ]
38 |
--------------------------------------------------------------------------------
/frontend/src/views/PageNotFound.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import Button from "components/elements/Button";
4 | import { useEpisodeId } from "contexts/EpisodeContext";
5 | import { PageContainer } from "components/elements/BattlecodeStyle";
6 |
7 | const PageNotFound: React.FC = () => {
8 | const navigate = useNavigate();
9 | const { episodeId } = useEpisodeId();
10 | return (
11 |
12 |
26 |
27 |
28 | Page Not Found
29 |
30 |
31 | {
35 | navigate(`/${episodeId}/home`);
36 | }}
37 | />
38 |
39 | );
40 | };
41 |
42 | export default PageNotFound;
43 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0003_remove_tournament_challonge_private_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-17 06:57
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | (
10 | "episodes",
11 | "0002_rename_release_version_episode_release_version_public_and_more",
12 | ),
13 | ]
14 |
15 | operations = [
16 | migrations.RemoveField(
17 | model_name="tournament",
18 | name="challonge_private",
19 | ),
20 | migrations.RemoveField(
21 | model_name="tournament",
22 | name="challonge_public",
23 | ),
24 | migrations.RemoveField(
25 | model_name="tournament",
26 | name="in_progress",
27 | ),
28 | migrations.AddField(
29 | model_name="tournament",
30 | name="challonge_id_private",
31 | field=models.SlugField(blank=True, null=True),
32 | ),
33 | migrations.AddField(
34 | model_name="tournament",
35 | name="challonge_id_public",
36 | field=models.SlugField(blank=True, null=True),
37 | ),
38 | migrations.AddField(
39 | model_name="tournamentround",
40 | name="in_progress",
41 | field=models.BooleanField(default=False),
42 | ),
43 | ]
44 |
--------------------------------------------------------------------------------
/saturn/pkg/run/recipe.go:
--------------------------------------------------------------------------------
1 | package run
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/battlecode/galaxy/saturn/pkg/saturn"
9 | "github.com/rs/zerolog/log"
10 | )
11 |
12 | type StepArguments struct {
13 | Details interface{}
14 | StorageClient
15 | saturn.Finisher
16 | }
17 |
18 | type Step struct {
19 | Name string
20 | Callable func(ctx context.Context, arg *StepArguments) error
21 | }
22 |
23 | func (s *Step) Run(ctx context.Context, desc string, arg *StepArguments) error {
24 | log.Ctx(ctx).Debug().Msgf(">>> Starting %s: %s", desc, s.Name)
25 | defer log.Ctx(ctx).Debug().Msgf(">>> Ending %s\n", desc)
26 | return s.Callable(ctx, arg)
27 | }
28 |
29 | type Recipe []*Step
30 |
31 | func (r Recipe) Run(ctx context.Context, arg *StepArguments) error {
32 | for i, step := range r {
33 | desc := fmt.Sprintf("step %d/%d", i+1, len(r))
34 | if err := step.Run(ctx, desc, arg); err != nil {
35 | log.Ctx(ctx).Warn().Err(err).Msgf("Step returned with an error: %v.", err)
36 | return fmt.Errorf("step.Run: %v", err)
37 | }
38 | }
39 | return nil
40 | }
41 |
42 | var StateVersion = Step{
43 | Name: "Hello world",
44 | Callable: func(ctx context.Context, arg *StepArguments) error {
45 | log.Ctx(ctx).Debug().Msg("Welcome to Saturn!")
46 | log.Ctx(ctx).Debug().Msgf("Node: %s", os.Getenv("HOSTNAME"))
47 | log.Ctx(ctx).Debug().Msgf("Revision: %s", os.Getenv("SATURN_REVISION"))
48 | return nil
49 | },
50 | }
51 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/compete/migrations/0009_adminsettings.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2025-01-13 19:22
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | def create_default_admin_settings(apps, schema_editor):
7 | # Get the AdminSettings model
8 | AdminSettings = apps.get_model("compete", "AdminSettings")
9 | if not AdminSettings.objects.exists():
10 | # Create a default instance
11 | AdminSettings.objects.create(is_allowed_ranked_scrimmages=True)
12 |
13 |
14 | class Migration(migrations.Migration):
15 |
16 | dependencies = [
17 | ("compete", "0008_alter_matchparticipant_match_and_more"),
18 | ]
19 |
20 | operations = [
21 | migrations.CreateModel(
22 | name="AdminSettings",
23 | fields=[
24 | (
25 | "id",
26 | models.BigAutoField(
27 | auto_created=True,
28 | primary_key=True,
29 | serialize=False,
30 | verbose_name="ID",
31 | ),
32 | ),
33 | ("is_allowed_ranked_scrimmages", models.BooleanField(default=True)),
34 | ],
35 | options={
36 | "verbose_name": "Admin Setting",
37 | "verbose_name_plural": "Admin Settings",
38 | },
39 | ),
40 | migrations.RunPython(create_default_admin_settings),
41 | ]
42 |
--------------------------------------------------------------------------------
/frontend/src/components/ScrimmageAcceptRejectLabel.tsx:
--------------------------------------------------------------------------------
1 | import { ScrimmageRequestAcceptRejectEnum } from "api/_autogen";
2 | import Icon from "./elements/Icon";
3 |
4 | interface ScrimmageAcceptRejectLabelProps {
5 | acceptRejectStatus?: ScrimmageRequestAcceptRejectEnum;
6 | }
7 |
8 | const ScrimmageAcceptRejectLabel: React.FC = ({
9 | acceptRejectStatus = ScrimmageRequestAcceptRejectEnum.M,
10 | }) => {
11 | const baseClasses =
12 | "flex flex-row items-center justify-center gap-2 py-1.5 rounded-lg border-1 w-36";
13 |
14 | switch (acceptRejectStatus) {
15 | case ScrimmageRequestAcceptRejectEnum.A:
16 | return (
17 |
20 | Auto-Accept
21 |
22 |
23 | );
24 | case ScrimmageRequestAcceptRejectEnum.R:
25 | return (
26 |
29 | Auto-Reject
30 |
31 |
32 | );
33 | default:
34 | return (
35 |
38 | Manual
39 |
40 |
41 | );
42 | }
43 | };
44 |
45 | export default ScrimmageAcceptRejectLabel;
46 |
--------------------------------------------------------------------------------
/deploy/cpw/variables.tf:
--------------------------------------------------------------------------------
1 | variable "name" {
2 | description = "Name of this service"
3 | type = string
4 | }
5 |
6 | variable "gcp_project" {
7 | description = "Google Cloud project where all resources are located"
8 | type = string
9 | }
10 |
11 | variable "gcp_region" {
12 | description = "Google Cloud region where all resources are located"
13 | type = string
14 | }
15 |
16 | variable "gcp_zone" {
17 | description = "Google Cloud zone where all zonal resources are located"
18 | type = string
19 | }
20 |
21 | variable "labels" {
22 | description = "The labels to attach to resources created by this module"
23 | type = map(string)
24 | }
25 |
26 | variable "secret_id" {
27 | description = "ID of the Secret resource"
28 | type = string
29 | }
30 |
31 | # variable "network_vpc_id" {
32 | # description = "ID of Google VPC network resource"
33 | # type = string
34 | # }
35 |
36 | variable "subnetwork_ip_cidr" {
37 | description = "Range of internal IP addresses to be allocated to this service"
38 | type = string
39 | }
40 |
41 | variable "machine_type" {
42 | description = "Type of instance to use in the worker pool"
43 | type = string
44 | }
45 |
46 | variable "image" {
47 | description = "Image for the Docker container to be run"
48 | type = string
49 | }
50 |
51 | variable "disk_size" {
52 | description = "disk size(in GB) for the container"
53 | type = number
54 | }
55 |
--------------------------------------------------------------------------------
/frontend/src/components/elements/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { forwardRef } from "react";
3 | import FormError from "./FormError";
4 | import FormLabel from "./FormLabel";
5 |
6 | interface TextAreaProps extends React.ComponentPropsWithoutRef<"textarea"> {
7 | label?: string;
8 | required?: boolean;
9 | className?: string;
10 | errorMessage?: string;
11 | }
12 |
13 | const Input = forwardRef(function Input(
14 | { label, required = false, className = "", errorMessage, ...rest },
15 | ref,
16 | ) {
17 | const invalid = errorMessage !== undefined;
18 | return (
19 |
20 |
21 |
22 |
23 |
32 |
33 | {invalid && }
34 |
35 |
36 | );
37 | });
38 |
39 | export default Input;
40 |
--------------------------------------------------------------------------------
/deploy/network/variables.tf:
--------------------------------------------------------------------------------
1 | variable "name" {
2 | description = "Name of this service"
3 | type = string
4 | }
5 |
6 | variable "gcp_project" {
7 | description = "Google Cloud project where all resources are located"
8 | type = string
9 | }
10 |
11 | variable "gcp_region" {
12 | description = "Google Cloud region where all resources are located"
13 | type = string
14 | }
15 |
16 | variable "gcp_zone" {
17 | description = "Google Cloud zone where all zonal resources are located"
18 | type = string
19 | }
20 |
21 | variable "labels" {
22 | description = "The labels to attach to resources created by this module"
23 | type = map(string)
24 | }
25 |
26 | variable "subdomain" {
27 | description = "Subomain under which all networking resources belong"
28 | type = string
29 | }
30 |
31 | variable "https_redirect" {
32 | description = "Whether to redirect to SSL."
33 | type = bool
34 | }
35 |
36 | variable "cloudrun_service_name" {
37 | description = "The name of the Cloud Run service to be routed to"
38 | type = string
39 | }
40 |
41 | variable "sql_instance_ip" {
42 | description = "The IP address of the Cloud SQL instance"
43 | type = string
44 | }
45 |
46 | variable "additional_buckets" {
47 | description = "Specifications for any additional buckets to be routed"
48 | type = map(object({
49 | bucket_name=string,
50 | enable_cdn=bool,
51 | cdn_cache_ttl=number,
52 | subsubdomain=string,
53 | }))
54 | }
55 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/queueLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import { redirect, type LoaderFunction } from "react-router-dom";
3 | import { matchListFactory } from "../compete/competeFactories";
4 | import { buildKey, safeEnsureQueryData } from "../helpers";
5 | import { searchTeamsFactory } from "../team/teamFactories";
6 | import type { Episode } from "api/_autogen";
7 | import { episodeInfoFactory } from "api/episode/episodeFactories";
8 | import toast from "react-hot-toast";
9 |
10 | export const queueLoader =
11 | (queryClient: QueryClient): LoaderFunction =>
12 | async ({ params }) => {
13 | const { episodeId } = params;
14 | if (episodeId === undefined) return null;
15 |
16 | // Ensure that this page is available for the episode
17 | const episodeData = queryClient.ensureQueryData({
18 | queryKey: buildKey(episodeInfoFactory.queryKey, { id: episodeId }),
19 | queryFn: async () => await episodeInfoFactory.queryFn({ id: episodeId }),
20 | });
21 |
22 | if ((await episodeData).game_release.getTime() > Date.now()) {
23 | toast.error(
24 | `Queue page not released yet for ${(await episodeData).name_long}.`,
25 | );
26 | return redirect(`/${episodeId}/home`);
27 | }
28 |
29 | // All matches
30 | safeEnsureQueryData({ episodeId }, matchListFactory, queryClient);
31 |
32 | // All teams
33 | safeEnsureQueryData({ episodeId }, searchTeamsFactory, queryClient);
34 |
35 | return null;
36 | };
37 |
--------------------------------------------------------------------------------
/frontend/src/components/MatchReplayButton.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { StatusBccEnum, type Episode, type Match } from "api/_autogen";
3 | import { isPresent, type Maybe } from "utils/utilTypes";
4 | import Button from "./elements/Button";
5 | import Tooltip from "./elements/Tooltip";
6 | import type { UseQueryResult } from "@tanstack/react-query";
7 | import { getClientUrl } from "api/helpers";
8 |
9 | interface MatchReplayButtonProps {
10 | episode: UseQueryResult;
11 | match: Maybe;
12 | }
13 |
14 | const MatchReplayButton: React.FC = ({
15 | episode,
16 | match,
17 | }) => {
18 | const disabled =
19 | !episode.isSuccess ||
20 | !isPresent(match) ||
21 | !isPresent(match.replay_url) ||
22 | match.status !== StatusBccEnum.Ok;
23 |
24 | const clientUrl = getClientUrl(
25 | episode.data?.name_short,
26 | episode.data?.artifact_name,
27 | episode.data?.release_version_client,
28 | match?.replay_url ?? "",
29 | );
30 |
31 | const button = (
32 | {
36 | if (isPresent(clientUrl)) {
37 | window.open(clientUrl);
38 | }
39 | }}
40 | />
41 | );
42 |
43 | if (disabled) {
44 | return (
45 | {button}
46 | );
47 | } else {
48 | return button;
49 | }
50 | };
51 |
52 | export default MatchReplayButton;
53 |
--------------------------------------------------------------------------------
/frontend/src/components/elements/Collapse.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { Fragment } from "react";
3 | import { Disclosure, Transition } from "@headlessui/react";
4 | import Icon from "./Icon";
5 |
6 | // Example usage:
7 | //
8 | // Some content here
9 | //
10 |
11 | interface CollapseProps {
12 | children?: React.ReactNode;
13 | title: string;
14 | }
15 |
16 | const Collapse: React.FC = ({ children, title }) => (
17 |
18 |
22 | {title}
23 |
27 |
28 |
29 |
30 |
39 | {children}
40 |
41 |
42 | );
43 |
44 | export default Collapse;
45 |
--------------------------------------------------------------------------------
/frontend/src/views/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { useRouteError, useNavigate } from "react-router-dom";
3 | import { PageTitle } from "../components/elements/BattlecodeStyle";
4 | import { useEpisodeId } from "../contexts/EpisodeContext";
5 | import Button from "components/elements/Button";
6 |
7 | const ErrorBoundary: React.FC = () => {
8 | const error = useRouteError();
9 | const { episodeId } = useEpisodeId();
10 | const navigate = useNavigate();
11 |
12 | return (
13 |
14 |
15 |
Oops, something went wrong.
16 |
17 | Please reach out on Discord or to battlecode@mit.edu with the
18 | following error message:
19 |
20 | {error instanceof Error ? (
21 |
22 | {error.message}
23 |
24 | ) : (
25 |
26 | An unknown error occurred.
27 |
28 | )}
29 |
{
31 | navigate(`/${episodeId}/home`);
32 | }}
33 | label="Go to Home"
34 | variant="dark"
35 | />
36 |
37 |
38 | );
39 | };
40 |
41 | export default ErrorBoundary;
42 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/teams/migrations/0002_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2022-11-26 20:29
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ("teams", "0001_initial"),
14 | ("episodes", "0001_initial"),
15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16 | ]
17 |
18 | operations = [
19 | migrations.AddField(
20 | model_name="team",
21 | name="members",
22 | field=models.ManyToManyField(
23 | blank=True, related_name="teams", to=settings.AUTH_USER_MODEL
24 | ),
25 | ),
26 | migrations.AddField(
27 | model_name="teamprofile",
28 | name="eligible_for",
29 | field=models.ManyToManyField(
30 | blank=True, related_name="teams", to="episodes.eligibilitycriterion"
31 | ),
32 | ),
33 | migrations.AddField(
34 | model_name="teamprofile",
35 | name="rating",
36 | field=models.OneToOneField(
37 | on_delete=django.db.models.deletion.PROTECT, to="teams.rating"
38 | ),
39 | ),
40 | migrations.AddConstraint(
41 | model_name="team",
42 | constraint=models.UniqueConstraint(
43 | fields=("episode", "name"), name="team-unique-episode-name"
44 | ),
45 | ),
46 | ]
47 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/user/migrations/0006_emailverificationtoken.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.2.8 on 2025-12-05 16:46
2 |
3 | import django.db.models.deletion
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ("user", "0005_user_email_verified"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="EmailVerificationToken",
17 | fields=[
18 | (
19 | "id",
20 | models.BigAutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name="ID",
25 | ),
26 | ),
27 | ("created_at", models.DateTimeField(auto_now_add=True)),
28 | ("token", models.CharField(db_index=True, max_length=64, unique=True)),
29 | (
30 | "user",
31 | models.ForeignKey(
32 | on_delete=django.db.models.deletion.CASCADE,
33 | related_name="email_verification_tokens",
34 | to=settings.AUTH_USER_MODEL,
35 | ),
36 | ),
37 | ],
38 | options={
39 | "verbose_name": "Email Verification Token",
40 | "verbose_name_plural": "Email Verification Tokens",
41 | },
42 | ),
43 | ]
44 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Battlecode Frontend
2 |
3 | The new and improved frontend, for the Battlecode competitor portal.
4 |
5 | ## Local setup
6 |
7 | See [docs/local-setup.md](docs/local-setup.md).
8 |
9 | ## Important scripts
10 |
11 | See [docs/create-react-app.md](docs/create-react-app.md) or [package.json](./package.json) for lesser-used scripts.
12 |
13 | You can run the scripts below in the project directory.
14 |
15 | `npm run start`
16 |
17 | Runs the app in the development mode. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
18 |
19 | `npm run lint`
20 |
21 | Runs ESLint and Prettier checks.
22 |
23 | `npm run format`
24 |
25 | Applies ESLint and Prettier fixes.
26 |
27 | `npm start`
28 |
29 | Runs the app in the development mode. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
30 |
31 | ## Codebase intro
32 |
33 | See [docs/onboard.md](docs/onboard.md) for an introduction to the frontend codebase, in order to make changes. This is helpful not only for frontend devs themselves but also for **just a working knowledge of frontend features, even if you're not a frontend dev!**
34 |
35 | ## Generate API types
36 |
37 | We use a bash script to generate API types / functions from our backend code automatically. To run the bash script, ensure you have followed all the setup setps in the top-level directory and in [docs/local-setup.md](docs/local-setup.md).
38 |
39 | The following steps assume that you begin in the `galaxy` directory.
40 |
41 | 1. `cd ./frontend`
42 | 2. `./generate_types.sh` (runs the bash script)
43 |
--------------------------------------------------------------------------------
/frontend/src/api/team/teamFactories.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | PaginatedTeamPublicList,
3 | TeamPrivate,
4 | TeamPublic,
5 | TeamTListRequest,
6 | TeamTMeRetrieveRequest,
7 | TeamTRetrieveRequest,
8 | } from "../_autogen";
9 | import type { PaginatedQueryFactory, QueryFactory } from "../apiTypes";
10 | import { prefetchNextPage } from "../helpers";
11 | import { getTeamInfo, getUserTeamInfo, searchTeams } from "./teamApi";
12 | import { teamQueryKeys } from "./teamKeys";
13 |
14 | export const myTeamFactory: QueryFactory =
15 | {
16 | queryKey: teamQueryKeys.myTeam,
17 | queryFn: async ({ episodeId }) => await getUserTeamInfo({ episodeId }),
18 | } as const;
19 |
20 | export const otherTeamInfoFactory: QueryFactory<
21 | TeamTRetrieveRequest,
22 | TeamPublic
23 | > = {
24 | queryKey: teamQueryKeys.otherInfo,
25 | queryFn: async ({ episodeId, id }) => await getTeamInfo({ episodeId, id }),
26 | } as const;
27 |
28 | export const searchTeamsFactory: PaginatedQueryFactory<
29 | TeamTListRequest,
30 | PaginatedTeamPublicList
31 | > = {
32 | queryKey: teamQueryKeys.search,
33 | queryFn: async (request, queryClient, prefetchNext) => {
34 | const result = await searchTeams(request);
35 | // Prefetch the next page if we want to prefetch
36 | if (prefetchNext) {
37 | await prefetchNextPage(
38 | request,
39 | result,
40 | { queryKey: teamQueryKeys.search, queryFn: searchTeamsFactory.queryFn },
41 | queryClient,
42 | );
43 | }
44 |
45 | return result;
46 | },
47 | } as const;
48 |
--------------------------------------------------------------------------------
/frontend/src/components/compete/MatchRatingDelta.tsx:
--------------------------------------------------------------------------------
1 | import type { MatchParticipant } from "api/_autogen";
2 | import type React from "react";
3 | import TeamWithRating from "./TeamWithRating";
4 |
5 | interface RatingDeltaProps {
6 | includeTeamName?: boolean;
7 | participant: MatchParticipant;
8 | ranked: boolean;
9 | }
10 |
11 | const MatchRatingDelta: React.FC = ({
12 | includeTeamName,
13 | participant,
14 | ranked,
15 | }) => {
16 | let newRating = 0;
17 | if (ranked) {
18 | newRating =
19 | /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition --
20 | * We need this check for ranked matches against staff teams
21 | */
22 | participant.rating !== null
23 | ? Math.round(participant.rating)
24 | : Math.round(participant.old_rating);
25 | } else {
26 | newRating = Math.round(participant.old_rating);
27 | }
28 | const oldRating =
29 | /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition --
30 | * We need this check for ranked matches against staff teams
31 | */
32 | participant.old_rating !== null ? Math.round(participant.old_rating) : 0;
33 | const ratingDelta = newRating - oldRating;
34 |
35 | const includeName = includeTeamName === undefined || includeTeamName;
36 | return (
37 |
44 | );
45 | };
46 |
47 | export default MatchRatingDelta;
48 |
--------------------------------------------------------------------------------
/frontend/src/content/ContentStruct.ts:
--------------------------------------------------------------------------------
1 | export enum QuickStartPage {
2 | OVERVIEW = "Overview",
3 | ACCOUNT_AND_TEAM_CREATION = "Account and Team Creation",
4 | INSTALLATION_AND_SETUP = "Installation and Setup",
5 | RESOURCES = "Resources",
6 | JOIN_THE_COMMUNITY = "Join the Community!",
7 | }
8 | export type QuickStartPageKey = `${QuickStartPage}`;
9 |
10 | export enum ResourcesPage {
11 | GAME_SPECIFICATION = "Game Specifications",
12 | CODING_RESOURCES = "Coding Resources",
13 | THIRD_PARTY_TOOLS = "Third-party Tools",
14 | LECTURES = "Lectures",
15 | }
16 | export type ResourcesPageKey = `${ResourcesPage}`;
17 |
18 | export enum CommonIssuesPage {
19 | INSTALLATION_ISSUES = "Installation Issues",
20 | CLIENT_ISSUES = "Client Issues",
21 | OTHER_ISSUES = "Other Issues",
22 | THINGS_TO_TRY = "Things to Try When Nothing Else Helps",
23 | }
24 | export type CommonIssuesPageKey = `${CommonIssuesPage}`;
25 |
26 | export enum DebuggingTipsPage {
27 | DEBUGGING = "Debugging",
28 | INTELLIJ = "Debugging in IntelliJ",
29 | ECLIPSE = "Debugging in Eclipse",
30 | SECOND_METHOD = "Second Method: Debugging in IntelliJ",
31 | }
32 | export type DebuggingTipsPageKey = `${DebuggingTipsPage}`;
33 |
34 | export enum TourneyPage {
35 | SCHEDULE = "Tournament Schedule",
36 | PRIZES = "Prizes",
37 | FORMAT = "Tournament Format",
38 | RULES = "Eligibility Rules",
39 | }
40 | export type TourneyPageKey = `${TourneyPage}`;
41 |
42 | export type PageKey =
43 | | QuickStartPageKey
44 | | ResourcesPageKey
45 | | TourneyPageKey
46 | | DebuggingTipsPageKey
47 | | CommonIssuesPageKey;
48 |
--------------------------------------------------------------------------------
/saturn/pkg/run/gcs.go:
--------------------------------------------------------------------------------
1 | package run
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 |
8 | "cloud.google.com/go/storage"
9 | "github.com/rs/zerolog/log"
10 | )
11 |
12 | type GCSClient struct {
13 | c *storage.Client
14 | }
15 |
16 | func NewGCSClient(ctx context.Context) (*GCSClient, error) {
17 | client, err := storage.NewClient(ctx)
18 | if err != nil {
19 | return nil, fmt.Errorf("storage.NewClient: %v", err)
20 | }
21 | return &GCSClient{client}, nil
22 | }
23 |
24 | func (c *GCSClient) GetFile(ctx context.Context, f FileSpecification, w io.Writer) error {
25 | object := c.c.Bucket(f.Bucket).Object(f.Name)
26 | reader, err := object.NewReader(ctx)
27 | if err != nil {
28 | return fmt.Errorf("object.NewReader: %v", err)
29 | }
30 | defer reader.Close()
31 |
32 | written, err := io.Copy(w, reader)
33 | if err != nil {
34 | return fmt.Errorf("io.Copy: %v", err)
35 | }
36 | log.Ctx(ctx).Debug().Msgf("Downloaded %d bytes.", written)
37 | return nil
38 | }
39 |
40 | func (c *GCSClient) UploadFile(ctx context.Context, f FileSpecification, r io.Reader, public bool) error {
41 | object := c.c.Bucket(f.Bucket).Object(f.Name)
42 | writer := object.NewWriter(ctx)
43 | defer writer.Close()
44 |
45 | writer.ContentType = "application/octet-stream"
46 | if public {
47 | writer.PredefinedACL = "publicRead"
48 | } else {
49 | writer.PredefinedACL = "projectPrivate"
50 | }
51 |
52 | written, err := io.Copy(writer, r)
53 | if err != nil {
54 | return fmt.Errorf("io.Copy: %v", err)
55 | }
56 | log.Ctx(ctx).Debug().Msgf("Uploaded %d bytes.", written)
57 | return nil
58 | }
59 |
--------------------------------------------------------------------------------
/frontend/src/components/ResponsiveIframe.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { useRef, useEffect, useState } from "react";
3 | import { isPresent } from "utils/utilTypes";
4 |
5 | interface ResponsiveIframeProps {
6 | url: string;
7 | className?: string;
8 | }
9 |
10 | const ResponsiveIframe: React.FC = ({
11 | url,
12 | className = "",
13 | }) => {
14 | const containerRef = useRef(null);
15 | const [dimensions, setDimensions] = useState({
16 | width: "100%",
17 | height: "100%",
18 | });
19 |
20 | useEffect(() => {
21 | function updateDimensions(): void {
22 | if (isPresent(containerRef.current)) {
23 | const { width, height } = containerRef.current.getBoundingClientRect();
24 | setDimensions({ width: `${width}px`, height: `${height}px` });
25 | }
26 | }
27 |
28 | updateDimensions();
29 | window.addEventListener("resize", updateDimensions);
30 |
31 | return () => {
32 | window.removeEventListener("resize", updateDimensions);
33 | };
34 | }, [
35 | containerRef.current?.getBoundingClientRect().width,
36 | containerRef.current?.getBoundingClientRect().height,
37 | ]);
38 |
39 | return (
40 |
41 |
50 |
51 | );
52 | };
53 |
54 | export default ResponsiveIframe;
55 |
--------------------------------------------------------------------------------
/saturn/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build go app
2 | FROM golang:1.23.4-bookworm AS go
3 |
4 | ENV BUILD_HOME /build
5 | WORKDIR $BUILD_HOME
6 |
7 | COPY go.mod go.sum ./
8 | RUN go mod download
9 |
10 | COPY . .
11 | RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /saturn -ldflags="-s -w" ./cmd/saturn/main.go
12 |
13 | # Link intermediate container for python
14 | FROM python:3.12-slim-bookworm as python
15 |
16 | FROM openjdk:21-jdk-slim-bookworm
17 |
18 | # Setup
19 | RUN echo "deb http://deb.debian.org/debian unstable main" >> /etc/apt/sources.list && \
20 | apt-get update
21 |
22 | # Install JDK8. The base image provides JDK21, but we still need JDK8 to
23 | # run matches with java8
24 | RUN apt-get install -y -t unstable openjdk-8-jdk
25 |
26 | # Copy Python executable, libraries, standard library, site-packages, include files, binary files, and Python path
27 | COPY --from=python /usr/local/bin/python3.12 /usr/local/bin/python3.12
28 | COPY --from=python /usr/local/lib/libpython3.* /usr/local/lib/
29 | COPY --from=python /usr/local/lib/python3.12 /usr/local/lib/python3.12
30 | COPY --from=python /usr/local/include/python3.12 /usr/local/include/python3.12
31 | COPY --from=python /usr/local/bin/pip* /usr/local/bin/
32 | ENV PYTHONPATH=/usr/local/lib/python3.12:/usr/local/lib/python3.12/site-packages
33 |
34 | # Install google cloud package for use in the scaffold
35 | RUN python3.12 -m pip install google-cloud-storage
36 |
37 | ENV APP_HOME /app
38 | WORKDIR $APP_HOME
39 |
40 | ARG REVISION_ARG=nightly
41 | ENV SATURN_REVISION=$REVISION_ARG
42 |
43 | EXPOSE 8005
44 |
45 | COPY --from=go /saturn .
46 | ENTRYPOINT ["./saturn"]
47 |
--------------------------------------------------------------------------------
/frontend/src/content/ManageContent.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | DebuggingTipsPageKey,
3 | TourneyPageKey,
4 | QuickStartPageKey,
5 | ResourcesPageKey,
6 | CommonIssuesPageKey,
7 | PageKey,
8 | } from "./ContentStruct";
9 |
10 | import * as BC26 from "./bc26";
11 | import * as BC25JAVA from "./bc25java";
12 | import * as BC25PYTHON from "./bc25python";
13 | import * as BC24 from "./bc24";
14 | import * as BC23 from "./bc23";
15 | import * as BC22 from "./bc22";
16 |
17 | const bcVersions: Record<
18 | string,
19 | | typeof BC26
20 | | typeof BC25JAVA
21 | | typeof BC25PYTHON
22 | | typeof BC24
23 | | typeof BC23
24 | | typeof BC22
25 | > = {
26 | bc26: BC26,
27 | bc25java: BC25JAVA,
28 | bc25python: BC25PYTHON,
29 | bc24: BC24,
30 | bc23: BC23,
31 | bc22: BC22,
32 | };
33 |
34 | function createTextRecord(
35 | key: string,
36 | ): Record>> {
37 | return Object.fromEntries(
38 | Object.entries(bcVersions).map(([version, content]) => [
39 | version,
40 | content[key as keyof typeof content] as Partial>,
41 | ]),
42 | ) as Record>>;
43 | }
44 |
45 | export const quickStartText = createTextRecord("QUICKSTART");
46 | export const resourcesText = createTextRecord("RESOURCES");
47 | export const tournamentsText = createTextRecord("TOURNAMENTS");
48 | export const debuggingTipsText =
49 | createTextRecord("DEBUGGINGTIPS");
50 | export const commonIssuesText =
51 | createTextRecord("COMMONISSUES");
52 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/SidebarSection.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { useMemo } from "react";
3 | import { renderableItems, type SidebarItemData } from ".";
4 | import SidebarItem from "./SidebarItem";
5 | import { useEpisodeId } from "contexts/EpisodeContext";
6 | import { useUserTeam } from "api/team/useTeam";
7 | import { useEpisodeInfo } from "api/episode/useEpisode";
8 | import { useCurrentUser } from "contexts/CurrentUserContext";
9 |
10 | interface SidebarSectionProps {
11 | items: SidebarItemData[];
12 | title?: string;
13 | }
14 |
15 | const SidebarSection: React.FC = ({ items, title }) => {
16 | const { episodeId } = useEpisodeId();
17 |
18 | const { authState } = useCurrentUser();
19 | const teamData = useUserTeam({ episodeId });
20 | const episodeData = useEpisodeInfo({ id: episodeId });
21 |
22 | const renderedItems = useMemo(
23 | () => renderableItems(items, episodeData, authState, teamData),
24 | [items, episodeData, teamData],
25 | );
26 |
27 | return renderedItems.length > 0 ? (
28 |
29 | {title !== undefined && (
30 |
31 | {title}
32 |
33 | )}
34 |
35 | {renderedItems.map((itemData) => (
36 |
41 | ))}
42 |
43 |
44 | ) : null;
45 | };
46 |
47 | export default SidebarSection;
48 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/teams/migrations/0006_classrequirement.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-27 03:39
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("episodes", "0007_remove_episode_pass_requirement_out_of_and_more"),
11 | ("teams", "0005_alter_teamprofile_auto_accept_ranked"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="ClassRequirement",
17 | fields=[
18 | (
19 | "id",
20 | models.BigAutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name="ID",
25 | ),
26 | ),
27 | ("min_score", models.PositiveSmallIntegerField()),
28 | (
29 | "episode",
30 | models.ForeignKey(
31 | on_delete=django.db.models.deletion.CASCADE,
32 | related_name="reference_players",
33 | to="episodes.episode",
34 | ),
35 | ),
36 | ("maps", models.ManyToManyField(to="episodes.map")),
37 | (
38 | "reference_player",
39 | models.ForeignKey(
40 | on_delete=django.db.models.deletion.CASCADE, to="teams.team"
41 | ),
42 | ),
43 | ],
44 | ),
45 | ]
46 |
--------------------------------------------------------------------------------
/frontend/src/components/HomeCountdown.tsx:
--------------------------------------------------------------------------------
1 | import { useEpisodeInfo, useNextTournament } from "api/episode/useEpisode";
2 | import { useEpisodeId } from "contexts/EpisodeContext";
3 | import { useMemo } from "react";
4 | import CountdownDisplay from "./CountdownDisplay";
5 | import SectionCard from "./SectionCard";
6 | import { isNil } from "lodash";
7 |
8 | const HomeCountdown: React.FC = () => {
9 | const { episodeId } = useEpisodeId();
10 |
11 | const episode = useEpisodeInfo({ id: episodeId });
12 | const nextTournament = useNextTournament({ episodeId });
13 |
14 | const title = useMemo(() => {
15 | if (!episode.isSuccess) {
16 | return "";
17 | } else if (episode.data.game_release.getTime() > Date.now()) {
18 | return "Countdown to Game Release";
19 | } else {
20 | return "Next Submission Deadline";
21 | }
22 | }, [episode]);
23 |
24 | const date = useMemo(() => {
25 | if (!episode.isSuccess) {
26 | return undefined;
27 | } else if (episode.data.game_release.getTime() > Date.now()) {
28 | return episode.data.game_release;
29 | } else if (nextTournament.isSuccess) {
30 | return nextTournament.data?.submission_freeze;
31 | } else {
32 | return undefined;
33 | }
34 | }, [episode, nextTournament]);
35 |
36 | return (
37 |
41 | {!isNil(date) ? (
42 |
43 | ) : (
44 | No upcoming submission deadlines.
45 | )}
46 |
47 | );
48 | };
49 |
50 | export default HomeCountdown;
51 |
--------------------------------------------------------------------------------
/frontend/src/components/tables/submissions/TourneySubTable.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import type { TournamentSubmission } from "../../../api/_autogen";
3 | import type { Maybe } from "../../../utils/utilTypes";
4 | import { NavLink } from "react-router-dom";
5 | import { dateTime } from "../../../utils/dateTime";
6 | import Table from "../../Table";
7 |
8 | interface TourneySubTableProps {
9 | data: Maybe;
10 | loading: boolean;
11 | }
12 |
13 | const TourneySubTable: React.FC = ({ data, loading }) => (
14 |
18 | match.tournament.toString() + "match" + match.id.toString()
19 | }
20 | columns={[
21 | {
22 | header: "Tournament",
23 | key: "tournament",
24 | value: (sub) => sub.tournament,
25 | },
26 | {
27 | header: "Submitted At",
28 | key: "submitted_at",
29 | value: (sub) => dateTime(sub.created).localFullString,
30 | },
31 | {
32 | header: "Description",
33 | key: "description",
34 | value: (sub) => sub.description,
35 | },
36 | {
37 | header: "Package Name",
38 | key: "package",
39 | value: (sub) => sub._package,
40 | },
41 | {
42 | header: "Submitter",
43 | key: "submitter",
44 | value: (sub) => (
45 |
46 | {sub.username}
47 |
48 | ),
49 | },
50 | ]}
51 | />
52 | );
53 |
54 | export default TourneySubTable;
55 |
--------------------------------------------------------------------------------
/backend/siarnaq/api/episodes/migrations/0005_remove_tournamentround_round-unique-tournament-challonge_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.2 on 2023-01-21 18:05
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("episodes", "0004_tournament_display_date"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveConstraint(
14 | model_name="tournamentround",
15 | name="round-unique-tournament-challonge",
16 | ),
17 | migrations.RenameField(
18 | model_name="tournamentround",
19 | old_name="challonge_id",
20 | new_name="external_id",
21 | ),
22 | migrations.RemoveField(
23 | model_name="tournament",
24 | name="challonge_id_private",
25 | ),
26 | migrations.RemoveField(
27 | model_name="tournament",
28 | name="challonge_id_public",
29 | ),
30 | migrations.AddField(
31 | model_name="tournament",
32 | name="external_id_private",
33 | field=models.SlugField(blank=True, max_length=128),
34 | ),
35 | migrations.AddField(
36 | model_name="tournament",
37 | name="external_id_public",
38 | field=models.SlugField(blank=True, max_length=128),
39 | ),
40 | migrations.AddConstraint(
41 | model_name="tournamentround",
42 | constraint=models.UniqueConstraint(
43 | fields=("tournament", "external_id"),
44 | name="round-unique-tournament-bracket",
45 | ),
46 | ),
47 | ]
48 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/Email.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | *
18 | * @export
19 | * @interface Email
20 | */
21 | export interface Email {
22 | /**
23 | *
24 | * @type {string}
25 | * @memberof Email
26 | */
27 | email: string;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the Email interface.
32 | */
33 | export function instanceOfEmail(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "email" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function EmailFromJSON(json: any): Email {
41 | return EmailFromJSONTyped(json, false);
42 | }
43 |
44 | export function EmailFromJSONTyped(json: any, ignoreDiscriminator: boolean): Email {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'email': json['email'],
51 | };
52 | }
53 |
54 | export function EmailToJSON(value?: Email | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | 'email': value.email,
64 | };
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/frontend/src/api/loaders/tournamentLoader.ts:
--------------------------------------------------------------------------------
1 | import type { QueryClient } from "@tanstack/react-query";
2 | import { redirect, type LoaderFunction } from "react-router-dom";
3 | import { buildKey, safeEnsureQueryData } from "../helpers";
4 | import {
5 | episodeInfoFactory,
6 | tournamentInfoFactory,
7 | } from "../episode/episodeFactories";
8 | import { tournamentMatchListFactory } from "../compete/competeFactories";
9 | import toast from "react-hot-toast";
10 |
11 | export const tournamentLoader =
12 | (queryClient: QueryClient): LoaderFunction =>
13 | async ({ params }) => {
14 | const { episodeId, tournamentId } = params;
15 | if (episodeId === undefined || tournamentId === undefined) return null;
16 |
17 | // Episode Info
18 | safeEnsureQueryData({ id: episodeId }, episodeInfoFactory, queryClient);
19 |
20 | // Tournament Info
21 | try {
22 | const tournamentInfo = await queryClient.ensureQueryData({
23 | queryKey: buildKey(tournamentInfoFactory.queryKey, {
24 | episodeId,
25 | id: tournamentId,
26 | }),
27 | queryFn: async () =>
28 | await tournamentInfoFactory.queryFn({ episodeId, id: tournamentId }),
29 | });
30 | if (!tournamentInfo.is_public) {
31 | toast.error("This tournament is not public.");
32 | return redirect(`/${episodeId}/home`);
33 | }
34 | } catch (_) {
35 | return redirect(`/${episodeId}/home`);
36 | }
37 |
38 | // Tournament Match List
39 | safeEnsureQueryData(
40 | {
41 | episodeId,
42 | tournamentId,
43 | },
44 | tournamentMatchListFactory,
45 | queryClient,
46 | );
47 |
48 | return null;
49 | };
50 |
--------------------------------------------------------------------------------
/frontend/src/components/elements/Input.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { forwardRef, type ReactElement } from "react";
3 | import FormError from "./FormError";
4 | import FormLabel from "./FormLabel";
5 |
6 | interface InputProps extends React.ComponentPropsWithoutRef<"input"> {
7 | label?: string;
8 | required?: boolean;
9 | className?: string;
10 | errorMessage?: string;
11 | endButton?: ReactElement;
12 | }
13 |
14 | const Input = forwardRef(function Input(
15 | { label, required = false, className = "", errorMessage, endButton, ...rest },
16 | ref,
17 | ) {
18 | const invalid = errorMessage !== undefined;
19 | return (
20 |
21 |
22 |
23 |
24 |
38 | {endButton}
39 |
40 | {invalid && }
41 |
42 |
43 | );
44 | });
45 |
46 | export default Input;
47 |
--------------------------------------------------------------------------------
/backend/siarnaq/bracket/example-data/match.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "id": "350944920",
4 | "type": "match",
5 | "attributes": {
6 | "state": "complete",
7 | "round": 1,
8 | "identifier": "A",
9 | "scores": "0 - 1",
10 | "suggested_play_order": 1,
11 | "score_in_sets": [[0, 1]],
12 | "points_by_participant": [
13 | {
14 | "participant_id": 221134365,
15 | "scores": [0]
16 | },
17 | {
18 | "participant_id": 221134366,
19 | "scores": [1]
20 | }
21 | ],
22 | "timestamps": {
23 | "started_at": "2024-01-15T00:50:33.213Z",
24 | "created_at": "2024-01-15T00:50:33.146Z",
25 | "updated_at": "2024-01-15T01:28:34.667Z",
26 | "underway_at": null
27 | },
28 | "winner_id": 221134366,
29 | "tie": false
30 | },
31 | "relationships": {
32 | "attachments": {
33 | "data": [
34 | {
35 | "id": "846883",
36 | "type": "match_attachment"
37 | }
38 | ],
39 | "links": {
40 | "related": "https://api.challonge.com/v2.1/tournaments/13984246/matches/350944920/attachments.json",
41 | "meta": {
42 | "count": 1
43 | }
44 | }
45 | }
46 | }
47 | },
48 | "included": [
49 | {
50 | "id": "846883",
51 | "type": "match_attachment",
52 | "attributes": {
53 | "url": "https://www.google.com/maps",
54 | "description": "Google Maps",
55 | "timestamps": {
56 | "created_at": "2024-01-15T02:17:34.853Z",
57 | "updated_at": "2024-01-15T02:17:34.853Z"
58 | }
59 | }
60 | }
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/deploy/variables.tf:
--------------------------------------------------------------------------------
1 | variable "terraform_service_account" {
2 | description = "Service account email for authenticating Terraform operations"
3 | type = string
4 | default = "terraform@mitbattlecode.iam.gserviceaccount.com"
5 | }
6 |
7 | variable "gcp_project" {
8 | description = "Google Cloud project where all resources are located"
9 | type = string
10 | default = "mitbattlecode"
11 | }
12 |
13 | variable "gcp_region" {
14 | description = "Google Cloud region where all resources are located"
15 | type = string
16 | default = "us-east1"
17 | }
18 |
19 | variable "gcp_zone" {
20 | description = "Google Cloud zone where all zonal resources are located"
21 | type = string
22 | default = "us-east1-b"
23 | }
24 |
25 | variable "labels" {
26 | description = "The labels to attach to all resources"
27 | type = map(string)
28 | default = {}
29 | }
30 |
31 | variable "siarnaq_secrets_common" {
32 | description = "Additional secrets to inject into the secret manager"
33 | type = map
34 | }
35 |
36 | variable "siarnaq_secrets_production" {
37 | description = "Additional secrets to inject into the secret manager, in production"
38 | type = map
39 | }
40 |
41 | variable "siarnaq_secrets_staging" {
42 | description = "Additional secrets to inject into the secret manager, in staging"
43 | type = map
44 | }
45 |
46 | variable "saturn_secrets_production" {
47 | description = "Additional secrets to inject into the secret manager, in production"
48 | type = map
49 | }
50 |
51 | variable "saturn_secrets_staging" {
52 | description = "Additional secrets to inject into the secret manager, in staging"
53 | type = map
54 | }
55 |
--------------------------------------------------------------------------------
/frontend/src/views/DebuggingTips.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { debuggingTipsText } from "../content/ManageContent";
3 | import { useEpisodeId } from "../contexts/EpisodeContext";
4 | import OptionalSectionCardMarkdown from "../components/OptionalSectionCardMarkdown";
5 | import { DebuggingTipsPage } from "../content/ContentStruct";
6 | import NoContentFound from "./NoContentFound";
7 | import { PageContainer } from "components/elements/BattlecodeStyle";
8 | import { isNil } from "lodash";
9 | const DebuggingTips: React.FC = () => {
10 | const { episodeId } = useEpisodeId();
11 |
12 | const currentDebuggingTipsText = debuggingTipsText[episodeId];
13 | const hasContent =
14 | !isNil(currentDebuggingTipsText) &&
15 | Object.values(currentDebuggingTipsText).some((value) => value !== "");
16 |
17 | if (!hasContent) {
18 | return ;
19 | }
20 |
21 | return (
22 |
23 |
24 |
28 |
29 |
33 |
34 |
38 |
39 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default DebuggingTips;
49 |
--------------------------------------------------------------------------------
/frontend/src/views/CommonIssues.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { commonIssuesText } from "../content/ManageContent";
3 |
4 | import { useEpisodeId } from "../contexts/EpisodeContext";
5 | import OptionalSectionCardMarkdown from "../components/OptionalSectionCardMarkdown";
6 | import { CommonIssuesPage } from "../content/ContentStruct";
7 | import NoContentFound from "./NoContentFound";
8 | import { PageContainer } from "components/elements/BattlecodeStyle";
9 | import { isNil } from "lodash";
10 | const CommmonIssues: React.FC = () => {
11 | const { episodeId } = useEpisodeId();
12 |
13 | const currentCommonIssuesText = commonIssuesText[episodeId];
14 | const hasContent =
15 | !isNil(currentCommonIssuesText) &&
16 | Object.values(currentCommonIssuesText).some((value) => value !== "");
17 |
18 | if (!hasContent) {
19 | return ;
20 | }
21 |
22 | return (
23 |
24 |
25 |
29 |
30 |
34 |
35 |
39 |
40 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default CommmonIssues;
50 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/ResetToken.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | *
18 | * @export
19 | * @interface ResetToken
20 | */
21 | export interface ResetToken {
22 | /**
23 | *
24 | * @type {string}
25 | * @memberof ResetToken
26 | */
27 | token: string;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the ResetToken interface.
32 | */
33 | export function instanceOfResetToken(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "token" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function ResetTokenFromJSON(json: any): ResetToken {
41 | return ResetTokenFromJSONTyped(json, false);
42 | }
43 |
44 | export function ResetTokenFromJSONTyped(json: any, ignoreDiscriminator: boolean): ResetToken {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'token': json['token'],
51 | };
52 | }
53 |
54 | export function ResetTokenToJSON(value?: ResetToken | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | 'token': value.token,
64 | };
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/TokenRefresh.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | *
18 | * @export
19 | * @interface TokenRefresh
20 | */
21 | export interface TokenRefresh {
22 | /**
23 | *
24 | * @type {string}
25 | * @memberof TokenRefresh
26 | */
27 | readonly access: string;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the TokenRefresh interface.
32 | */
33 | export function instanceOfTokenRefresh(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "access" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function TokenRefreshFromJSON(json: any): TokenRefresh {
41 | return TokenRefreshFromJSONTyped(json, false);
42 | }
43 |
44 | export function TokenRefreshFromJSONTyped(json: any, ignoreDiscriminator: boolean): TokenRefresh {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'access': json['access'],
51 | };
52 | }
53 |
54 | export function TokenRefreshToJSON(value?: TokenRefresh | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | };
64 | }
65 |
66 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/EmailRequest.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | *
18 | * @export
19 | * @interface EmailRequest
20 | */
21 | export interface EmailRequest {
22 | /**
23 | *
24 | * @type {string}
25 | * @memberof EmailRequest
26 | */
27 | email: string;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the EmailRequest interface.
32 | */
33 | export function instanceOfEmailRequest(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "email" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function EmailRequestFromJSON(json: any): EmailRequest {
41 | return EmailRequestFromJSONTyped(json, false);
42 | }
43 |
44 | export function EmailRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): EmailRequest {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'email': json['email'],
51 | };
52 | }
53 |
54 | export function EmailRequestToJSON(value?: EmailRequest | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | 'email': value.email,
64 | };
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/frontend/src/utils/dateTime.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * estDateStr: "Saturday, October 14, 2023 at 1:37 PM",
3 | * localDateStr: "Saturday, October 14, 2023 at 2:37 PM",
4 | * localTimezone: "EDT",
5 | * localFullString: "Saturday, October 14, 2023 at 2:37 PM EDT"
6 | * shortDateStr: "10/14/2023"
7 | * localShortStr: "10/14/2023 at 2:37 PM EDT"
8 | * zeroOffsetShortStr: "10/14/2023"
9 | */
10 | interface DateTimeStrings {
11 | estDateStr: string;
12 | localDateStr: string;
13 | localTimezone: string;
14 | localFullString: string;
15 | shortDateStr: string;
16 | localShortStr: string;
17 | zeroOffsetShortStr: string;
18 | }
19 |
20 | export const dateTime = (date: Date): DateTimeStrings => {
21 | const estDateStr = date.toLocaleString("en-US", {
22 | timeZone: "EST",
23 | timeStyle: "short",
24 | dateStyle: "full",
25 | });
26 |
27 | const localDateStr = date.toLocaleString([], {
28 | timeStyle: "short",
29 | dateStyle: "full",
30 | });
31 |
32 | const localTimezone = date
33 | .toLocaleTimeString("en-us", { timeZoneName: "short" })
34 | .split(" ")[2];
35 |
36 | const shortDateStr = date.toLocaleDateString("en-US", { timeZone: "EST" });
37 |
38 | const localShortStr = date.toLocaleTimeString("en-US", {
39 | timeZone: "EST",
40 | timeZoneName: "short",
41 | month: "2-digit",
42 | day: "2-digit",
43 | year: "numeric",
44 | hour: "2-digit",
45 | minute: "2-digit",
46 | });
47 |
48 | const zeroOffsetShortStr = date.toLocaleDateString("en-US", {
49 | timeZone: "UTC",
50 | });
51 |
52 | return {
53 | estDateStr,
54 | localDateStr,
55 | localTimezone,
56 | localFullString: `${localDateStr} ${localTimezone}`,
57 | shortDateStr,
58 | localShortStr,
59 | zeroOffsetShortStr,
60 | };
61 | };
62 |
--------------------------------------------------------------------------------
/deploy/releases/main.tf:
--------------------------------------------------------------------------------
1 | resource "google_service_account" "this" {
2 | account_id = "${var.name}-agent"
3 | display_name = "Releases Agent"
4 | description = "Service account for performing releases"
5 | }
6 |
7 | resource "google_storage_bucket" "this" {
8 | name = "mitbattlecode-${var.name}"
9 |
10 | location = "US"
11 | storage_class = "STANDARD"
12 | labels = var.labels
13 | }
14 |
15 | resource "google_storage_bucket_iam_member" "this" {
16 | bucket = google_storage_bucket.this.name
17 | role = "roles/storage.objectAdmin"
18 | member = "serviceAccount:${google_service_account.this.email}"
19 | }
20 |
21 | resource "google_storage_bucket_iam_member" "reader" {
22 | bucket = google_storage_bucket.this.name
23 | role = "roles/storage.legacyBucketReader"
24 | member = "serviceAccount:${google_service_account.this.email}"
25 | }
26 |
27 | resource "google_iam_workload_identity_pool" "this" {
28 | workload_identity_pool_id = var.name
29 | description = "Workload identity for releases"
30 | }
31 |
32 | resource "google_iam_workload_identity_pool_provider" "this" {
33 | workload_identity_pool_id = google_iam_workload_identity_pool.this.workload_identity_pool_id
34 | workload_identity_pool_provider_id = "github-workflow"
35 |
36 | attribute_mapping = {
37 | "google.subject" = "assertion.sub"
38 | }
39 | attribute_condition = "assertion.repository_owner=='battlecode'"
40 | oidc {
41 | issuer_uri = "https://token.actions.githubusercontent.com/"
42 | }
43 | }
44 |
45 | resource "google_service_account_iam_member" "this" {
46 | service_account_id = google_service_account.this.name
47 | role = "roles/iam.workloadIdentityUser"
48 | member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.this.name}/*"
49 | }
50 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/AutoscrimRequest.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | *
18 | * @export
19 | * @interface AutoscrimRequest
20 | */
21 | export interface AutoscrimRequest {
22 | /**
23 | *
24 | * @type {number}
25 | * @memberof AutoscrimRequest
26 | */
27 | best_of: number;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the AutoscrimRequest interface.
32 | */
33 | export function instanceOfAutoscrimRequest(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "best_of" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function AutoscrimRequestFromJSON(json: any): AutoscrimRequest {
41 | return AutoscrimRequestFromJSONTyped(json, false);
42 | }
43 |
44 | export function AutoscrimRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): AutoscrimRequest {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'best_of': json['best_of'],
51 | };
52 | }
53 |
54 | export function AutoscrimRequestToJSON(value?: AutoscrimRequest | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | 'best_of': value.best_of,
64 | };
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/ResetTokenRequest.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | *
18 | * @export
19 | * @interface ResetTokenRequest
20 | */
21 | export interface ResetTokenRequest {
22 | /**
23 | *
24 | * @type {string}
25 | * @memberof ResetTokenRequest
26 | */
27 | token: string;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the ResetTokenRequest interface.
32 | */
33 | export function instanceOfResetTokenRequest(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "token" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function ResetTokenRequestFromJSON(json: any): ResetTokenRequest {
41 | return ResetTokenRequestFromJSONTyped(json, false);
42 | }
43 |
44 | export function ResetTokenRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): ResetTokenRequest {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'token': json['token'],
51 | };
52 | }
53 |
54 | export function ResetTokenRequestToJSON(value?: ResetTokenRequest | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | 'token': value.token,
64 | };
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/TeamReportRequest.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | *
18 | * @export
19 | * @interface TeamReportRequest
20 | */
21 | export interface TeamReportRequest {
22 | /**
23 | *
24 | * @type {Blob}
25 | * @memberof TeamReportRequest
26 | */
27 | report: Blob;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the TeamReportRequest interface.
32 | */
33 | export function instanceOfTeamReportRequest(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "report" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function TeamReportRequestFromJSON(json: any): TeamReportRequest {
41 | return TeamReportRequestFromJSONTyped(json, false);
42 | }
43 |
44 | export function TeamReportRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): TeamReportRequest {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'report': json['report'],
51 | };
52 | }
53 |
54 | export function TeamReportRequestToJSON(value?: TeamReportRequest | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | 'report': value.report,
64 | };
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/frontend/src/components/compete/chart/TeamChart.tsx:
--------------------------------------------------------------------------------
1 | import { useQueryClient } from "@tanstack/react-query";
2 | import { useEpisodeInfo, useTournamentList } from "api/episode/useEpisode";
3 | import { useEpisodeId } from "contexts/EpisodeContext";
4 | import type React from "react";
5 | import { useMemo } from "react";
6 | import {
7 | type ChartData,
8 | type PlotLine,
9 | formatRatingHistory,
10 | formatTournamentList,
11 | } from "./chartUtils";
12 | import ChartBase from "./ChartBase";
13 | import type { HistoricalRating } from "api/_autogen";
14 |
15 | interface TeamChartProps {
16 | teamRatings: HistoricalRating[];
17 | loading?: boolean;
18 | crownTop?: boolean;
19 | }
20 |
21 | const TeamChart: React.FC = ({
22 | teamRatings,
23 | loading = false,
24 | crownTop = false,
25 | }) => {
26 | const { episodeId } = useEpisodeId();
27 | const queryClient = useQueryClient();
28 |
29 | const tournamentList = useTournamentList({ episodeId }, queryClient);
30 |
31 | const ratingData: Record | undefined = useMemo(
32 | () => formatRatingHistory(teamRatings),
33 | [teamRatings],
34 | );
35 |
36 | const tournamentData: PlotLine[] | undefined = useMemo(() => {
37 | if (!tournamentList.isSuccess) return undefined;
38 | return formatTournamentList(tournamentList.data.results ?? []);
39 | }, [tournamentList]);
40 |
41 | const episode = useEpisodeInfo({ id: episodeId });
42 | const xAxisEndDate = episode.data?.game_archive;
43 |
44 | return (
45 |
54 | );
55 | };
56 |
57 | export default TeamChart;
58 |
--------------------------------------------------------------------------------
/saturn/pkg/saturn/monitor.go:
--------------------------------------------------------------------------------
1 | package saturn
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "net"
8 |
9 | "github.com/rs/zerolog/log"
10 | )
11 |
12 | type Monitor struct {
13 | l net.Listener
14 | stop chan struct{}
15 | }
16 |
17 | func NewMonitor(address string) (*Monitor, error) {
18 | log.Info().Msgf("Monitor listening for shutdown on %v.", address)
19 | l, err := net.Listen("tcp", address)
20 | if err != nil {
21 | return nil, fmt.Errorf("net.Listen: %v", err)
22 | }
23 | stop := make(chan struct{})
24 | monitor := &Monitor{l, stop}
25 | return monitor, nil
26 | }
27 |
28 | func (m *Monitor) Start() {
29 | defer func() {
30 | log.Info().Msg("Monitor shutting down.")
31 | close(m.stop)
32 | }()
33 |
34 | log.Info().Msg("Monitor waiting for incoming connection.")
35 | conn, err := m.l.Accept()
36 | if err != nil {
37 | if errors.Is(err, net.ErrClosed) {
38 | log.Info().Err(err).Msg("Ignoring monitor error as it has been closed.")
39 | } else {
40 | // TODO log traceback
41 | log.Error().Err(err).Msg("Monitor failed to accept a connection.")
42 | }
43 | return
44 | }
45 | log.Info().Msg("Monitor accepted a connection.")
46 | defer conn.Close()
47 | }
48 |
49 | func (m *Monitor) Close() error {
50 | log.Info().Msg("Closing monitor.")
51 | if err := m.l.Close(); err != nil {
52 | return fmt.Errorf("l.Close: %v", err)
53 | }
54 | return nil
55 | }
56 |
57 | func (m *Monitor) WithContext(ctx context.Context) context.Context {
58 | ctx, cancel := context.WithCancel(ctx)
59 | go func(ctx context.Context) {
60 | defer cancel()
61 | select {
62 | case <-m.stop:
63 | log.Ctx(ctx).Info().Msg("Cancelling context as monitor detected shutdown.")
64 | case <-ctx.Done():
65 | log.Ctx(ctx).Debug().Msg("Ignoring monitor as context was cancelled.")
66 | }
67 | }(ctx)
68 | return ctx
69 | }
70 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/TokenVerifyRequest.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | *
18 | * @export
19 | * @interface TokenVerifyRequest
20 | */
21 | export interface TokenVerifyRequest {
22 | /**
23 | *
24 | * @type {string}
25 | * @memberof TokenVerifyRequest
26 | */
27 | token: string;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the TokenVerifyRequest interface.
32 | */
33 | export function instanceOfTokenVerifyRequest(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "token" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function TokenVerifyRequestFromJSON(json: any): TokenVerifyRequest {
41 | return TokenVerifyRequestFromJSONTyped(json, false);
42 | }
43 |
44 | export function TokenVerifyRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): TokenVerifyRequest {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'token': json['token'],
51 | };
52 | }
53 |
54 | export function TokenVerifyRequestToJSON(value?: TokenVerifyRequest | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | 'token': value.token,
64 | };
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/saturn/pkg/saturn/queue.go:
--------------------------------------------------------------------------------
1 | package saturn
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 |
8 | "cloud.google.com/go/pubsub"
9 | "github.com/rs/zerolog/log"
10 | )
11 |
12 | type QueuedTaskHandler func(context.Context, TaskPayload) error
13 |
14 | type QueueClient interface {
15 | Subscribe(context.Context, QueuedTaskHandler) error
16 | }
17 |
18 | type GCPPubsubSubscriber struct {
19 | client *pubsub.Client
20 | subscription *pubsub.Subscription
21 | }
22 |
23 | func NewGCPPubsubSubscriber(ctx context.Context, projectID, subscriptionID string) (*GCPPubsubSubscriber, error) {
24 | client, err := pubsub.NewClient(ctx, projectID)
25 | if err != nil {
26 | return nil, fmt.Errorf("pubsub.NewClient: %v", err)
27 | }
28 |
29 | subscription := client.Subscription(subscriptionID)
30 | subscription.ReceiveSettings.Synchronous = true
31 | subscription.ReceiveSettings.MaxOutstandingMessages = 1
32 |
33 | return &GCPPubsubSubscriber{client, subscription}, nil
34 | }
35 |
36 | func (c *GCPPubsubSubscriber) Subscribe(ctx context.Context, handler QueuedTaskHandler) error {
37 | err := c.subscription.Receive(ctx, func(ctx context.Context, msg *pubsub.Message) {
38 | defer msg.Nack()
39 | log.Ctx(ctx).Info().Bytes("message", msg.Data).Msg("Starting task.")
40 | var task TaskPayload
41 | if err := json.Unmarshal(msg.Data, &task); err != nil {
42 | log.Ctx(ctx).Error().Err(err).Msg("Invalid message.")
43 | // TODO log a traceback
44 | msg.Ack() // No point in retrying this.
45 | return
46 | }
47 | if err := handler(ctx, task); err != nil {
48 | log.Ctx(ctx).Error().Err(err).Msg("Invocation failed.")
49 | // TODO log a traceback, unless context canceled.
50 | return
51 | }
52 | msg.Ack()
53 | })
54 | if err != nil {
55 | return fmt.Errorf("subscription.Receive: %v", err)
56 | }
57 | return nil
58 | }
59 |
--------------------------------------------------------------------------------
/titan/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/battlecode/galaxy/titan
2 |
3 | go 1.18
4 |
5 | require (
6 | cloud.google.com/go/storage v1.28.1
7 | github.com/baruwa-enterprise/clamd v1.0.1
8 | github.com/cloudevents/sdk-go/v2 v2.15.2
9 | github.com/rs/zerolog v1.28.0
10 | )
11 |
12 | require (
13 | cloud.google.com/go v0.110.0 // indirect
14 | cloud.google.com/go/compute v1.19.1 // indirect
15 | cloud.google.com/go/compute/metadata v0.2.3 // indirect
16 | cloud.google.com/go/iam v0.13.0 // indirect
17 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
18 | github.com/golang/protobuf v1.5.3 // indirect
19 | github.com/google/go-cmp v0.5.9 // indirect
20 | github.com/google/uuid v1.3.0 // indirect
21 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
22 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect
23 | github.com/json-iterator/go v1.1.10 // indirect
24 | github.com/mattn/go-colorable v0.1.12 // indirect
25 | github.com/mattn/go-isatty v0.0.14 // indirect
26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
27 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
28 | go.opencensus.io v0.24.0 // indirect
29 | go.uber.org/atomic v1.4.0 // indirect
30 | go.uber.org/multierr v1.1.0 // indirect
31 | go.uber.org/zap v1.10.0 // indirect
32 | golang.org/x/net v0.9.0 // indirect
33 | golang.org/x/oauth2 v0.7.0 // indirect
34 | golang.org/x/sys v0.7.0 // indirect
35 | golang.org/x/text v0.9.0 // indirect
36 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
37 | google.golang.org/api v0.114.0 // indirect
38 | google.golang.org/appengine v1.6.7 // indirect
39 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
40 | google.golang.org/grpc v1.56.3 // indirect
41 | google.golang.org/protobuf v1.33.0 // indirect
42 | )
43 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/TokenRefreshRequest.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | *
18 | * @export
19 | * @interface TokenRefreshRequest
20 | */
21 | export interface TokenRefreshRequest {
22 | /**
23 | *
24 | * @type {string}
25 | * @memberof TokenRefreshRequest
26 | */
27 | refresh: string;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the TokenRefreshRequest interface.
32 | */
33 | export function instanceOfTokenRefreshRequest(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "refresh" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function TokenRefreshRequestFromJSON(json: any): TokenRefreshRequest {
41 | return TokenRefreshRequestFromJSONTyped(json, false);
42 | }
43 |
44 | export function TokenRefreshRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): TokenRefreshRequest {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'refresh': json['refresh'],
51 | };
52 | }
53 |
54 | export function TokenRefreshRequestToJSON(value?: TokenRefreshRequest | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | 'refresh': value.refresh,
64 | };
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/frontend/src/views/QuickStart.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { quickStartText } from "../content/ManageContent";
3 | import { QuickStartPage } from "../content/ContentStruct";
4 | import { useEpisodeId } from "../contexts/EpisodeContext";
5 | import OptionalSectionCardMarkdown from "../components/OptionalSectionCardMarkdown";
6 | import NoContentFound from "./NoContentFound";
7 | import { PageContainer } from "components/elements/BattlecodeStyle";
8 | import { isNil } from "lodash";
9 | const QuickStart: React.FC = () => {
10 | const { episodeId } = useEpisodeId();
11 |
12 | const currentQuickStartText = quickStartText[episodeId];
13 | const hasContent =
14 | !isNil(currentQuickStartText) &&
15 | Object.values(currentQuickStartText).some((value) => value !== "");
16 |
17 | if (!hasContent) {
18 | return ;
19 | }
20 |
21 | return (
22 |
23 |
24 |
28 |
29 |
33 |
34 |
38 |
39 |
43 |
44 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default QuickStart;
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Galaxy
2 |
3 | 
4 |
5 | Galaxy is the framework that powers the infrastructure for MIT Battlecode.
6 | The galaxy consists of three main parts:
7 |
8 | - Siarnaq, the competitor dashboard that interfaces with our data storage. (Siarnaq, in turn, has two parts: a frontend and a backend.)
9 | - Saturn, the compute cluster that compiles competitor bots and runs matches.
10 | - Titan, the malware scanner that scans all file uploads.
11 |
12 | ## Development environment installation
13 |
14 | Please follow these steps carefully to ensure your development environment is initialized correctly.
15 |
16 | 1. Clone this repository at [battlecode/galaxy](https://github.com/battlecode/galaxy).
17 | 1. Install [Conda](https://docs.conda.io/en/latest/miniconda.html), our package-manager and build-system.
18 | Prepare your environment using `conda env create -n galaxy -f environment-dev.yml` and `conda activate galaxy`.
19 | 1. We use [Pre-commit](https://pre-commit.com/) to sanity-check the codebase before committing.
20 | It will be automatically installed by Conda.
21 | Your local git will reject commits that fail these sanity-checks.
22 | Initialize Pre-commit using `pre-commit install`.
23 |
24 | If the Conda specifications are updated upstream, you can refresh your local environment to match it by running `conda env update -n galaxy -f environment-dev.yml`.
25 |
26 | Afterwards, to run systems locally, see each of their respective directory's readme files.
27 |
28 | ## Development workflow
29 |
30 | See [docs-general/workflow.md](docs-general/workflow.md) for workflow information. **Reading this page, especially the "Coding" and "Review" sections, before starting work is highly recommended.**
31 |
32 | For specific development workflows in each module, see the README files in the respective folder.
33 |
--------------------------------------------------------------------------------
/frontend/src/api/_autogen/models/VerifyTokenRequest.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | /**
4 | *
5 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
6 | *
7 | * The version of the OpenAPI document: 0.0.0
8 | *
9 | *
10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
11 | * https://openapi-generator.tech
12 | * Do not edit the class manually.
13 | */
14 |
15 | import { exists, mapValues } from '../runtime';
16 | /**
17 | * Serializer for verifying an email verification token.
18 | * @export
19 | * @interface VerifyTokenRequest
20 | */
21 | export interface VerifyTokenRequest {
22 | /**
23 | *
24 | * @type {string}
25 | * @memberof VerifyTokenRequest
26 | */
27 | token: string;
28 | }
29 |
30 | /**
31 | * Check if a given object implements the VerifyTokenRequest interface.
32 | */
33 | export function instanceOfVerifyTokenRequest(value: object): boolean {
34 | let isInstance = true;
35 | isInstance = isInstance && "token" in value;
36 |
37 | return isInstance;
38 | }
39 |
40 | export function VerifyTokenRequestFromJSON(json: any): VerifyTokenRequest {
41 | return VerifyTokenRequestFromJSONTyped(json, false);
42 | }
43 |
44 | export function VerifyTokenRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): VerifyTokenRequest {
45 | if ((json === undefined) || (json === null)) {
46 | return json;
47 | }
48 | return {
49 |
50 | 'token': json['token'],
51 | };
52 | }
53 |
54 | export function VerifyTokenRequestToJSON(value?: VerifyTokenRequest | null): any {
55 | if (value === undefined) {
56 | return undefined;
57 | }
58 | if (value === null) {
59 | return null;
60 | }
61 | return {
62 |
63 | 'token': value.token,
64 | };
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
20 |
21 | Battlecode
22 |
23 |
24 |
25 | You need to enable JavaScript to run this app.
26 |
27 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/frontend/src/components/elements/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | // Example usage
4 | //
5 | // Hovering this makes the tooltip appear
6 | //
7 |
8 | interface TooltipProps {
9 | children?: React.ReactNode;
10 | text: string;
11 | delay?: number;
12 | location?: "top" | "bottom" | "left" | "right";
13 | }
14 |
15 | const TOOLTIP_CLASSES = {
16 | top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
17 | bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
18 | left: "right-full top-1/2 -translate-y-1/2 mr-2",
19 | right: "left-full top-1/2 -translate-y-1/2 ml-2",
20 | };
21 | const TOOLTIP_TRIANGLE_CLASSES = {
22 | top: "-bottom-1 left-1/2 -translate-x-1/2",
23 | bottom: "-top-1 left-1/2 -translate-x-1/2",
24 | left: "-right-1 top-1/2 -translate-y-1/2",
25 | right: "-left-1 top-1/2 -translate-y-1/2",
26 | };
27 |
28 | const HOVER_DELAY_MILLIS = 400;
29 |
30 | const Tooltip: React.FC = ({
31 | children,
32 | // the string to display inside the tooltip
33 | text,
34 | // delay between hover and tooltip appearance
35 | delay = HOVER_DELAY_MILLIS,
36 | // the location that the tooltip will appear (relative to the children)
37 | location = "top",
38 | }) => (
39 |
40 | {children}
41 |
52 |
53 | );
54 |
55 | export default Tooltip;
56 |
--------------------------------------------------------------------------------
/frontend/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 |
3 | interface SpinnerProps {
4 | size?: "sm" | "md" | "xs" | "lg" | "xl";
5 | variant?: "danger" | "";
6 | className?: string;
7 | }
8 |
9 | const sizeToClass = {
10 | xs: "h-4 w-4",
11 | sm: "h-6 w-6",
12 | md: "h-8 w-8",
13 | lg: "h-10 w-10",
14 | xl: "h-16 w-16",
15 | };
16 |
17 | const Spinner: React.FC = ({
18 | size = "lg",
19 | variant = "",
20 | className = "",
21 | }) => (
22 |
33 |
37 |
41 |
42 | );
43 |
44 | export default Spinner;
45 |
--------------------------------------------------------------------------------
/frontend/src/components/compete/TeamWithRating.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { useMemo } from "react";
3 | import { useEpisodeId } from "../../contexts/EpisodeContext";
4 | import { NavLink } from "react-router-dom";
5 |
6 | interface TeamWithRatingProps {
7 | teamName: string;
8 | teamId: number;
9 | includeTeamName: boolean;
10 | rating: number;
11 | ratingDelta?: number;
12 | }
13 |
14 | const TeamWithRating: React.FC = ({
15 | teamName,
16 | teamId,
17 | includeTeamName,
18 | rating,
19 | ratingDelta,
20 | }) => {
21 | const { episodeId } = useEpisodeId();
22 |
23 | const ratingComponent = useMemo(() => {
24 | if (ratingDelta !== undefined) {
25 | const deltaClass =
26 | ratingDelta > 0
27 | ? "text-xs font-semibold slashed-zero text-green-700"
28 | : ratingDelta < 0
29 | ? "text-xs font-semibold slashed-zero text-red-700"
30 | : "text-xs font-semibold slashed-zero text-gray-700";
31 |
32 | return (
33 |
34 | {" "}
35 | {includeTeamName && {"("} }
36 | {`${
37 | ratingDelta > 0 ? "+" : ratingDelta < 0 ? "" : "±"
38 | }${ratingDelta.toFixed(0)}`}
39 | {includeTeamName && {")"} }
40 |
41 | );
42 | } else {
43 | return (
44 |
45 | {" "}
46 | {includeTeamName && {`(${rating.toFixed(0)})`} }
47 | {!includeTeamName && {rating.toFixed(0)} }
48 |
49 | );
50 | }
51 | }, [rating, ratingDelta, includeTeamName]);
52 |
53 | return (
54 | <>
55 |
56 | {includeTeamName && {teamName} }
57 | {ratingComponent}
58 |
59 | >
60 | );
61 | };
62 |
63 | export default TeamWithRating;
64 |
--------------------------------------------------------------------------------