├── .gitignore ├── backend ├── siarnaq │ ├── __init__.py │ ├── api │ │ ├── teams │ │ │ ├── __init__.py │ │ │ ├── migrations │ │ │ │ ├── __init__.py │ │ │ │ ├── 0007_teamprofile_has_report.py │ │ │ │ ├── 0005_alter_teamprofile_auto_accept_ranked.py │ │ │ │ ├── 0004_alter_teamprofile_team.py │ │ │ │ ├── 0003_alter_team_name_alter_teamprofile_quote.py │ │ │ │ ├── 0008_remove_teamprofile_auto_accept_ranked_and_more.py │ │ │ │ ├── 0002_initial.py │ │ │ │ └── 0006_classrequirement.py │ │ │ ├── exceptions.py │ │ │ ├── apps.py │ │ │ ├── permissions.py │ │ │ └── urls.py │ │ ├── user │ │ │ ├── __init__.py │ │ │ ├── migrations │ │ │ │ ├── __init__.py │ │ │ │ ├── 0005_user_email_verified.py │ │ │ │ ├── 0003_alter_userprofile_country.py │ │ │ │ ├── 0004_alter_user_managers.py │ │ │ │ ├── 0002_add_superuser_and_groups.py │ │ │ │ └── 0006_emailverificationtoken.py │ │ │ ├── apps.py │ │ │ ├── forms.py │ │ │ ├── urls.py │ │ │ └── permissions.py │ │ ├── compete │ │ │ ├── __init__.py │ │ │ ├── migrations │ │ │ │ ├── __init__.py │ │ │ │ ├── 0002_initial.py │ │ │ │ ├── 0004_match_challonge_id_matchparticipant_challonge_id.py │ │ │ │ ├── 0005_rename_challonge_id_match_external_id_private_and_more.py │ │ │ │ ├── 0006_rename_external_id_matchparticipant_external_id_private_and_more.py │ │ │ │ ├── 0007_alter_match_tournament_round.py │ │ │ │ ├── 0008_alter_matchparticipant_match_and_more.py │ │ │ │ └── 0009_adminsettings.py │ │ │ ├── apps.py │ │ │ ├── filters.py │ │ │ ├── permissions.py │ │ │ └── urls.py │ │ ├── episodes │ │ │ ├── __init__.py │ │ │ ├── migrations │ │ │ │ ├── __init__.py │ │ │ │ ├── 0010_episode_is_allowed_ranked_scrimmage.py │ │ │ │ ├── 0012_eligibilitycriterion_is_private.py │ │ │ │ ├── 0008_episode_release_version_client.py │ │ │ │ ├── 0004_tournament_display_date.py │ │ │ │ ├── 0007_remove_episode_pass_requirement_out_of_and_more.py │ │ │ │ ├── 0006_tournamentround_display_order.py │ │ │ │ ├── 0009_alter_episode_language.py │ │ │ │ ├── 0011_episode_ranked_scrimmage_hourly_limit_and_more.py │ │ │ │ ├── 0002_rename_release_version_episode_release_version_public_and_more.py │ │ │ │ ├── 0003_remove_tournament_challonge_private_and_more.py │ │ │ │ └── 0005_remove_tournamentround_round-unique-tournament-challonge_and_more.py │ │ │ ├── tests.py │ │ │ ├── apps.py │ │ │ ├── urls.py │ │ │ ├── managers.py │ │ │ └── permissions.py │ │ ├── views.py │ │ └── refs.py │ ├── gcloud │ │ ├── __init__.py │ │ ├── tasks.py │ │ └── saturn.py │ ├── middleware.py │ ├── bracket │ │ ├── __init__.py │ │ └── example-data │ │ │ └── match.json │ ├── asgi.py │ ├── wsgi.py │ └── templates │ │ ├── password_reset.html │ │ └── email_verification.html ├── Dockerfile ├── mypy_django_configurations_plugin.py ├── manage.py └── environment.yml ├── frontend ├── src │ ├── api │ │ ├── _autogen │ │ │ ├── .openapi-generator │ │ │ │ └── VERSION │ │ │ ├── index.ts │ │ │ ├── apis │ │ │ │ └── index.ts │ │ │ ├── models │ │ │ │ ├── StyleEnum.ts │ │ │ │ ├── LanguageEnum.ts │ │ │ │ ├── Status526Enum.ts │ │ │ │ ├── GenderEnum.ts │ │ │ │ ├── PlayerOrderEnum.ts │ │ │ │ ├── ReleaseStatusEnum.ts │ │ │ │ ├── ScrimmageStatusEnum.ts │ │ │ │ ├── StatusBccEnum.ts │ │ │ │ ├── ScrimmageRequestAcceptRejectEnum.ts │ │ │ │ ├── Email.ts │ │ │ │ ├── ResetToken.ts │ │ │ │ ├── TokenRefresh.ts │ │ │ │ ├── EmailRequest.ts │ │ │ │ ├── AutoscrimRequest.ts │ │ │ │ ├── ResetTokenRequest.ts │ │ │ │ ├── TeamReportRequest.ts │ │ │ │ ├── TokenVerifyRequest.ts │ │ │ │ ├── TokenRefreshRequest.ts │ │ │ │ └── VerifyTokenRequest.ts │ │ │ └── .openapi-generator-ignore │ │ ├── cookies.ts │ │ ├── loaders │ │ │ ├── tournamentsLoader.ts │ │ │ ├── accountLoader.ts │ │ │ ├── rankingsLoader.ts │ │ │ ├── teamProfileLoader.ts │ │ │ ├── userProfileLoader.ts │ │ │ ├── myTeamLoader.ts │ │ │ ├── homeIfLoggedIn.ts │ │ │ ├── adminTournamentLoader.ts │ │ │ ├── homeLoader.ts │ │ │ ├── queueLoader.ts │ │ │ └── tournamentLoader.ts │ │ ├── README.md │ │ └── team │ │ │ └── teamFactories.ts │ ├── vite-env.d.ts │ ├── utils │ │ ├── constants.ts │ │ ├── searchParamHelpers.ts │ │ ├── loadTeams.ts │ │ ├── utilTypes.ts │ │ └── dateTime.ts │ ├── views │ │ ├── Logout.tsx │ │ ├── NoContentFound.tsx │ │ ├── EpisodeNotFound.tsx │ │ ├── PageNotFound.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── DebuggingTips.tsx │ │ ├── CommonIssues.tsx │ │ └── QuickStart.tsx │ ├── components │ │ ├── Loading.tsx │ │ ├── elements │ │ │ ├── FormError.tsx │ │ │ ├── FormLabel.tsx │ │ │ ├── BattlecodeStyle.tsx │ │ │ ├── Pill.tsx │ │ │ ├── Markdown.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── TextArea.tsx │ │ │ ├── Collapse.tsx │ │ │ ├── Input.tsx │ │ │ └── Tooltip.tsx │ │ ├── EligibilityIcon.tsx │ │ ├── PrivateRoute.tsx │ │ ├── OptionalSectionCardMarkdown.tsx │ │ ├── sidebar │ │ │ ├── SidebarItem.tsx │ │ │ └── SidebarSection.tsx │ │ ├── team │ │ │ └── SearchTeamsMenu.tsx │ │ ├── compete │ │ │ ├── MatchStatus.tsx │ │ │ ├── MatchRatingDelta.tsx │ │ │ ├── chart │ │ │ │ └── TeamChart.tsx │ │ │ └── TeamWithRating.tsx │ │ ├── SectionCard.tsx │ │ ├── DocumentationPage.tsx │ │ ├── ScrimmageAcceptRejectLabel.tsx │ │ ├── MatchReplayButton.tsx │ │ ├── ResponsiveIframe.tsx │ │ ├── HomeCountdown.tsx │ │ ├── tables │ │ │ └── submissions │ │ │ │ └── TourneySubTable.tsx │ │ └── Spinner.tsx │ ├── index.tsx │ ├── contexts │ │ ├── EpisodeContext.ts │ │ ├── EpisodeProvider.tsx │ │ ├── CurrentUserProvider.tsx │ │ └── CurrentUserContext.ts │ ├── index.css │ └── content │ │ ├── ContentStruct.ts │ │ └── ManageContent.ts ├── .prettierrc.json ├── public │ ├── favicon.png │ ├── robots.txt │ ├── default_profile_picture.png │ ├── battlecode-logo-horiz-white.png │ ├── battlecode-logo-vert-white.png │ ├── manifest.json │ └── number_4.svg ├── postcss.config.js ├── .env.development ├── .env.production ├── openapitools.json ├── .prettierignore ├── .gitignore ├── tailwind.config.js ├── docs │ └── local-setup.md ├── tsconfig.json ├── vite.config.ts ├── generate_types.sh ├── README.md └── index.html ├── deploy ├── saturn │ ├── shutdown-script.sh │ └── outputs.tf ├── architecture.png ├── releases │ ├── output.tf │ ├── variables.tf │ └── main.tf ├── cd │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── siarnaq │ └── outputs.tf ├── state.tf ├── network │ ├── outputs.tf │ └── variables.tf ├── galaxy │ └── outputs.tf ├── cpw │ ├── output.tf │ └── variables.tf ├── .gitignore ├── titan │ └── variables.tf └── variables.tf ├── docs-general └── system-diagram.png ├── pyproject.toml ├── titan ├── Dockerfile ├── .gitignore ├── bootstrap.sh ├── cmd │ └── titan │ │ └── main.go ├── README.md ├── pkg │ └── titan │ │ ├── file.go │ │ └── scan.go └── go.mod ├── saturn ├── .gitignore ├── pkg │ ├── run │ │ ├── protocol.go │ │ ├── recipe.go │ │ └── gcs.go │ └── saturn │ │ ├── secret.go │ │ ├── monitor.go │ │ └── queue.go └── Dockerfile ├── .github └── ISSUE_TEMPLATE │ └── bug-report-or-feature-request.md ├── LICENSE ├── environment-dev.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /backend/siarnaq/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/siarnaq/api/teams/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/siarnaq/api/user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/siarnaq/gcloud/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/siarnaq/api/compete/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/siarnaq/api/episodes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/siarnaq/api/teams/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/siarnaq/api/user/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/siarnaq/api/compete/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/siarnaq/api/episodes/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/api/_autogen/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 6.6.0 -------------------------------------------------------------------------------- /deploy/saturn/shutdown-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nc localhost 8005 4 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /deploy/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/battlecode/galaxy/HEAD/deploy/architecture.png -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/battlecode/galaxy/HEAD/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /backend/siarnaq/api/episodes/tests.py: -------------------------------------------------------------------------------- 1 | # from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /deploy/releases/output.tf: -------------------------------------------------------------------------------- 1 | output "storage_bucket_name" { 2 | value = google_storage_bucket.this.name 3 | } 4 | -------------------------------------------------------------------------------- /docs-general/system-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/battlecode/galaxy/HEAD/docs-general/system-diagram.png -------------------------------------------------------------------------------- /deploy/saturn/outputs.tf: -------------------------------------------------------------------------------- 1 | output "compute_group_name" { 2 | value = google_compute_instance_group_manager.this.name 3 | } 4 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/triple-slash-reference */ 2 | /// 3 | -------------------------------------------------------------------------------- /frontend/public/default_profile_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/battlecode/galaxy/HEAD/frontend/public/default_profile_picture.png -------------------------------------------------------------------------------- /frontend/public/battlecode-logo-horiz-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/battlecode/galaxy/HEAD/frontend/public/battlecode-logo-horiz-white.png -------------------------------------------------------------------------------- /frontend/public/battlecode-logo-vert-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/battlecode/galaxy/HEAD/frontend/public/battlecode-logo-vert-white.png -------------------------------------------------------------------------------- /frontend/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_EPISODE = "bc26"; 2 | export const FIELD_REQUIRED_ERROR_MSG = "This field is required."; 3 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_THIS_URL=http://localhost:3000 2 | VITE_BACKEND_URL=http://localhost:8000 3 | VITE_REPLAY_URL=https://play.battlecode.org 4 | -------------------------------------------------------------------------------- /frontend/src/views/Logout.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | 3 | const Logout: React.FC = () =>

logout page

; 4 | 5 | export default Logout; 6 | -------------------------------------------------------------------------------- /frontend/src/api/_autogen/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export * from './runtime'; 4 | export * from './apis'; 5 | export * from './models'; 6 | -------------------------------------------------------------------------------- /backend/siarnaq/api/teams/exceptions.py: -------------------------------------------------------------------------------- 1 | class TeamMaxSizeExceeded(Exception): 2 | """An operation tried to cause a team to exceed its maximum size.""" 3 | 4 | pass 5 | -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_THIS_URL=https://play.battlecode.org 2 | VITE_BACKEND_URL=https://api.battlecode.org 3 | VITE_REPLAY_URL=https://play.battlecode.org 4 | GENERATE_SOURCEMAP=false 5 | -------------------------------------------------------------------------------- /frontend/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "6.6.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # production 7 | /build 8 | 9 | # auto-generated types/models 10 | src/api/_autogen 11 | schema.yml 12 | 13 | /coverage 14 | 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /deploy/cd/main.tf: -------------------------------------------------------------------------------- 1 | resource "google_artifact_registry_repository" "this" { 2 | repository_id = var.name 3 | project = var.gcp_project 4 | location = var.gcp_region 5 | labels = var.labels 6 | format = "DOCKER" 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | import Spinner from "./Spinner"; 3 | 4 | const Loading: React.FC = () => ( 5 | <> 6 | Loading... 7 | 8 | 9 | ); 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /frontend/src/api/_autogen/apis/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export * from './CompeteApi'; 4 | export * from './EpisodeApi'; 5 | export * from './SpecsApi'; 6 | export * from './TeamApi'; 7 | export * from './TokenApi'; 8 | export * from './UserApi'; 9 | -------------------------------------------------------------------------------- /backend/siarnaq/api/user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "siarnaq.api.user" 7 | 8 | def ready(self): 9 | from . import signals # noqa: F401 10 | -------------------------------------------------------------------------------- /backend/siarnaq/api/teams/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TeamsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "siarnaq.api.teams" 7 | 8 | def ready(self): 9 | from . import signals # noqa: F401 10 | -------------------------------------------------------------------------------- /backend/siarnaq/api/compete/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CompeteConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "siarnaq.api.compete" 7 | 8 | def ready(self): 9 | from . import signals # noqa: F401 10 | -------------------------------------------------------------------------------- /backend/siarnaq/api/episodes/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EpisodesConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "siarnaq.api.episodes" 7 | 8 | def ready(self): 9 | from . import signals # noqa: F401 10 | -------------------------------------------------------------------------------- /frontend/src/utils/searchParamHelpers.ts: -------------------------------------------------------------------------------- 1 | export const getParamEntries = ( 2 | prev: URLSearchParams, 3 | ): Record => Object.fromEntries(prev); 4 | 5 | export const parsePageParam = ( 6 | paramName: string, 7 | searchParams: URLSearchParams, 8 | ): number => parseInt(searchParams.get(paramName) ?? "1"); 9 | -------------------------------------------------------------------------------- /backend/siarnaq/api/compete/filters.py: -------------------------------------------------------------------------------- 1 | from rest_framework import filters 2 | 3 | 4 | class IsSubmissionCreatorFilterBackend(filters.BaseFilterBackend): 5 | """Filter that only allows users to see their own submissions.""" 6 | 7 | def filter_queryset(self, request, queryset, view): 8 | return queryset.filter(team__members=request.user) 9 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById("root") as HTMLElement, 8 | ); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /backend/siarnaq/middleware.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | from django.utils import timezone 3 | 4 | 5 | class TimezoneMiddleware: 6 | def __init__(self, get_response): 7 | self.get_response = get_response 8 | 9 | def __call__(self, request): 10 | timezone.activate(pytz.timezone("US/Eastern")) 11 | return self.get_response(request) 12 | -------------------------------------------------------------------------------- /backend/siarnaq/api/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponse 3 | 4 | 5 | def ping(request): 6 | """Check that the server is alive.""" 7 | return HttpResponse("pong") 8 | 9 | 10 | def revision(request): 11 | """Return the current version of the server.""" 12 | return HttpResponse(settings.SIARNAQ_REVISION) 13 | -------------------------------------------------------------------------------- /backend/siarnaq/api/refs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Names of models 3 | """ 4 | 5 | from django.conf import settings 6 | 7 | USER_MODEL = settings.AUTH_USER_MODEL 8 | EPISODE_MODEL = "episodes.Episode" 9 | MAP_MODEL = "episodes.Map" 10 | ELIGIBILITY_CRITERION_MODEL = "episodes.EligibilityCriterion" 11 | TOURNAMENT_ROUND_MODEL = "episodes.TournamentRound" 12 | 13 | RATING_MODEL = "teams.Rating" 14 | TEAM_MODEL = "teams.Team" 15 | -------------------------------------------------------------------------------- /backend/siarnaq/api/user/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm 2 | from django.contrib.auth.forms import UsernameField 3 | 4 | from siarnaq.api.user.models import User 5 | 6 | 7 | class UserCreationForm(BaseUserCreationForm): 8 | class Meta: 9 | model = User 10 | fields = ("username", "email") 11 | field_classes = {"username": UsernameField} 12 | -------------------------------------------------------------------------------- /deploy/siarnaq/outputs.tf: -------------------------------------------------------------------------------- 1 | output "run_service_name" { 2 | value = google_cloud_run_service.this.name 3 | } 4 | 5 | output "sql_instance_ip" { 6 | value = google_sql_database_instance.this.first_ip_address 7 | } 8 | 9 | output "topic_compile_name" { 10 | value = google_pubsub_topic.this["compile"].name 11 | } 12 | 13 | output "topic_execute_name" { 14 | value = google_pubsub_topic.this["execute"].name 15 | } 16 | -------------------------------------------------------------------------------- /deploy/state.tf: -------------------------------------------------------------------------------- 1 | # resource "google_storage_bucket" "tf_bucket" { 2 | # name = "mitbattlecode-terraform-state" 3 | # 4 | # location = "US" 5 | # storage_class = "STANDARD" 6 | # force_destroy = false 7 | # 8 | # versioning { 9 | # enabled = true 10 | # } 11 | # } 12 | 13 | terraform { 14 | backend "gcs" { 15 | bucket = "mitbattlecode-terraform-state" 16 | prefix = "prod" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/elements/FormError.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | 3 | const FormError: React.FC<{ message?: string; className?: string }> = ({ 4 | message, 5 | className, 6 | }) => ( 7 | 11 | {message ?? "This field is invalid."} 12 | 13 | ); 14 | 15 | export default FormError; 16 | -------------------------------------------------------------------------------- /deploy/cd/outputs.tf: -------------------------------------------------------------------------------- 1 | output "artifact_image" { 2 | value = { 3 | for k in ["siarnaq", "titan", "saturn"] : 4 | k => "${google_artifact_registry_repository.this.location}-docker.pkg.dev/${google_artifact_registry_repository.this.project}/${google_artifact_registry_repository.this.repository_id}/${k}" 5 | } 6 | } 7 | 8 | output "artifact_registry_name" { 9 | value = google_artifact_registry_repository.this.name 10 | } 11 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | *.log 26 | -------------------------------------------------------------------------------- /backend/siarnaq/api/teams/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | from siarnaq.api.teams.models import Team 4 | 5 | 6 | class IsOnTeam(permissions.BasePermission): 7 | """Allows access to users who have a team in the current episode.""" 8 | 9 | def has_permission(self, request, view): 10 | return Team.objects.filter( 11 | episode=view.kwargs["episode_id"], members=request.user 12 | ).exists() 13 | -------------------------------------------------------------------------------- /deploy/network/outputs.tf: -------------------------------------------------------------------------------- 1 | output "dns_records" { 2 | value = concat( 3 | [ 4 | for subdomain in local.subdomains : { 5 | type = "A", 6 | subdomain = subdomain, 7 | rrdatas = [module.lb.external_ip], 8 | } 9 | ], 10 | [ 11 | { 12 | type = "A", 13 | subdomain = "db.${var.subdomain}", 14 | rrdatas = [var.sql_instance_ip], 15 | }, 16 | ], 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/EligibilityIcon.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | import type { EligibilityCriterion } from "../api/_autogen"; 3 | import Tooltip from "./elements/Tooltip"; 4 | 5 | interface EligibilityIconProps { 6 | criterion: EligibilityCriterion; 7 | } 8 | 9 | const EligibilityIcon: React.FC = ({ criterion }) => ( 10 | {criterion.icon} 11 | ); 12 | 13 | export default EligibilityIcon; 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | src_paths = ["backend"] 4 | include_trailing_comma = true 5 | ensure_newline_before_comments = true 6 | multi_line_output = 3 7 | line_length = 88 8 | 9 | [tool.mypy] 10 | python_version = "3.10" 11 | mypy_path = "backend" 12 | ignore_missing_imports = true 13 | scripts_are_modules = true 14 | plugins = "backend/mypy_django_configurations_plugin.py" 15 | 16 | [tool.django-stubs] 17 | django_settings_module = "siarnaq.settings" 18 | -------------------------------------------------------------------------------- /backend/siarnaq/api/teams/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework.routers import DefaultRouter 3 | 4 | from siarnaq.api.teams import views 5 | 6 | router = DefaultRouter() 7 | router.register(r"(?P[^\/.]+)/t", views.TeamViewSet, basename="team") 8 | router.register( 9 | r"(?P[^\/.]+)/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 |
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 |
13 | 17 | 21 | 25 |
26 | 27 | 28 | Page Not Found 29 | 30 | 31 |