diff --git a/.github/workflows/ICSServer.yml b/.github/workflows/ICSServer.yml
deleted file mode 100644
index a6c6e57..0000000
--- a/.github/workflows/ICSServer.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-# This workflow will build a golang project
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
-
-name: ICSServer
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-jobs:
-
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Go
- uses: actions/setup-go@v4
- with:
- go-version: '1.23'
-
- - name: Build
- working-directory: ICSServer/
- run: go build -v ./...
-
- # - name: Test
- # run: go test -v ./...
diff --git a/.github/workflows/Scraper.yml b/.github/workflows/Scraper.yml
deleted file mode 100644
index 6e21b8c..0000000
--- a/.github/workflows/Scraper.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-# This workflow will build a golang project
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
-
-name: Scraper
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-jobs:
-
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Go
- uses: actions/setup-go@v4
- with:
- go-version: '1.23'
-
- - name: Build
- working-directory: Scraper/
- run: go build -v ./...
-
- # - name: Test
- # run: go test -v ./...
diff --git a/.github/workflows/API.yml b/.github/workflows/Server.yml
similarity index 91%
rename from .github/workflows/API.yml
rename to .github/workflows/Server.yml
index 671d6f9..8415ca4 100644
--- a/.github/workflows/API.yml
+++ b/.github/workflows/Server.yml
@@ -1,7 +1,7 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
-name: API
+name: Server
on:
push:
@@ -22,7 +22,7 @@ jobs:
go-version: '1.23'
- name: Build
- working-directory: API/
+ working-directory: Server/
run: go build -v ./...
# - name: Test
diff --git a/.gitignore b/.gitignore
index f00d620..aa06736 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,19 +8,4 @@ node_modules/
.idea/
-API.exe
-API
-
-Scraper.exe
-Scraper
-
-postgres-data
-
-ClaretBot.exe
-ClaretBot
-
-RateMyProfScraper
-RateMyProfScraper.exe
-rmp.json
-output*.txt
-API/API.exe
+postgres-data
\ No newline at end of file
diff --git a/API/.env.example b/API/.env.example
deleted file mode 100644
index a98ad84..0000000
--- a/API/.env.example
+++ /dev/null
@@ -1,2 +0,0 @@
-DB_URL=postgresql://postgres:admin@127.0.0.1:5432/db
-PORT=8080
\ No newline at end of file
diff --git a/API/engi.go b/API/engi.go
deleted file mode 100644
index ccbf84e..0000000
--- a/API/engi.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "net/http"
-)
-
-func engi(w http.ResponseWriter, r *http.Request) {
- if r.URL.Query().Get("semester") == "" {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Semester was not provided, please add ?semester={semester} in your URL."))
- return
- }
-
- var output []EngSeats
-
- seats, err := db.Query("SELECT * FROM eng_seats WHERE semester = $1", r.URL.Query().Get("semester"))
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer seats.Close()
-
- for seats.Next() {
- var engSeat EngSeats
-
- err := seats.Scan(&engSeat.Id, &engSeat.Subject, &engSeat.Name, &engSeat.Course, &engSeat.Section, &engSeat.Registered, &engSeat.Date, &engSeat.Semester)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output = append(output, engSeat)
- }
-
- course, err := json.Marshal(output)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(course))
-}
diff --git a/API/go.mod b/API/go.mod
deleted file mode 100644
index 156d670..0000000
--- a/API/go.mod
+++ /dev/null
@@ -1,34 +0,0 @@
-module API
-
-go 1.21
-
-require (
- github.com/PuerkitoBio/goquery v1.9.1
- github.com/gocolly/colly v1.2.0
- github.com/jackc/pgx/v5 v5.5.5
- github.com/joho/godotenv v1.5.1
- github.com/lestrrat-go/strftime v1.0.6
-)
-
-require (
- github.com/andybalholm/cascadia v1.3.2 // indirect
- github.com/antchfx/htmlquery v1.3.1 // indirect
- github.com/antchfx/xmlquery v1.4.0 // indirect
- github.com/antchfx/xpath v1.3.0 // indirect
- github.com/gobwas/glob v0.2.3 // indirect
- github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/golang/protobuf v1.5.4 // indirect
- github.com/jackc/pgpassfile v1.0.0 // indirect
- github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
- github.com/jackc/puddle/v2 v2.2.1 // indirect
- github.com/kennygrant/sanitize v1.2.4 // indirect
- github.com/pkg/errors v0.9.1 // indirect
- github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
- github.com/temoto/robotstxt v1.1.2 // indirect
- golang.org/x/crypto v0.22.0 // indirect
- golang.org/x/net v0.24.0 // indirect
- golang.org/x/sync v0.7.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- google.golang.org/appengine v1.6.8 // indirect
- google.golang.org/protobuf v1.33.0 // indirect
-)
diff --git a/API/go.sum b/API/go.sum
deleted file mode 100644
index d6fb87e..0000000
--- a/API/go.sum
+++ /dev/null
@@ -1,107 +0,0 @@
-github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
-github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
-github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
-github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
-github.com/antchfx/htmlquery v1.3.1 h1:wm0LxjLMsZhRHfQKKZscDf2COyH4vDYA3wyH+qZ+Ylc=
-github.com/antchfx/htmlquery v1.3.1/go.mod h1:PTj+f1V2zksPlwNt7uVvZPsxpKNa7mlVliCRxLX6Nx8=
-github.com/antchfx/xmlquery v1.4.0 h1:xg2HkfcRK2TeTbdb0m1jxCYnvsPaGY/oeZWTGqX/0hA=
-github.com/antchfx/xmlquery v1.4.0/go.mod h1:Ax2aeaeDjfIw3CwXKDQ0GkwZ6QlxoChlIBP+mGnDFjI=
-github.com/antchfx/xpath v1.3.0 h1:nTMlzGAK3IJ0bPpME2urTuFL76o4A96iYvoKFHRXJgc=
-github.com/antchfx/xpath v1.3.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
-github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
-github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
-github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
-github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
-github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
-github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
-github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
-github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
-github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
-github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
-github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
-github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
-github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
-github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
-github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
-github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
-github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
-golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/API/handlers.go b/API/handlers.go
deleted file mode 100644
index 7cc5c83..0000000
--- a/API/handlers.go
+++ /dev/null
@@ -1,555 +0,0 @@
-package main
-
-import (
- "database/sql"
- "encoding/json"
- "net/http"
- "time"
-
- "github.com/PuerkitoBio/goquery"
- "github.com/gocolly/colly"
- "github.com/lestrrat-go/strftime"
-)
-
-//TODO probably isnt an awful idea to further split this up
-
-func all(w http.ResponseWriter, r *http.Request) {
- output := make(map[string][]any)
-
- var subjects *sql.Rows
- var err error
-
- //TODO: maybe make it cache at one point through cloudflare tasks or something?
- if r.URL.Query().Get("semester") == "" {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Semester was not provided, please add ?semester={semester} in your URL."))
- return
- }
-
- subjects, err = db.Query("SELECT DISTINCT subject, \"subjectFull\" FROM courses WHERE semester = $1", r.URL.Query().Get("semester"))
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer subjects.Close()
-
- for subjects.Next() {
- var subject Subject
-
- err := subjects.Scan(&subject.Name, &subject.FriendlyName)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output["subjects"] = append(output["subjects"], subject)
- }
-
- var courses *sql.Rows
-
- if r.URL.Query().Get("semester") != "" {
- courses, err = db.Query("SELECT crn, id, name, section, \"dateRange\", type, instructor, subject, \"subjectFull\", campus, comment, credits, semester, level, identifier FROM courses WHERE semester = $1", r.URL.Query().Get("semester"))
- } else {
- courses, err = db.Query("SELECT crn, id, name, section, \"dateRange\", type, instructor, subject, \"subjectFull\", campus, comment, credits, semester, level, identifier FROM courses")
- }
-
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer courses.Close()
-
- for courses.Next() {
- var course Course
-
- err := courses.Scan(&course.Crn, &course.Id, &course.Name, &course.Section, &course.DateRange, &course.CourseType, &course.Instructor, &course.Subject, &course.SubjectFull, &course.Campus, &course.Comment, &course.Credits, &course.Semester, &course.Level, &course.Identifier)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output["courses"] = append(output["courses"], course)
- }
-
- var times *sql.Rows
-
- if r.URL.Query().Get("semester") != "" {
- times, err = db.Query("SELECT times.crn, times.days, times.\"startTime\", times.\"endTime\", times.location, times.type, times.identifier FROM times WHERE semester = $1", r.URL.Query().Get("semester"))
- } else {
- times, err = db.Query("SELECT times.crn, times.days, times.\"startTime\", times.\"endTime\", times.location, times.type, times.identifier FROM times")
- }
-
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer times.Close()
-
- for times.Next() {
- var time Time
-
- err := times.Scan(&time.Crn, &time.Days, &time.StartTime, &time.EndTime, &time.Location, &time.Type, &time.Identifier)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output["times"] = append(output["times"], time)
- }
-
- var seatings *sql.Rows
-
- if r.URL.Query().Get("semester") != "" {
- seatings, err = db.Query("SELECT identifier, crn, available, max, waitlist, checked, semester FROM seatings WHERE semester = $1", r.URL.Query().Get("semester"))
- } else {
- seatings, err = db.Query("SELECT identifier, crn, available, max, waitlist, checked, semester FROM seatings")
- }
-
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer seatings.Close()
-
- for seatings.Next() {
- var seating Seating
-
- err := seatings.Scan(&seating.Identifier, &seating.Crn, &seating.Available, &seating.Max, &seating.Waitlist, &seating.Checked, &seating.Semester)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output["seatings"] = append(output["seatings"], seating)
- }
-
- profs, err := db.Query("SELECT DISTINCT p.name, p.rating, p.id, p.difficulty, p.rating_count, p.would_retake FROM professors p JOIN prof_and_semesters ps ON p.name = ps.name AND ps.semester = $1", r.URL.Query().Get("semester"))
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer profs.Close()
-
- for profs.Next() {
- var prof Professor
-
- err := profs.Scan(&prof.Name, &prof.Rating, &prof.Id, &prof.Difficulty, &prof.RatingCount, &prof.WouldRetake)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output["profs"] = append(output["profs"], prof)
-
- }
-
- if r.URL.Query().Get("semester") == "" {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Semester was not provided, please add ?semester={semester} in your URL."))
- return
- }
-
- examTimes, err := db.Query("SELECT DISTINCT e.crn, e.location, e.time, c.id, c.section FROM exam_times e JOIN courses c ON c.crn = e.crn AND c.semester = e.semester WHERE e.semester = $1", r.URL.Query().Get("semester"))
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer examTimes.Close()
-
- for examTimes.Next() {
- var examTime ExamTime
-
- err := examTimes.Scan(&examTime.Crn, &examTime.Location, &examTime.Time, &examTime.Id, &examTime.Section)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output["exams"] = append(output["exams"], examTime)
- }
-
- jsonString, err := json.Marshal(output)
- if err != nil {
- logger.Fatal(err)
- }
-
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(jsonString))
-}
-
-func subjects(w http.ResponseWriter, r *http.Request) {
- var output []Subject
-
- var subjects *sql.Rows
- var err error
-
- if r.URL.Query().Get("semester") != "" {
- subjects, err = db.Query("SELECT DISTINCT subject, \"subjectFull\" FROM courses WHERE semester = $1", r.URL.Query().Get("semester"))
- } else {
- subjects, err = db.Query("SELECT DISTINCT subject, \"subjectFull\" FROM courses")
- }
-
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer subjects.Close()
-
- for subjects.Next() {
- var subject Subject
-
- err := subjects.Scan(&subject.Name, &subject.FriendlyName)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output = append(output, subject)
- }
-
- jsonString, err := json.Marshal(output)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(jsonString))
-}
-
-func semesters(w http.ResponseWriter, _ *http.Request) {
- var output []Semester
-
- semesters, err := db.Query("SELECT id, name, latest, \"viewOnly\", medical, mi FROM semesters")
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer semesters.Close()
-
- for semesters.Next() {
- var semester Semester
-
- err := semesters.Scan(&semester.ID, &semester.Name, &semester.Latest, &semester.ViewOnly, &semester.Medical, &semester.MI)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output = append(output, semester)
- }
-
- jsonString, err := json.Marshal(output)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(jsonString))
-}
-
-func courses(w http.ResponseWriter, r *http.Request) {
- var output []Course
-
- if r.URL.Query().Get("semester") == "" {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Semester was not provided, please add ?semester={semester} in your URL."))
- return
- }
-
- courses, err := db.Query("SELECT crn, id, name, section, \"dateRange\", type, instructor, subject, \"subjectFull\", campus, comment, credits, semester, level, identifier FROM courses WHERE courses.semester = $1 AND courses.crn LIKE $2", r.URL.Query().Get("semester"), "%"+r.URL.Query().Get("id")+"%")
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer courses.Close()
-
- for courses.Next() {
- var course Course
-
- err := courses.Scan(&course.Crn, &course.Id, &course.Name, &course.Section, &course.DateRange, &course.CourseType, &course.Instructor, &course.Subject, &course.SubjectFull, &course.Campus, &course.Comment, &course.Credits, &course.Semester, &course.Level, &course.Identifier)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output = append(output, course)
- }
-
- course, err := json.Marshal(output)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(course))
-}
-
-func rmp(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Access-Control-Allow-Origin", "*")
-
- var output []Professor
-
- profs, err := db.Query("SELECT name, rating, id, difficulty, rating_count, would_retake FROM professors WHERE LOWER(name) LIKE LOWER($1)", "%"+r.URL.Query().Get("name")+"%")
-
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer profs.Close()
-
- for profs.Next() {
- var prof Professor
-
- err := profs.Scan(&prof.Name, &prof.Rating, &prof.Id, &prof.Difficulty, &prof.RatingCount, &prof.WouldRetake)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output = append(output, prof)
- }
-
- course, err := json.Marshal(output)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(course))
-}
-
-func times(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Access-Control-Allow-Origin", "*")
-
- var output []Time
-
- if r.URL.Query().Get("crn") == "" || r.URL.Query().Get("semester") == "" {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("CRN was not provided, please add ?crn={crn}&semester={semester} in your URL."))
- return
- }
-
- times, err := db.Query("SELECT times.crn, times.days, times.\"startTime\", times.\"endTime\", times.location, times.type, times.identifier FROM times WHERE times.crn = $1 AND times.semester = $2", r.URL.Query().Get("crn"), r.URL.Query().Get("semester"))
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer times.Close()
-
- for times.Next() {
- var time Time
-
- err := times.Scan(&time.Crn, &time.Days, &time.StartTime, &time.EndTime, &time.Location, &time.Type, &time.Identifier)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output = append(output, time)
- }
-
- course, err := json.Marshal(output)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(course))
-}
-
-func seating(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Access-Control-Allow-Origin", "*")
-
- if r.URL.Query().Get("crn") == "" || r.URL.Query().Get("semester") == "" {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("CRN was not provided, please add ?crn={crn}&semester={semester} in your URL."))
- return
- }
-
- var checked string
- var jsonString []byte
-
- var semester string
- err := db.QueryRow("SELECT courses.semester FROM courses WHERE courses.identifier = $1", r.URL.Query().Get("semester")+r.URL.Query().Get("crn")).Scan(&semester)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Course could not be found, double-check your CRN and try again." + r.URL.Query().Get("semester") + r.URL.Query().Get("crn")))
- return
- }
-
- exists := true
-
- time1, err := time.ParseInLocation("2006-01-02T15:04", checked, loc)
- if err != nil {
- time1 = time.Now().Add(time.Duration(-6) * time.Minute)
- }
-
- if !time1.After(time.Now().Add(-5 * time.Minute)) {
- c := colly.NewCollector()
-
- var cells []string
-
- if exists {
- c.OnHTML("caption", func(e *colly.HTMLElement) {
- if e.Text == "Registration Availability" {
- e.DOM.Parent().Find("td.dddefault").Each(func(i int, s *goquery.Selection) {
- cells = append(cells, s.Text())
- })
- }
- })
-
- c.OnHTML("span.errortext", func(e *colly.HTMLElement) {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Course could not be found, double-check your CRN and try again."))
- exists = false
- })
-
- c.Visit("https://selfservice.mun.ca/direct/bwckschd.p_disp_detail_sched?term_in=" + semester + "&crn_in=" + r.URL.Query().Get("crn"))
- c.Wait()
-
- if !exists {
- return
- }
-
- var output []Seating
- var seating Seating
-
- seating.Crn = r.URL.Query().Get("crn")
- seating.Identifier = r.URL.Query().Get("semester") + r.URL.Query().Get("crn")
- if len(cells) != 0 {
- seating.Available = cells[2]
- seating.Max = cells[0]
- if len(cells) >= 6 {
- seating.Waitlist = cells[4]
- } else {
- seating.Waitlist = nil
- }
- }
- checkedTime, err := strftime.Format("%Y-%m-%dT%H:%M", time.Now().UTC())
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- seating.Checked = checkedTime
-
- jsonString, err = json.Marshal(append(output, seating))
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- _, err = db.Exec(`UPDATE seatings
- SET available = $2, max = $3, waitlist = $4, checked = $5
- WHERE identifier = $1;`, seating.Identifier, seating.Available, seating.Max, seating.Waitlist, seating.Checked)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- }
- } else {
- var seating Seating
- var output []Seating
-
- err := db.QueryRow("SELECT identifier, crn, available, max, waitlist, checked, semester FROM seatings WHERE seatings.identifier = $1", r.URL.Query().Get("semester")+r.URL.Query().Get("crn")).Scan(&seating.Crn, &seating.Available, &seating.Max, &seating.Waitlist, &seating.Checked, &seating.Identifier)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- jsonString, err = json.Marshal(append(output, seating))
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(jsonString))
-}
-
-func exams(w http.ResponseWriter, r *http.Request) {
- var output []ExamTime
-
- if r.URL.Query().Get("semester") == "" {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Semester was not provided, please add ?semester={semester} in your URL."))
- return
- }
-
- examTimes, err := db.Query("SELECT DISTINCT e.crn, e.location, e.time, c.id, c.section FROM exam_times e JOIN courses c ON c.crn = e.crn AND c.semester = e.semester WHERE e.semester = $1 AND ($2 = '' OR c.crn = $2)", r.URL.Query().Get("semester"), r.URL.Query().Get("crn"))
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- defer examTimes.Close()
-
- for examTimes.Next() {
- var examTime ExamTime
-
- err := examTimes.Scan(&examTime.Crn, &examTime.Location, &examTime.Time, &examTime.Id, &examTime.Section)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- output = append(output, examTime)
- }
-
- course, err := json.Marshal(output)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
-
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(course))
-}
-
-func index(w http.ResponseWriter, _ *http.Request) {
- w.Write([]byte("
values in square brackets are optional, while braces are mandatory
/all?semester=[semester] - get all data of a semester, or if no semester is provided return every semester combined (will be >60MB of raw JSON)
/subjects?semester=[semester] - return a list of all subjects from a semester, or all semesters if none is provided
/semesters - return a list of all semesters
/courses?semester={semester}&id=[id] - return a list of all courses from a semester that contains crn, if no crn is provided it will return all courses
/times?semester={semester}&crn={crn} - return a list of all times for a certain course slot
/seating?semester={semester}&crn={crn} - scrapes muns course offering for seatings, then returns them
/rmp?name=[name] - returns all mun rate my prof ratings, or a search for a specific name
/exams?semester={semester} - returns all final exams from specified semester"))
-}
diff --git a/API/main.go b/API/main.go
deleted file mode 100644
index 21a3a5b..0000000
--- a/API/main.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package main
-
-import (
- "database/sql"
- "log"
- "net/http"
- "os"
- "time"
-
- _ "github.com/jackc/pgx/v5/stdlib"
- _ "github.com/joho/godotenv/autoload"
-)
-
-var db *sql.DB
-var logger *log.Logger
-var err error
-var loc *time.Location
-
-func main() {
- logger = log.Default()
- logger.Println("👋 Claret API")
-
- DB_URL := os.Getenv("DB_URL")
- if DB_URL == "" {
- logger.Fatal("DB_URL is not defined in environment variables")
- }
-
- PORT := os.Getenv("PORT")
- if PORT == "" {
- logger.Fatal("PORT is not defined in environment variables")
- }
-
- db, err = sql.Open("pgx", DB_URL)
- if err != nil {
- logger.Fatal(err)
- }
- defer db.Close()
-
- err = db.Ping()
- if err != nil {
- logger.Fatal(err)
- }
- logger.Println("💿 Connected to Database!")
-
- loc, err = time.LoadLocation("America/St_Johns")
- if err != nil {
- logger.Fatal(err)
- }
-
- http.HandleFunc("/", index)
- http.HandleFunc("/all", all)
- http.HandleFunc("/subjects", subjects)
- http.HandleFunc("/semesters", semesters)
- http.HandleFunc("/courses", courses)
- http.HandleFunc("/times", times)
- http.HandleFunc("/seating", seating)
- http.HandleFunc("/rmp", rmp)
- http.HandleFunc("/exams", exams)
- http.HandleFunc("/engi", engi)
-
- logger.Println("✅ API running server on port", PORT)
- http.ListenAndServe(":"+PORT, nil)
-}
diff --git a/API/types.go b/API/types.go
deleted file mode 100644
index 7a213dc..0000000
--- a/API/types.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package main
-
-type Semester struct {
- ID int `json:"id"`
- Name string `json:"name"`
- Latest bool `json:"latest"`
- ViewOnly bool `json:"viewOnly"`
- Medical bool `json:"medical"`
- MI bool `json:"mi"`
-}
-
-type Subject struct {
- Name string `json:"name"`
- FriendlyName string `json:"friendlyName"`
-}
-
-type Course struct {
- Crn string `json:"crn"`
- Id string `json:"id"`
- Name string `json:"name"`
- Section string `json:"section"`
- DateRange any `json:"dateRange"`
- CourseType any `json:"type"`
- Instructor any `json:"instructor"`
- SubjectFull string `json:"subjectFull"`
- Subject string `json:"subject"`
- Campus string `json:"campus"`
- Comment any `json:"comment"`
- Credits int `json:"credits"`
- Semester int `json:"semester"`
- Level string `json:"level"`
- Identifier string `json:"identifier"`
-}
-
-type Time struct {
- Identifier string `json:"identifier"`
- Crn string `json:"crn"`
- Days string `json:"days"`
- StartTime string `json:"startTime"`
- EndTime string `json:"endTime"`
- Location string `json:"location"`
- Type string `json:"courseType"`
- Semester int `json:"semester"`
-}
-
-type Seating struct {
- Crn string `json:"crn"`
- Available string `json:"available"`
- Max string `json:"max"`
- Waitlist any `json:"waitlist"`
- Checked string `json:"checked"`
- Identifier string `json:"identifier"`
- Semester int `json:"semester"`
-}
-
-type Professor struct {
- Name string `json:"name"`
- Rating float64 `json:"rating"`
- Id int `json:"id"`
- Difficulty float64 `json:"difficulty"`
- RatingCount int `json:"ratings"`
- WouldRetake float64 `json:"wouldRetake"`
-}
-
-type ExamTime struct {
- Id string `json:"id"`
- Section string `json:"section"`
- Crn string `json:"crn"`
- Time string `json:"time"`
- Location string `json:"location"`
-}
-
-type EngSeats struct {
- Id int `json:"id"`
- Subject string `json:"subject"`
- Name string `json:"name"`
- Course string `json:"course"`
- Section string `json:"section"`
- Registered int `json:"registered"`
- Date string `json:"date"`
- Semester int `json:"semester"`
-}
diff --git a/ICSServer/Dockerfile b/ICSServer/Dockerfile
deleted file mode 100644
index 65d73e4..0000000
--- a/ICSServer/Dockerfile
+++ /dev/null
@@ -1,12 +0,0 @@
-# Build Stage Container
-FROM golang:1.21.5 AS build-stage
-WORKDIR /app
-COPY . /app
-RUN go mod download
-RUN CGO_ENABLED=0 GOOS=linux go build -o /ics_server
-
-# Production Container
-FROM gcr.io/distroless/base-debian11:latest
-COPY --from=build-stage /ics_server /ics_server
-USER nonroot:nonroot
-ENTRYPOINT [ "/ics_server" ]
diff --git a/ICSServer/go.mod b/ICSServer/go.mod
deleted file mode 100644
index 184301b..0000000
--- a/ICSServer/go.mod
+++ /dev/null
@@ -1,14 +0,0 @@
-module main
-
-go 1.21
-
-require github.com/jackc/pgx/v5 v5.5.5
-
-require (
- github.com/jackc/pgpassfile v1.0.0 // indirect
- github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
- github.com/jackc/puddle/v2 v2.2.1 // indirect
- golang.org/x/crypto v0.22.0 // indirect
- golang.org/x/sync v0.7.0 // indirect
- golang.org/x/text v0.14.0 // indirect
-)
diff --git a/ICSServer/go.sum b/ICSServer/go.sum
deleted file mode 100644
index 6cdc9da..0000000
--- a/ICSServer/go.sum
+++ /dev/null
@@ -1,28 +0,0 @@
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
-github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
-github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
-github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
-github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
-github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/ICSServer/handlers.go b/ICSServer/handlers.go
deleted file mode 100644
index 2791d35..0000000
--- a/ICSServer/handlers.go
+++ /dev/null
@@ -1,117 +0,0 @@
-package main
-
-import (
- "bytes"
- "database/sql"
- "net/http"
- "strings"
- "time"
-)
-
-func health(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("Ok"))
-}
-
-func ics(w http.ResponseWriter, r *http.Request) {
- var rows *sql.Rows
- var err error
-
- query_crn := r.URL.Query().Get("crn")
- query_semester := r.URL.Query().Get("semester")
-
- if query_crn == "" {
- rows, err = db.Query("SELECT courses.semester, times.crn, courses.id, courses.name, courses.\"dateRange\", times.days, times.\"startTime\", times.\"endTime\", times.location FROM times JOIN courses ON times.crn = courses.crn")
- } else {
- query_crn_split := strings.Split(query_crn, ",")
- for i := range query_crn_split { query_crn_split[i] = query_semester + query_crn_split[i] }
- rows, err = db.Query(`select courses.id, courses.name, courses."dateRange", times.days, times."startTime", times."endTime", times.location from courses join times on courses.identifier = times.identifier where courses.identifier = any($1);`, query_crn_split)
- }
-
- if err != nil {
- logger.Println(err)
- w.WriteHeader(http.StatusServiceUnavailable)
- return
- }
- defer rows.Close()
-
- out_buf := new(bytes.Buffer)
-
- // start calendar
- out_buf.Write([]byte("BEGIN:VCALENDAR\r\n"))
- out_buf.Write([]byte("VERSION:2.0\r\n"))
-
- // PRODID: This value is required and should be in the form of a FPI
- // (Formal Product Identifier), as defined in ISO.9070.1991.
- // https://en.wikipedia.org/wiki/Formal_Public_Identifier
- out_buf.Write([]byte("PRODID:+//IDN evanvokey.com//Claret ICS Server//EN\r\n"))
-
- // iCal Method (values not specified in the RFC, for feeds use PUBLISH)
- out_buf.Write([]byte("METHOD:PUBLISH\r\n"))
-
- // Calendar Name & Description
- cal_name := "Courses (via Claret)" // TODO: Include semester names (ex: Fall 2023)
- out_buf.Write([]byte("X-WR-CALNAME:" + cal_name + "\r\n"))
-
- for rows.Next() {
- var (
- id string
- name string
- date_range string
- days string
- start string
- end string
- location string
- )
-
- err := rows.Scan(&id, &name, &date_range, &days, &start, &end, &location)
- if err != nil {
- logger.Println(err)
-
- }
-
- day_of_week := strings.Split(days, "")
- start_date, end_reccurence_date, err := dateRangeParse(date_range)
- if err != nil {
- // TODO: If error parsing date range, use some default semester range
- // For now, skip entry
- continue
- }
-
- // TODO: multiple days a week can be expressed in RRULE, so this loop could be removed
- for _, day := range day_of_week {
- ICAL_DATE_TIME_LOCAL_FORM := "20060102T150405"
- first_event_date := next_weekday(start_date, day_of_week_map[day])
- start_time, err := time.Parse("15:04", start)
- if err != nil {
- continue // if parsing time fails (such as TBA), skip entry
- }
-
- end_time, err := time.Parse("15:04", end)
- if err != nil {
- continue // if parsing time fails (such as TBA), skip entry
- }
- dt_start := time.Date(first_event_date.Year(), first_event_date.Month(), first_event_date.Day(), start_time.Hour(), start_time.Minute(), start_time.Second(), start_time.Nanosecond(), first_event_date.Location())
- dt_end := time.Date(first_event_date.Year(), first_event_date.Month(), first_event_date.Day(), end_time.Hour(), end_time.Minute(), end_time.Second(), end_time.Nanosecond(), first_event_date.Location())
-
- out_buf.Write([]byte("BEGIN:VEVENT\r\n"))
- out_buf.Write([]byte("UID:" + day + dt_start.Format(ICAL_DATE_TIME_LOCAL_FORM) + dt_end.Format(ICAL_DATE_TIME_LOCAL_FORM) + "@claret-cal-uid.evanvokey.com" + "\r\n"))
- out_buf.Write([]byte("SUMMARY:" + id + " - " + name + "\r\n"))
- out_buf.Write([]byte("DESCRIPTION:No Event Description" + "\r\n"))
- out_buf.Write([]byte("LOCATION:" + location + "\r\n"))
-
- out_buf.Write([]byte("DTSTART;TZID=/" + dt_start.Location().String() + ":" + dt_start.Format(ICAL_DATE_TIME_LOCAL_FORM) + "\r\n"))
- out_buf.Write([]byte("DTEND;TZID=/" + dt_end.Location().String() + ":" + dt_end.Format(ICAL_DATE_TIME_LOCAL_FORM) + "\r\n"))
-
- out_buf.Write([]byte("RRULE:FREQ=WEEKLY;UNTIL=" + end_reccurence_date.UTC().Format(ICAL_DATE_TIME_LOCAL_FORM) + "Z \r\n"))
- out_buf.Write([]byte("DTSTAMP;TZID=" + time.Now().Location().String() + ":" + time.Now().Format(ICAL_DATE_TIME_LOCAL_FORM) + "\r\n"))
-
- out_buf.Write([]byte("END:VEVENT\r\n"))
- }
- }
-
- out_buf.Write([]byte("END:VCALENDAR\r\n"))
-
- lineFoldBytes(out_buf, w)
- w.Header().Add("content-type", "text/calendar")
-
-}
diff --git a/ICSServer/ical_utilities.go b/ICSServer/ical_utilities.go
deleted file mode 100644
index 8317780..0000000
--- a/ICSServer/ical_utilities.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package main
-
-import (
- "bytes"
- "net/http"
- "strings"
- "time"
- "unicode/utf8"
-)
-
-func lineFoldBytes(in *bytes.Buffer, out http.ResponseWriter) {
-
- in_lines := strings.Split(in.String(), "\r\n")
-
- for _, line := range in_lines {
- var left, right int
- for left, right = 0, 74; right < len(line); left, right = right, right+74 {
- for !utf8.RuneStart(line[right]) {
- right--
- }
-
- if left != 0 {
- out.Write([]byte("\t"))
- }
-
- out.Write([]byte(line[left:right]))
- out.Write([]byte("\r\n"))
- }
- if left != 0 {
- out.Write([]byte("\t"))
- }
-
- out.Write([]byte(line[left:]))
- out.Write([]byte("\r\n"))
- }
-}
-
-func dateRangeParse(dr string) (start_time time.Time, end_time time.Time, err error) {
- x := strings.Split(dr, " - ")
- date_range_start := x[0]
- date_range_end := x[1]
-
- date_range_form := "Jan 02, 2006"
-
- start_time, err = time.ParseInLocation(date_range_form, date_range_start, banner_tz)
- if err != nil {
- return
- }
- end_time, err = time.ParseInLocation(date_range_form, date_range_end, banner_tz)
- return
-}
-
-func next_weekday(given_date_time time.Time, weekday time.Weekday) time.Time {
- weekday_delta := (7 + int(weekday-given_date_time.Weekday())) % 7
- result := given_date_time.AddDate(0, 0, weekday_delta)
- return result
-}
diff --git a/ICSServer/main.go b/ICSServer/main.go
deleted file mode 100644
index ede47fb..0000000
--- a/ICSServer/main.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package main
-
-import (
- "database/sql"
- "log"
- "net/http"
- "os"
- "time"
-
- _ "github.com/jackc/pgx/v5/stdlib"
-)
-
-// Global
-var db *sql.DB
-var logger *log.Logger
-var banner_tz *time.Location
-var day_of_week_map map[string]time.Weekday
-
-func main() {
- // Setup Logger
- logger = log.Default()
-
- logger.Println("👋 Claret ICS Server")
-
- day_of_week_map = map[string]time.Weekday{
- "M": time.Monday,
- "T": time.Tuesday,
- "W": time.Wednesday,
- "R": time.Thursday,
- "F": time.Friday,
- "S": time.Saturday,
- "U": time.Sunday,
- }
-
- // Load Configuration from Enviroment Variables
- DB_URL := os.Getenv("DB_URL")
- if DB_URL == "" {
- logger.Fatal("No DB_URL in Enviroment Variables")
- }
-
- PORT := os.Getenv("PORT")
- if PORT == "" {
- logger.Fatal("No PORT in Enviroment Variables")
- }
-
- BANNER_IANA_TZ := os.Getenv("BANNER_TZ")
- if BANNER_IANA_TZ == "" {
- BANNER_IANA_TZ := os.Getenv("TZ")
- if BANNER_IANA_TZ == "" {
- logger.Fatal("No TZ or BANNER_TZ in Enviroment Variables")
- }
- }
-
- var err error
- banner_tz, err = time.LoadLocation(BANNER_IANA_TZ)
- if err != nil {
- logger.Fatal(err)
- }
- logger.Println("🕒 Banner Time Zone:", banner_tz.String())
-
- // Connect to Database
- db, err = sql.Open("pgx", DB_URL) // db is global, maybe change?
- if err != nil {
- logger.Fatal(err)
- }
- defer db.Close()
-
- // Check Connection to Database
- pingErr := db.Ping()
- if pingErr != nil {
- logger.Fatal(pingErr)
- }
- logger.Println("💿 Connected to Database!")
-
- // Register HTTP Handlers
- http.HandleFunc("/health", health)
- http.HandleFunc("/feed.ics", ics)
-
- // Start HTTP Server
- logger.Println("✅ ICS Server running server on port", PORT)
- http.ListenAndServe(":"+PORT, nil)
-}
diff --git a/ScheduleBuilder/src/App.tsx b/ScheduleBuilder/src/App.tsx
index 466dd68..cc21a82 100644
--- a/ScheduleBuilder/src/App.tsx
+++ b/ScheduleBuilder/src/App.tsx
@@ -27,17 +27,17 @@ export default function App() {
fetch((process.env.NODE_ENV === "production" ? "https://api.claretformun.com" : "http://127.0.0.1:8080")+"/semesters").then(response => response.json()).then((data: Semester[]) => {
setSemesters(data);
const params = new URLSearchParams(window.location.search);
- let semester = "";
- semester = data.filter((semester: Semester) => (params.get("semester") || "") == semester.id.toString()).length >= 1 ? params.get("semester") || data.filter((semester: Semester) => semester.latest)[0].id.toString() : data.filter((semester: Semester) => semester.latest)[0].id.toString();
- params.set("semester", semester);
+ const semester: Semester = data.find((s: Semester) => s.id.toString() === params.get("semester")) ?? data.find((s: Semester) => s.latest)!;
+ setSelectedSemester(semester);
+ params.set("semester", semester.id.toString());
window.history.replaceState(null, "", `?${params}`);
- setSelectedSemester(data.filter((semester1: Semester) => semester == semester1.id.toString())[0]);
+ setSelectedSemester(data.filter((semester1: Semester) => semester.id.toString() == semester1.id.toString())[0]);
});
}, []);
React.useEffect(() => {
if (selectedSemester == null) return;
- fetch((process.env.NODE_ENV === "production" ? "https://api.claretformun.com" : "http://127.0.0.1:8080")+"/all?semester=" + selectedSemester.id).then(response => response.json()).then((data: {subjects: Subject[], courses: Course[], times: Time[], seatings: Seating[], profs: Professor[], exams: ExamTime[]}) => {
+ fetch((process.env.NODE_ENV === "production" ? "https://api.claretformun.com" : "http://127.0.0.1:8080")+"/frontend?semester=" + selectedSemester.id).then(response => response.json()).then((data: {subjects: Subject[], courses: Course[], times: Time[], seatings: Seating[], profs: Professor[], exams: ExamTime[]}) => {
setSubjects(data.subjects);
setCourses(data.courses);
setTimes(data.times);
@@ -91,8 +91,8 @@ export default function App() {
if (event !== null) setSelectedTab([event, "-1"]);
}, 500);
}}>
- {subjects.sort(function(a, b) {if (a.friendlyName < b.friendlyName) return -1; else return 1;}).map((subject, index) => {
- if (courses.filter((course: Course) => course.subject == subject.name && shouldShow(course, filters) && (searchQuery == "" || course.id.toLowerCase().includes(searchQuery.toLowerCase()) || course.subjectFull.toLowerCase().includes(searchQuery.toLowerCase()) || course.name.toLowerCase().includes(searchQuery.toLowerCase()))).length > 0) {
+ {subjects.sort(function(a, b) {if (a.name < b.name) return -1; else return 1;}).map((subject, index) => {
+ if (courses.filter((course: Course) => course.subject == subject.id && shouldShow(course, filters) && (searchQuery == "" || course.id.toLowerCase().includes(searchQuery.toLowerCase()) || course.name.toLowerCase().includes(searchQuery.toLowerCase()))).length > 0) {
return ( );
}
})}
diff --git a/ScheduleBuilder/src/api/types.tsx b/ScheduleBuilder/src/api/types.tsx
index 2f2d96c..c647d6c 100644
--- a/ScheduleBuilder/src/api/types.tsx
+++ b/ScheduleBuilder/src/api/types.tsx
@@ -1,6 +1,6 @@
export interface Subject {
+ id: string;
name: string;
- friendlyName: string;
}
export interface Semester {
@@ -36,18 +36,24 @@ export interface Time {
startTime: string;
endTime: string;
location: string;
- courseType: string;
+ type: string;
id: number;
identifier: string;
}
export interface Seating {
- identifier: string;
- crn: string;
- available: string;
- max: string;
- waitlist: string;
- checked: string;
+ semester: string;
+ crn: string;
+ seats: {
+ capacity: number;
+ actual: number;
+ remaining: number;
+ };
+ waitlist: {
+ capacity: number;
+ actual: number;
+ remaining: number;
+ };
}
export interface Professor {
@@ -60,9 +66,7 @@ export interface Professor {
}
export interface ExamTime {
- id: string;
- section: string;
- crn: string;
- time: string;
- location: string;
+ crn: string;
+ time: string;
+ location: string;
}
\ No newline at end of file
diff --git a/ScheduleBuilder/src/components/ExamModal.tsx b/ScheduleBuilder/src/components/ExamModal.tsx
index d83b21e..ceba017 100644
--- a/ScheduleBuilder/src/components/ExamModal.tsx
+++ b/ScheduleBuilder/src/components/ExamModal.tsx
@@ -37,7 +37,7 @@ export default function ExamModal(props: { isOpen: boolean; onHide: () => void }
{selectedCourses.map((course: Course) => {
- const exam: ExamTime | undefined = exams.filter((exam: ExamTime) => exam.crn == course.crn && exam.section == course.section)[0];
+ const exam: ExamTime | undefined = exams.filter((exam: ExamTime) => exam.crn == course.crn)[0];
if (exam === undefined) return null;
return (
diff --git a/ScheduleBuilder/src/components/ICalModal.tsx b/ScheduleBuilder/src/components/ICalModal.tsx
index 95840e4..2c97204 100644
--- a/ScheduleBuilder/src/components/ICalModal.tsx
+++ b/ScheduleBuilder/src/components/ICalModal.tsx
@@ -8,7 +8,7 @@ export default function ICalModal(props: { isOpen: boolean; onHide: () => void }
const [selectedSemester] = useAtom(selectedSemesterAtom);
const generateiCalURL = () => {
const crnString: string = selectedCourses.map(obj => obj.crn).join(",");
- return `https://ics.claretformun.com/feed.ics?semester=${selectedSemester?.id}&crn=${crnString}`;
+ return `https://api.claretformun.com/claret.ics?semester=${selectedSemester?.id}&crns=${crnString}`;
};
const copyURL = () => {
diff --git a/ScheduleBuilder/src/components/Schedule.tsx b/ScheduleBuilder/src/components/Schedule.tsx
index 93f24e7..0e9f283 100644
--- a/ScheduleBuilder/src/components/Schedule.tsx
+++ b/ScheduleBuilder/src/components/Schedule.tsx
@@ -4,7 +4,6 @@ import timeGridPlugin from "@fullcalendar/timegrid";
import { useAtom } from "jotai";
import {
selectedCoursesAtom,
- selectedSemesterAtom,
timesAtom,
} from "../api/atoms";
import { Course, Time } from "../api/types";
@@ -17,18 +16,18 @@ import ICalModal from "./ICalModal";
import ExamModal from "./ExamModal";
const moment = extendMoment(Moment);
-export function SectionButton1(props: {section: Course}) {
+export function SectionButton1(props: { section: Course }) {
const [modalOpen, setModalOpen] = React.useState(false);
const closeModal = () => setModalOpen(false);
return (
-
- {setModalOpen(true);}}>
- {props.section.id} - {props.section.crn} - {props.section.type} - {props.section.instructor}
-
-
-
+
+ setModalOpen(true)}>
+ {props.section.id} - {props.section.crn} - {props.section.type} - {props.section.instructor}
+
+
+
);
}
@@ -45,253 +44,88 @@ export default function Schedule() {
const [clearModalOpen, setClearModalOpen] = React.useState(false);
const closeClearModal = () => setClearModalOpen(false);
- const [selectedSemester] = useAtom(selectedSemesterAtom);
-
let credits = 0;
let overlapping = false;
- let courseTimes: { title: string; start: string; end?: string }[] = [];
+ const courseTimes: { title: string; start: string; end?: string }[] = [];
const startTimes: number[] = [];
const endTimes: number[] = [];
- let NACourses = 0;
-
- React.useEffect(() => {
- courseTimes = [];
- }, [selectedSemester]);
-
- for (const course of selectedCourses) {
- if (selectedSemester !== null) {
- const courseTimes = times.filter((time: Time) => time.crn == course.crn);
- courseTimes.forEach((time: Time) => {
- if (
- (time.startTime == "00:00" || time.endTime == "00:01") &&
- course.semester == selectedSemester?.id
- ) {
- NACourses++;
- return;
- }
- startTimes.push(moment(time.startTime, "HH:mm").hour());
- endTimes.push(moment(time.endTime, "HH:mm").hour() + 1);
- });
- }
- }
- let NAStartTime = startTimes.length == 0 ? 9 : Math.min(...startTimes);
-
- const min: number = Math.min(...startTimes);
- const max: number = Math.max(...endTimes);
+ let weekend = false;
- let otherDay = false;
function dayOfWeekName(day: string) {
- if (day == "Sunday") {
- if (!otherDay) otherDay = true;
- else return "Others";
+ if (day === "Sunday") {
+ return "Others";
}
- if (day == "Saturday" && !weekend) return "Others";
+ if (day === "Saturday" && !weekend) return "Others";
return day;
}
- let weekend = false;
-
selectedCourses.forEach((course: Course) => {
credits += course.credits;
times
- .filter((time: Time) => time.crn == course.crn)
+ .filter((time: Time) => time.crn === course.crn && time.days !== null)
.forEach((time: Time) => {
- if (time.days.includes("M"))
- courseTimes.push({
- title: `${course.id}-${course.section} - ${time.location}`,
- start:
- moment()
- .startOf("week")
- .add(1, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.startTime,
- end:
- moment()
- .startOf("week")
- .add(1, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.endTime,
- });
- if (time.days.includes("T"))
- courseTimes.push({
- title: `${course.id}-${course.section} - ${time.location}`,
- start:
- moment()
- .startOf("week")
- .add(2, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.startTime,
- end:
- moment()
- .startOf("week")
- .add(2, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.endTime,
- });
- if (time.days.includes("W"))
- courseTimes.push({
- title: `${course.id}-${course.section} - ${time.location}`,
- start:
- moment()
- .startOf("week")
- .add(3, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.startTime,
- end:
- moment()
- .startOf("week")
- .add(3, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.endTime,
- });
- if (time.days.includes("R"))
- courseTimes.push({
- title: `${course.id}-${course.section} - ${time.location}`,
- start:
- moment()
- .startOf("week")
- .add(4, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.startTime,
- end:
- moment()
- .startOf("week")
- .add(4, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.endTime,
- });
- if (time.days.includes("F"))
+ if (
+ (time.startTime === "00:00" && time.endTime === "00:01") ||
+ time.startTime === "TBA"
+ ) {
+ return;
+ }
+ startTimes.push(moment(time.startTime, "HH:mm").hour());
+ endTimes.push(moment(time.endTime, "HH:mm").hour() + 1);
+
+ const addEvent = (dayOffset: number) => {
courseTimes.push({
title: `${course.id}-${course.section} - ${time.location}`,
- start:
- moment()
- .startOf("week")
- .add(5, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.startTime,
- end:
- moment()
- .startOf("week")
- .add(5, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.endTime,
+ start: moment().startOf("week").add(dayOffset, "days").format("YYYY-MM-DD") + "T" + time.startTime,
+ end: moment().startOf("week").add(dayOffset, "days").format("YYYY-MM-DD") + "T" + time.endTime,
});
+ };
+
+ if (time.days.includes("M")) addEvent(1);
+ if (time.days.includes("T")) addEvent(2);
+ if (time.days.includes("W")) addEvent(3);
+ if (time.days.includes("R")) addEvent(4);
+ if (time.days.includes("F")) addEvent(5);
if (time.days.includes("S")) {
weekend = true;
- courseTimes.push({
- title: `${course.id}-${course.section} - ${time.location}`,
- start:
- moment()
- .startOf("week")
- .add(6, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.startTime,
- end:
- moment()
- .startOf("week")
- .add(6, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.endTime,
- });
+ addEvent(6);
}
if (time.days.includes("U")) {
weekend = true;
- courseTimes.push({
- title: `${course.id}-${course.section} - ${time.location}`,
- start:
- moment()
- .startOf("week")
- .add("days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.startTime,
- end:
- moment()
- .startOf("week")
- .add("days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- time.endTime,
- });
+ addEvent(7);
}
});
});
- //another loop because it may not bring other courses to the others tab
+ const min = startTimes.length > 0 ? Math.min(...startTimes) : 9;
+ const max = endTimes.length > 0 ? Math.max(...endTimes) : 17;
+
+ const othersStartingHour = startTimes.length > 0 ? Math.min(...startTimes) : 9;
+ let othersHourCursor = othersStartingHour;
+
selectedCourses.forEach((course: Course) => {
times
- .filter((time: Time) => time.crn == course.crn)
+ .filter((time: Time) =>
+ time.crn === course.crn &&
+ ((time.startTime === "00:00" && time.endTime === "00:01") || time.startTime === "TBA")
+ )
.forEach((time: Time) => {
- if (
- (time.startTime == "00:00" && time.endTime == "00:01") ||
- time.startTime == "TBA" ||
- time.startTime == "TBA"
- ) {
- courseTimes.push({
- title: `${course.id}-${course.section} - ${time.location}`,
- start:
- moment()
- .startOf("week")
- .add(weekend ? 7 : 6, "days")
- .toDate()
- .toISOString()
- .split("T")[0] +
- "T" +
- NAStartTime.toString().padStart(2, "0") +
- ":00",
- });
- NAStartTime++;
- }
+ courseTimes.push({
+ title: `${course.id}-${course.section} - ${time.location}`,
+ start: moment().startOf("week").add(weekend ? 7 : 6, "days").format("YYYY-MM-DD") + "T" + othersHourCursor.toString().padStart(2, "0") + ":00",
+ end: moment().startOf("week").add(weekend ? 7 : 6, "days").format("YYYY-MM-DD") + "T" + (othersHourCursor + 1).toString().padStart(2, "0") + ":00",
+ });
+ othersHourCursor++;
});
});
+ const finalHour = Math.max(max, othersHourCursor);
+
overlapCheck: for (const time of courseTimes) {
for (const time1 of courseTimes) {
if (
- moment
- .range(moment(time.start), moment(time.end))
- .overlaps(moment.range(moment(time1.start), moment(time1.end))) &&
+ moment.range(moment(time.start), moment(time.end)).overlaps(moment.range(moment(time1.start), moment(time1.end))) &&
time !== time1
) {
overlapping = true;
@@ -311,9 +145,7 @@ export default function Schedule() {
};
function removeCourse(course: Course) {
- setSelectedCourses(
- selectedCourses.filter((course1: Course) => course !== course1),
- );
+ setSelectedCourses(selectedCourses.filter((course1: Course) => course !== course1));
const params = new URLSearchParams(window.location.search);
let crns = "";
selectedCourses.forEach((course1: Course) => {
@@ -334,22 +166,14 @@ export default function Schedule() {
events={courseTimes}
height="auto"
dayHeaderFormat={{ weekday: "long" }}
- dayHeaderContent={(arg) => {
- return dayOfWeekName(arg.text);
- }}
- slotDuration={max - min > 12 ? "00:30:00" : "00:15:00"}
- slotMinTime={`${startTimes.length == 0 ? 9 : min}:00`}
- slotMaxTime={`${Math.max(endTimes.length == 0 ? 17 : max, (startTimes.length == 0 ? 9 : min) + NACourses)}:00`}
+ dayHeaderContent={(arg) => dayOfWeekName(arg.text)}
+ slotDuration={finalHour - min > 12 ? "00:30:00" : "00:15:00"}
+ slotMinTime={`${min}:00`}
+ slotMaxTime={`${finalHour}:00`}
initialView="timeGrid"
visibleRange={{
- start: moment()
- .startOf("week")
- .add(weekend ? 0 : 1, "days")
- .format("YYYY-MM-DD"),
- end: moment()
- .endOf("week")
- .add(weekend ? 2 : 1, "days")
- .format("YYYY-MM-DD"),
+ start: moment().startOf("week").add(weekend ? 0 : 1, "days").format("YYYY-MM-DD"),
+ end: moment().endOf("week").add(weekend ? 2 : 1, "days").format("YYYY-MM-DD"),
}}
eventColor="#A8415B"
/>
@@ -360,8 +184,7 @@ export default function Schedule() {
)}
{credits > 15 && (
- Warning: Without explicit permission, MUN does not allow registration
- for more than 15 credit hours.
+ Warning: Without explicit permission, MUN does not allow registration for more than 15 credit hours.
)}
@@ -375,37 +198,15 @@ export default function Schedule() {
Selected Courses:
{selectedCourses.map((course: Course) => (
-
+
- removeCourse(course)}
- >
+ removeCourse(course)}>
✖
))}
-
setClearModalOpen(true)}
- >
+ setClearModalOpen(true)}>
Clear Courses
@@ -414,40 +215,20 @@ export default function Schedule() {
Sharing/Exporting
- setCalModalOpen(true)}
- disabled={selectedCourses.length == 0}
- >
- {selectedCourses.length == 0
- ? "Subscribe to Calendar (No courses selected)"
- : "Subscribe to Calendar"}
+ setCalModalOpen(true)} disabled={selectedCourses.length === 0}>
+ {selectedCourses.length === 0 ? "Subscribe to Calendar (No courses selected)" : "Subscribe to Calendar"}
-
- {selectedCourses.length == 0
- ? "Copy Claret link to clipboard (No courses selected)"
- : isCopied
- ? "Copied!"
- : "Copy Claret link to clipboard"}
+
+ {selectedCourses.length === 0 ? "Copy Claret link to clipboard (No courses selected)" : isCopied ? "Copied!" : "Copy Claret link to clipboard"}
- setExamModalOpen(true)}
- disabled={selectedCourses.length == 0}
- >
- {selectedCourses.length == 0
- ? "View final exam schedule (no courses selected)"
- : "View final exam schedule"}
+ setExamModalOpen(true)} disabled={selectedCourses.length === 0}>
+ {selectedCourses.length === 0 ? "View final exam schedule (no courses selected)" : "View final exam schedule"}
);
-}
+}
\ No newline at end of file
diff --git a/ScheduleBuilder/src/components/SectionButton.tsx b/ScheduleBuilder/src/components/SectionButton.tsx
index 5bf6272..380d6f6 100644
--- a/ScheduleBuilder/src/components/SectionButton.tsx
+++ b/ScheduleBuilder/src/components/SectionButton.tsx
@@ -1,6 +1,6 @@
import React from "react";
-import { Course, Time } from "../api/types";
-import { timesAtom } from "../api/atoms";
+import { Course, Seating, Time } from "../api/types";
+import { seatingAtom, selectedSemesterAtom, timesAtom } from "../api/atoms";
import { useAtom } from "jotai";
import SectionModal from "./SectionModal";
import { Button } from "react-bootstrap";
@@ -8,14 +8,30 @@ import { Button } from "react-bootstrap";
export function SectionButton(props: {section: Course}) {
const [times] = useAtom(timesAtom);
const [modalOpen, setModalOpen] = React.useState(false);
- const tmp = times.filter((time: Time) => time.identifier === props.section.identifier).map((time: Time) => `${time.days}: ${time.startTime}-${time.endTime}`).join(", ");
+ const [seatings, setSeatings] = useAtom(seatingAtom);
+ const tmp = times.filter((time: Time) => time.crn === props.section.crn && time.days != null).map((time: Time) => ` - ${time.days}: ${time.startTime}-${time.endTime}`).join(", ");
+ const [semester] = useAtom(selectedSemesterAtom);
+
+ async function openModal() {
+ const existing = seatings.some(s => s.crn === props.section.crn);
+ setModalOpen(true);
+ if (!existing) {
+ fetch(`${process.env.NODE_ENV === "production" ? "https://api.claretformun.com" : "https://api.claretformun.com"}/seats?crn=${props.section.crn}&semester=${semester?.id.toString()}`)
+ .then(response => response.json())
+ .then((data: Seating) => {
+ setSeatings(prev => [...prev, data]);
+ })
+ .finally(() => {
+ });
+ }
+ }
const closeModal = () => setModalOpen(false);
return (
- {setModalOpen(true);}}>
- {props.section.dateRange !== null ? `${props.section.type} - ${tmp} ${!props.section.type.includes("Laboratory") ? "- " + props.section.instructor : ""}` : `No Information, Section: ${props.section.section}`}
+
+ {props.section.dateRange !== null ? `${props.section.type} ${tmp} ${!props.section.type.includes("Laboratory") && props.section.instructor != "" ? "- " + props.section.instructor : ""}` : `No Information, Section: ${props.section.section}`}
diff --git a/ScheduleBuilder/src/components/SectionModal.tsx b/ScheduleBuilder/src/components/SectionModal.tsx
index c555bfb..787811c 100644
--- a/ScheduleBuilder/src/components/SectionModal.tsx
+++ b/ScheduleBuilder/src/components/SectionModal.tsx
@@ -8,11 +8,7 @@ export default function SectionModal(props: {isOpen: boolean; onHide: () => void
const [times] = useAtom(timesAtom);
const [profs] = useAtom(profsAtom);
const [selectedCourses, setSelectedCourses] = useAtom(selectedCoursesAtom);
- const [seatings, setSeatings] = useAtom(seatingAtom);
-
- async function updateSeatings(crn: string, semester: number) {
- fetch(`${process.env.NODE_ENV === "production" ? "https://api.claretformun.com" : "http://127.0.0.1:8080"}/seating?crn=${crn}&semester=${semester.toString()}`).then(response => response.json()).then((data: Seating[]) => {setSeatings(seatings.map((seating: Seating) => seating.identifier == props.section.identifier ? data[0] : seating));});
- }
+ const [seatings] = useAtom(seatingAtom);
function formatDateString(input: string){
input = input.replace("M", "Monday, ").replace("T", "Tuesday, ").replace("W", "Wednesday, ").replace("R", "Thursday, ").replace("F", "Friday, ").replace("S", "Saturday, ").replace("U", "Sunday, ");
@@ -62,35 +58,45 @@ export default function SectionModal(props: {isOpen: boolean; onHide: () => void
Campus: {props.section.campus}
Type: {props.section.type !== null ? props.section.type : "Unknown"}
Date Range: {props.section.dateRange !== null ? props.section.dateRange : "Unknown"}
- Instructors:
-
- {props.section.instructor != null && props.section.instructor.split(", ").map((instructor: string) => {
- if (profs !== undefined && profs.filter((prof: Professor) => prof.name == instructor).length > 0) {
- const prof = profs.filter((prof: Professor) => prof.name == instructor)[0];
- return {instructor} {instructor !== "TBA" && (RateMyProfessors Rating: {prof.rating}/5) } ;
- }
- else
- return {instructor} {instructor !== "TBA" && (Search on RateMyProfessors) } ;
- })}
-
- Times:
-
- {times.filter((time: Time) => time.crn === props.section.crn).map((time: Time) => (
- {formatDateString(time.days)} - {moment(time.startTime, "HH:mm").format("hh:mm A").replace("Invalid date", "TBA")}-{moment(time.endTime, "HH:mm").format("hh:mm A").replace("Invalid date", "TBA")} - {time.location} {props.section.type.includes(", ") ? `(${time.courseType})` : ""}
- ))}
-
- {seatings.filter((seating: Seating) => seating.identifier == props.section.identifier).map((seating: Seating) => {
- if (props.isOpen && (moment(seating.checked).isBefore(moment().subtract(1, "hours")) || seating.checked == "Never")) {
- setTimeout(() => {updateSeatings(props.section.crn, props.section.semester);}, 200);
- }
+ {props.section.instructor != null && props.section.instructor != "" && (
+ <>
+ Instructors:
+
+ {props.section.instructor.split(", ").map((instructor: string) => {
+ if (profs !== undefined && profs.filter((prof: Professor) => prof.name == instructor).length > 0) {
+ const prof = profs.filter((prof: Professor) => prof.name == instructor)[0];
+ return {instructor} {instructor !== "TBA" && (RateMyProfessors Rating: {prof.rating}/5) } ;
+ }
+ else
+ return {instructor} {instructor !== "TBA" && (Search on RateMyProfessors) } ;
+ })}
+
+ >
+ )}
+ {times.some(time => time.days && time.crn === props.section.crn) && (
+ <>
+ Times:
+
+ {times.filter((time: Time) => time.days != null && time.crn === props.section.crn).map((time: Time) => (
+ {formatDateString(time.days)} - {moment(time.startTime, "HH:mm").format("hh:mm A").replace("Invalid date", "TBA")}-{moment(time.endTime, "HH:mm").format("hh:mm A").replace("Invalid date", "TBA")} - {time.location} {props.section.type.includes(", ") ? `(${time.type})` : ""}
+ ))}
+
+ >
+ )}
+ {seatings.filter((seating: Seating) => seating.crn == props.section.crn).map((seating: Seating) => {
return (
-
Seats Available: {seating.available}/{seating.max}
-
Waitlist: {seating.waitlist}
-
Last Checked: {moment.utc(seating.checked).fromNow().replace("Invalid date", "Never")} await updateSeatings(props.section.crn, props.section.semester)}>(Update)
+
Seats Available: {seating.seats.remaining}/{seating.seats.capacity}
+
Waitlist Available: {seating.waitlist.remaining}/{seating.waitlist.remaining}
);
})}
+ {seatings.filter((seating: Seating) => seating.crn == props.section.crn).length == 0 &&
+ <>
+ Seats Available: Loading...
+ Waitlist Available: Loading...
+ >
+ }
props.onHide()}>Close
diff --git a/ScheduleBuilder/src/components/SubjectAccordion.tsx b/ScheduleBuilder/src/components/SubjectAccordion.tsx
index 8198d43..6901264 100644
--- a/ScheduleBuilder/src/components/SubjectAccordion.tsx
+++ b/ScheduleBuilder/src/components/SubjectAccordion.tsx
@@ -8,7 +8,7 @@ import { shouldShow } from "../api/functions";
export default function SubjectAccordion(props: {subject: Subject, index: string,}) {
const [filters] = useAtom(filterAtom);
const [courses] = useAtom(coursesAtom);
- const subjectCourses = courses.filter((course: Course) => course.subject === props.subject.name);
+ const subjectCourses = courses.filter((course: Course) => course.subject === props.subject.id);
const uniqueCourses: [string, string, string][] = [];
const [selectedTab] = useAtom(selectedTabAtom);
const sortingOrder: {[name: string]: number} = {"Lecture": 1, "Laboratory": 2, "World Wide Web": 3};
@@ -21,13 +21,13 @@ export default function SubjectAccordion(props: {subject: Subject, index: string
return (
- {props.subject.friendlyName}
+ {props.subject.name}
{selectedTab.includes(props.index) &&
{uniqueCourses.sort(function(x, y) {return x>y ? 1: -1;}).map((course: [id: string, name: string, subject: string]) => {
- if (courses.filter((course1: Course) => course1.id == course[0] && shouldShow(course1, filters) && (searchQuery == "" || course[0].toLowerCase().includes(searchQuery.toLowerCase()) || course[2].toLowerCase().includes(searchQuery.toLowerCase()) || course[1].toLowerCase().includes(searchQuery.toLowerCase()))).length > 0) return (
+ if (courses.filter((course1: Course) => course1.id == course[0] && shouldShow(course1, filters) && (searchQuery == "" || course[0].toLowerCase().includes(searchQuery.toLowerCase()) || (course[2] || "").toLowerCase().includes(searchQuery.toLowerCase()) || course[1].toLowerCase().includes(searchQuery.toLowerCase()))).length > 0) return (
{course[0]} - {course[1]}
diff --git a/Scraper/.env.example b/Scraper/.env.example
deleted file mode 100644
index bbb67dc..0000000
--- a/Scraper/.env.example
+++ /dev/null
@@ -1,2 +0,0 @@
-DB_URL=postgresql://postgres:admin@127.0.0.1:5432/db
-WEBHOOK_URL=https://discord.com/api/webhooks/id/token #optional if you want scraped data to be put into a discord channel
\ No newline at end of file
diff --git a/Scraper/Dockerfile b/Scraper/Dockerfile
deleted file mode 100644
index 66fa4c9..0000000
--- a/Scraper/Dockerfile
+++ /dev/null
@@ -1,8 +0,0 @@
-FROM golang:alpine AS build-stage
-WORKDIR /app
-COPY . /app
-RUN CGO_ENABLED=0 GOOS=linux go build -o /scraper
-FROM gcr.io/distroless/base-debian11:latest
-COPY --from=build-stage /scraper /scraper
-USER nonroot:nonroot
-ENTRYPOINT [ "/scraper" ]
\ No newline at end of file
diff --git a/Scraper/engi.go b/Scraper/engi.go
deleted file mode 100644
index 93ab433..0000000
--- a/Scraper/engi.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package main
-
-import (
- "fmt"
- "strconv"
- "time"
-
- "github.com/PuerkitoBio/goquery"
- "github.com/gocolly/colly"
-)
-
-type EngSeats struct {
- Id int `gorm:"autoIncrement"`
- Subject string `gorm:"not null"`
- Name string `gorm:"not null"`
- Course string `gorm:"not null"`
- Section string `gorm:"not null"`
- Registered int `gorm:"not null"`
- Date string `gorm:"not null"`
- Semester int `gorm:"not null"`
-}
-
-func engSeating(semester int, crn string, subject string, id string, section string, name string) {
- if db.Where("course = ? AND section = ? AND date = ?", id, section, fmt.Sprintf("%d-%d-%d", time.Now().Day(), time.Now().Month(), time.Now().Year())).Find(&EngSeats{}).RowsAffected > 0 {
- return
- }
-
- c := colly.NewCollector()
-
- var cells []string
-
- c.OnHTML("caption", func(e *colly.HTMLElement) {
- if e.Text == "Registration Availability" {
- e.DOM.Parent().Find("td.dddefault").Each(func(i int, s *goquery.Selection) {
- cells = append(cells, s.Text())
- })
- }
- })
-
- c.Visit("https://selfservice.mun.ca/direct/bwckschd.p_disp_detail_sched?term_in=" + strconv.Itoa(semester) + "&crn_in=" + crn)
- c.Wait()
-
- if len(cells) <= 0 {
- return
- }
-
- max, err := strconv.Atoi(cells[0])
- if err != nil {
- logger.Fatal(err)
- }
- available, err := strconv.Atoi(cells[2])
- if err != nil {
- logger.Fatal(err)
- }
-
- db.Save(&EngSeats{
- Subject: subject,
- Name: name,
- Course: id,
- Section: section,
- Registered: max - available,
- Date: fmt.Sprintf("%d-%d-%d", time.Now().Day(), time.Now().Month(), time.Now().Year()),
- Semester: semester,
- })
-}
diff --git a/Scraper/go.mod b/Scraper/go.mod
deleted file mode 100644
index 3012db7..0000000
--- a/Scraper/go.mod
+++ /dev/null
@@ -1,38 +0,0 @@
-module Scraper
-
-go 1.21
-
-require (
- github.com/PuerkitoBio/goquery v1.9.1
- github.com/gocolly/colly v1.2.0
- github.com/joho/godotenv v1.5.1
- github.com/robfig/cron v1.2.0
- gorm.io/driver/postgres v1.5.7
- gorm.io/gorm v1.25.9
-)
-
-require (
- github.com/andybalholm/cascadia v1.3.2 // indirect
- github.com/antchfx/htmlquery v1.3.1 // indirect
- github.com/antchfx/xmlquery v1.4.0 // indirect
- github.com/antchfx/xpath v1.3.0 // indirect
- github.com/gobwas/glob v0.2.3 // indirect
- github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/golang/protobuf v1.5.4 // indirect
- github.com/google/go-cmp v0.5.8 // indirect
- github.com/jackc/pgpassfile v1.0.0 // indirect
- github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
- github.com/jackc/pgx/v5 v5.5.5 // indirect
- github.com/jackc/puddle/v2 v2.2.1 // indirect
- github.com/jinzhu/inflection v1.0.0 // indirect
- github.com/jinzhu/now v1.1.5 // indirect
- github.com/kennygrant/sanitize v1.2.4 // indirect
- github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
- github.com/temoto/robotstxt v1.1.2 // indirect
- golang.org/x/crypto v0.22.0 // indirect
- golang.org/x/net v0.24.0 // indirect
- golang.org/x/sync v0.7.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- google.golang.org/appengine v1.6.8 // indirect
- google.golang.org/protobuf v1.33.0 // indirect
-)
diff --git a/Scraper/go.sum b/Scraper/go.sum
deleted file mode 100644
index 0007f27..0000000
--- a/Scraper/go.sum
+++ /dev/null
@@ -1,111 +0,0 @@
-github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
-github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
-github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
-github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
-github.com/antchfx/htmlquery v1.3.1 h1:wm0LxjLMsZhRHfQKKZscDf2COyH4vDYA3wyH+qZ+Ylc=
-github.com/antchfx/htmlquery v1.3.1/go.mod h1:PTj+f1V2zksPlwNt7uVvZPsxpKNa7mlVliCRxLX6Nx8=
-github.com/antchfx/xmlquery v1.4.0 h1:xg2HkfcRK2TeTbdb0m1jxCYnvsPaGY/oeZWTGqX/0hA=
-github.com/antchfx/xmlquery v1.4.0/go.mod h1:Ax2aeaeDjfIw3CwXKDQ0GkwZ6QlxoChlIBP+mGnDFjI=
-github.com/antchfx/xpath v1.3.0 h1:nTMlzGAK3IJ0bPpME2urTuFL76o4A96iYvoKFHRXJgc=
-github.com/antchfx/xpath v1.3.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
-github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
-github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
-github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
-github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
-github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
-github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
-github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
-github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
-github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
-github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
-github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
-github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
-github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
-github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
-github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
-github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
-github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
-golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
-gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
-gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
-gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
diff --git a/Scraper/main.go b/Scraper/main.go
deleted file mode 100644
index bbee967..0000000
--- a/Scraper/main.go
+++ /dev/null
@@ -1,419 +0,0 @@
-package main
-
-import (
- "bytes"
- "fmt"
- "log"
- "net/http"
- "os"
- "slices"
- "strconv"
- "strings"
- "time"
-
- "github.com/PuerkitoBio/goquery"
- "github.com/gocolly/colly"
- _ "github.com/joho/godotenv/autoload"
- "github.com/robfig/cron"
- "gorm.io/driver/postgres"
- "gorm.io/gorm"
-)
-
-//TODO further cleaning up isnt a bad idea
-
-var db *gorm.DB
-var logger *log.Logger
-var replaceMap map[string]string
-var coursesScraped int
-
-func first(s string, _ bool) string { return s }
-func Ternary[T any](b bool, t, f T) T {
- if b {
- return t
- }
- return f
-}
-func parseTime(t string) string {
- startTime, err := time.Parse("3:04 pm", t)
- if err != nil {
- logger.Fatal(err)
- }
- return startTime.Format("15:04")
-}
-
-func getSemesters() []Semester {
- c := colly.NewCollector()
-
- var semesters []Semester
- foundLatest := false
-
- c.OnHTML("select[name=p_term]", func(e *colly.HTMLElement) {
- e.DOM.Find("option").Each(func(i int, s *goquery.Selection) {
- if s.Text() != "None" {
- output, err := strconv.Atoi(first(s.Attr("value")))
- if err != nil {
- logger.Fatal(err)
- }
- semesters = append(semesters, Semester{output, strings.Replace(s.Text(), " (View only)", "", 1), !foundLatest && !strings.Contains(s.Text(), "M"), strings.Contains(s.Text(), "(View only)"), strings.Contains(s.Text(), "Medicine"), !strings.Contains(s.Text(), "Medicine") && strings.Contains(s.Text(), "M"), false})
- if !foundLatest && !strings.Contains(s.Text(), "M") {
- foundLatest = true
- }
- }
- })
- })
-
- c.Visit("https://selfservice.mun.ca/direct/bwckschd.p_disp_dyn_sched")
- c.Wait()
-
- return semesters
-}
-
-func processSemester(semester int) []Subject {
- var subjects []Subject
-
- c := colly.NewCollector()
-
- c.OnHTML("select[name=sel_subj]", func(e *colly.HTMLElement) {
- e.DOM.Find("option").EachWithBreak(func(i int, s *goquery.Selection) bool {
- if s.Text() != "All" {
- subjects = append(subjects, Subject{Name: first(s.Attr("value")), FriendlyName: s.Text()})
- }
- return true
- })
- })
-
- params := []byte("p_calling_proc=bwckschd.p_disp_dyn_sched&p_term=" + strconv.Itoa(semester))
- err := c.PostRaw("https://selfservice.mun.ca/direct/bwckgens.p_proc_term_date", params)
- if err != nil {
- logger.Fatal(err)
- }
- c.Wait()
-
- return subjects
-}
-
-func processCourse(title []string, body []string, semester int, subject string, viewOnly bool) {
- var campus string
- var credits int
- var comment *string
- var timeStartLine int
- var commentEndLine int
- var level string
-
- for i, line := range body {
- if strings.Contains(line, "Campus") {
- campus = line[:len(line)-7]
- }
- if strings.Contains(line, "Credits") {
- var err error
- credits, err = strconv.Atoi(string(strings.TrimSpace(line)[0]))
- if err != nil {
- logger.Fatal(err)
- }
- }
- if line == "Scheduled Meeting Times" {
- timeStartLine = i
- }
- if strings.HasPrefix(line, "Associated") {
- commentEndLine = i
- }
- if strings.HasPrefix(line, "Levels:") {
- level = strings.TrimSpace(line[8:])
- }
- }
-
- if commentEndLine != 0 {
- jointStrings := strings.Replace(strings.Join(body[0:commentEndLine], ""), "\n", "", -1)
- comment = &jointStrings
- }
-
- var types []string
-
- var instructor string
-
- if timeStartLine != 0 {
- times := body[timeStartLine+8:]
-
- for i := 0; i <= len(times)/7-1; i++ {
- if !slices.Contains(types, times[7*i+5]) {
- types = append(types, times[7*i+5])
- }
- for _, name := range strings.Split(strings.ReplaceAll(times[7*i+6], "(P)", ""), ", ") {
- if !strings.Contains(instructor, name) {
- instructor += name + ", "
- }
- }
- }
-
- instructor = strings.TrimSuffix(instructor, ", ")
- } else {
- instructor = strings.ReplaceAll(body[len(body)-1], "(P)", "")
- }
-
- var typesStr = strings.Join(types, ", ")
-
- if strings.Contains(subject, "Engineer") && !viewOnly {
- engSeating(semester, title[len(title)-3], subject, title[len(title)-2], title[len(title)-1], strings.Join(title[:len(title)-3], " - "))
- }
-
- if timeStartLine != 0 {
- db.Save(&Course{
- Name: strings.Join(title[:len(title)-3], " - "),
- Id: title[len(title)-2],
- CRN: title[len(title)-3],
- Section: title[len(title)-1],
- DateRange: &body[len(body)-3],
- Type: &typesStr,
- Instructor: &instructor,
- Subject: strings.Split(title[len(title)-2], " ")[0],
- SubjectFull: subject,
- Campus: campus,
- Comment: comment,
- Credits: credits,
- SemesterID: semester,
- Level: level,
- Identifier: strconv.Itoa(semester) + title[len(title)-3],
- })
- for _, prof := range strings.Split(instructor, ", ") {
- if instructor != "TBA" && db.Where("name = ? AND semester = ?", prof, semester).Find(&ProfAndSemester{}).RowsAffected == 0 {
- db.Create(&ProfAndSemester{Name: prof, SemesterID: semester})
- }
- }
- } else {
- db.Save(&Course{
- Name: strings.Join(title[:len(title)-3], " - "),
- Id: title[len(title)-2],
- CRN: title[len(title)-3],
- Section: title[len(title)-1],
- Subject: strings.Split(title[len(title)-2], " ")[0],
- SubjectFull: subject,
- Campus: campus,
- Comment: comment,
- Credits: credits,
- SemesterID: semester,
- Level: level,
- Identifier: strconv.Itoa(semester) + title[len(title)-3],
- })
- }
-
- coursesScraped++
-
- if timeStartLine != 0 {
- times := body[timeStartLine+8:]
-
- for i := 0; i <= len(times)/7-1; i++ {
- location := times[3+(i*7)]
- for from, to := range replaceMap {
- location = strings.Replace(location, from, to, 1)
- }
-
- if times[1+(i*7)] == "TBA" {
- db.Save(&CourseTime{
- CRN: title[len(title)-3],
- StartTime: "TBA",
- EndTime: "TBA",
- Days: times[2+(i*7)],
- Location: location,
- SemesterID: semester,
- CourseIdentifier: strconv.Itoa(semester) + title[len(title)-3],
- })
- } else {
- db.Save(&CourseTime{
- CRN: title[len(title)-3],
- StartTime: parseTime(strings.Split(times[1+(i*7)], " - ")[0]),
- EndTime: Ternary(times[1+(i*7)] == "TBA", "TBA", parseTime(strings.Split(times[1+(i*7)], " - ")[1])),
- Days: times[2+(i*7)],
- Location: location,
- Type: times[5+(i*7)],
- SemesterID: semester,
- CourseIdentifier: strconv.Itoa(semester) + title[len(title)-3],
- })
- }
- }
- }
-
- db.Save(&Seating{Identifier: strconv.Itoa(semester) + title[len(title)-3], Crn: title[len(title)-3], Available: 0, Max: 0, Waitlist: 0, Checked: "Never", SemesterID: semester})
-}
-
-func processSubject(subject Subject, semester int, course string, viewOnly bool) {
- c := colly.NewCollector()
-
- var courses []*goquery.Selection
-
- c.OnHTML("th.ddtitle", func(e *colly.HTMLElement) {
- courses = append(courses, e.DOM)
- })
-
- params := []byte("term_in=" + strconv.Itoa(semester) + "&sel_subj=dummy&sel_day=dummy&sel_schd=dummy&sel_insm=dummy&sel_camp=dummy&sel_levl=dummy&sel_sess=dummy&sel_instr=dummy&sel_ptrm=dummy&sel_attr=dummy&sel_subj=" + subject.Name + "&sel_crse=" + course + "&sel_title=&sel_schd=%25&sel_insm=%25&sel_from_cred=&sel_to_cred=&sel_camp=%25&sel_levl=%25&sel_ptrm=%25&sel_instr=%25&sel_sess=%25&sel_attr=%25&begin_hh=0&begin_mi=0&begin_ap=a&end_hh=0&end_mi=0&end_ap=a")
- err := c.PostRaw("https://selfservice.mun.ca/direct/bwckschd.p_get_crse_unsec", params)
- if err != nil {
- logger.Fatal(err)
- }
- c.Wait()
-
- if len(courses) == 101 && course == "" {
- for i := 1; i <= 9; i++ {
- processSubject(subject, semester, strconv.Itoa(i), viewOnly)
- }
- return
- }
-
- for _, course := range courses {
- tmp := strings.Split(course.Parent().Next().Text(), "\n")
- var body []string
- for _, line := range tmp {
- if len(line) > 0 {
- body = append(body, line)
- }
- }
- processCourse(strings.Split(course.Text(), " - "), body, semester, subject.FriendlyName, viewOnly)
- }
-}
-
-func scrape() {
- startTime := time.Now()
- coursesScraped = 0
-
- logger.Println("⭐ Scraping Started!")
-
- for _, semester := range getSemesters() {
- //if course has already been scraped and its view only (is not going to be changed), dont scrape it
- //this does make the first scrape SIGNIFICANTLY longer
- semester1 := Semester{}
- if db.Where("id = ?", semester.ID).Find(&Semester{}).RowsAffected > 0 {
- db.Where("id = ?", semester.ID).First(&Semester{}).Scan(&semester1)
- }
-
- if (!semester1.ViewOnly || !semester.ViewOnly) || db.Where("id = ?", semester.ID).Find(&Semester{}).RowsAffected == 0 || !semester1.Scraped {
- logger.Println("📝 Processing Semester: " + semester.Name + " (" + strconv.Itoa(semester.ID) + ")")
- //NOTE: this will warn about slow sql, this can safely be ignored
- db.Where("id = ?", semester.ID).Delete(&Semester{})
- db.Save(&semester)
- exams(semester.ID)
- for _, subject := range processSemester(semester.ID) {
- logger.Println(" 📝 Processing " + subject.FriendlyName + " (" + subject.Name + ")")
- processSubject(subject, semester.ID, "", semester.ViewOnly)
- }
- semester.Scraped = true
- db.Save(&semester)
- exams(semester.ID)
- }
- }
-
- scrapingTime := time.Since(startTime)
-
- if os.Getenv("WEBHOOK_URL") != "" {
- logger.Println("🔔 Sending message to Discord")
- params := fmt.Sprintf(`{"username":"Claret Scraper","embeds":[{"author":{"name":"Claret Scraper Report","url":"https://claretformun.com"},"timestamp":"%s","color":65280,"fields":[{"name":"Scraping Time","value":"%s"},{"name":"Courses Scraped","value":"%d"}]}]}`, time.Now().Format(time.RFC3339), fmt.Sprintf("%02d:%02d", int(scrapingTime.Minutes()), int(scrapingTime.Seconds())%60), coursesScraped)
- r, err := http.NewRequest("POST", os.Getenv("WEBHOOK_URL"), bytes.NewBuffer([]byte(params)))
- if err != nil {
- panic(err)
- }
- r.Header.Add("Content-Type", "application/json")
- client := &http.Client{}
- res, err := client.Do(r)
- if err != nil {
- panic(err)
- }
- defer res.Body.Close()
- }
-
- logger.Println("✅ Scrape Complete in " + fmt.Sprintf("%02d:%02d", int(scrapingTime.Minutes()), int(scrapingTime.Seconds())%60) + "!")
- logger.Printf("Courses scraped: %d", coursesScraped)
-
- rmp()
-
- //makes adding rmp stuff easier
- rows, err := db.Raw(`
- SELECT DISTINCT unnest(string_to_array(instructor, ', ')), semester from courses
- WHERE instructor IS NOT NULL AND instructor != 'TBA'
- EXCEPT
- SELECT name, semester from prof_and_semesters;
- `).Rows()
- if err != nil {
- panic(err)
- }
- for rows.Next() {
- var instructor string
- var semester int
- rows.Scan(&instructor, &semester)
- db.Create(&ProfAndSemester{Name: instructor, SemesterID: semester})
- }
-}
-
-func main() {
- logger = log.Default()
- logger.Println("👋 Claret Scraper")
-
- DB_URL := os.Getenv("DB_URL")
- if DB_URL == "" {
- logger.Fatal("DB_URL is not defined in environment variables")
- }
-
- replaceMap = map[string]string{
- "Arts and Administration Bldg": "A",
- "Henrietta Harvey Bldg": "HH",
- "Business Administration Bldg": "BN",
- "INCO Innovation Centre": "IIC",
- "Biotechnology Bldg": "BT",
- "St. John's College": "J",
- "Chemistry - Physics Bldg": "C",
- "Core Science Facility": "CSF",
- "M. O. Morgan Bldg": "MU",
- "Computing Services": "CS",
- "Physical Education Bldg": "PE",
- "G. A. Hickman Bldg": "ED",
- "Queen's College": "QC",
- "Queen Elizabeth II Library": "L",
- "S. J. Carew Bldg.": "EN",
- "Science Bldg": "S",
- "Alexander Murray Bldg": "ER",
- "Health Sciences Centre": "H",
- "Coughlan College": "CL",
- "Marine Institute": "MI",
- "Center for Nursing Studies": "N",
- "Arts and Science (SWGC)": "AS",
- "Fine Arts (SWGC)": "FA",
- "Forest Centre": "FC",
- "Library/Computing (SWGC)": "LC",
- "Western Memorial Hospital": "WMH",
- "\u00A0": "N/A",
- }
-
- var err error
-
- db, err = gorm.Open(postgres.Open(DB_URL), &gorm.Config{})
- if err != nil {
- logger.Fatal(err)
- }
- logger.Println("💿 Connected to Database!")
-
- // migrate schemas
- db.AutoMigrate(&Semester{})
- db.AutoMigrate(&Course{})
- db.AutoMigrate(&CourseTime{})
- db.AutoMigrate(&Seating{})
- db.AutoMigrate(&Professor{})
- db.AutoMigrate(&ProfAndSemester{})
- db.AutoMigrate(&ExamTime{})
- db.AutoMigrate(&EngSeats{})
- logger.Println("💾 Migrated Schemas!")
-
- if slices.Contains(os.Args, "--rmp") {
- rmp()
- os.Exit(0)
- }
- if slices.Contains(os.Args, "--exam") {
- for _, semester := range getSemesters() {
- exams(semester.ID)
- }
- os.Exit(0)
- }
- scrape()
-
- c := cron.New()
- c.AddFunc("0 30 4 * * 1", func() { scrape() })
- c.Start()
-
- select {}
-}
diff --git a/Scraper/types.go b/Scraper/types.go
deleted file mode 100644
index 564658b..0000000
--- a/Scraper/types.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package main
-
-type Semester struct {
- ID int `gorm:"not null"`
- Name string `gorm:"not null"`
- Latest bool `gorm:"not null"`
- ViewOnly bool `gorm:"column:viewOnly;not null"`
- Medical bool `gorm:"not null"`
- MI bool `gorm:"not null"`
- Scraped bool `gorm:"not null"`
-}
-
-type Subject struct {
- Name string `gorm:"primaryKey;not null"`
- FriendlyName string `gorm:"not null"`
-}
-
-type Course struct {
- CRN string `gorm:"not null"`
- Id string `gorm:"not null"`
- Name string `gorm:"not null"`
- Section string `gorm:"not null"`
- DateRange *string `gorm:"column:dateRange"`
- Type *string
- Instructor *string
- Subject string `gorm:"column:subject;not null"`
- SubjectFull string `gorm:"column:subjectFull;not null"`
- Campus string `gorm:"not null"`
- Comment *string
- Credits int `gorm:"not null"`
- SemesterID int `gorm:"column:semester;not null"`
- Semester Semester `gorm:"constraint:OnDelete:CASCADE;"`
- Level string `gorm:"not null"`
- Identifier string `gorm:"primaryKey"`
-}
-
-type CourseTime struct {
- ID int `gorm:"primaryKey;autoIncrement"`
- CRN string `gorm:"not null"`
- Days string `gorm:"not null"`
- StartTime string `gorm:"column:startTime;not null"`
- EndTime string `gorm:"column:endTime;not null"`
- Location string `gorm:"not null"`
- Type string `gorm:"not null"`
- SemesterID int `gorm:"column:semester;not null"`
- Semester Semester `gorm:"constraint:OnDelete:CASCADE;"`
- CourseIdentifier string `gorm:"column:identifier"`
- Course Course `gorm:"constraint:OnDelete:CASCADE;"`
-}
-
-type ProfAndSemester struct {
- ID int `gorm:"primaryKey;autoIncrement"`
- Name string `gorm:"not null"`
- SemesterID int `gorm:"column:semester;not null"`
- Semester Semester `gorm:"constraint:OnDelete:CASCADE;"`
-}
-
-func (CourseTime) TableName() string {
- return "times"
-}
-
-type Seating struct {
- Identifier string `gorm:"primaryKey"`
- Crn string `gorm:"not null"`
- Available int `gorm:"not null"`
- Max int `gorm:"not null"`
- Waitlist int
- Checked string `gorm:"not null"`
- SemesterID int `gorm:"column:semester;not null"`
- Semester Semester `gorm:"constraint:OnDelete:CASCADE;"`
-}
diff --git a/Server/.env.example b/Server/.env.example
new file mode 100644
index 0000000..795885a
--- /dev/null
+++ b/Server/.env.example
@@ -0,0 +1,18 @@
+# required
+POSTGRES_URL=postgresql://postgres:admin@127.0.0.1:5432/db
+REDIS_URL=127.0.0.1:6379
+REDIS_USERNAME=
+REDIS_PASSWORD=
+REDIS_CACHE_DB=0
+
+SCRAPER_ENABLED=false
+SCRAPER_ALL=false
+SCRAPER_WEBHOOK_URL=https://discord.com/api/webhooks/id/token
+
+API_ENABLED=true
+GIN_MODE=debug
+API_RATE_LIMIT_ENABLED=true
+API_RATE_LIMIT="120-M"
+
+#port for gin to run on
+PORT=8080
\ No newline at end of file
diff --git a/API/Dockerfile b/Server/Dockerfile
similarity index 52%
rename from API/Dockerfile
rename to Server/Dockerfile
index b06caa8..b5cf1f3 100644
--- a/API/Dockerfile
+++ b/Server/Dockerfile
@@ -1,8 +1,8 @@
FROM golang:alpine AS build-stage
WORKDIR /app
COPY . /app
-RUN CGO_ENABLED=0 GOOS=linux go build -o /api
+RUN CGO_ENABLED=0 GOOS=linux go build -o /server
FROM gcr.io/distroless/base-debian11:latest
-COPY --from=build-stage /api /api
+COPY --from=build-stage /server /server
USER nonroot:nonroot
-ENTRYPOINT [ "/api" ]
\ No newline at end of file
+ENTRYPOINT [ "/server" ]
\ No newline at end of file
diff --git a/Server/cmd/api/main.go b/Server/cmd/api/main.go
new file mode 100644
index 0000000..f813548
--- /dev/null
+++ b/Server/cmd/api/main.go
@@ -0,0 +1,141 @@
+package api
+
+import (
+ "net/http"
+
+ _ "github.com/evaan/Claret/docs"
+ "github.com/evaan/Claret/internal/api"
+ "github.com/gin-contrib/cors"
+ "github.com/gin-gonic/gin"
+ "github.com/redis/go-redis/v9"
+ swaggerFiles "github.com/swaggo/files"
+ ginSwagger "github.com/swaggo/gin-swagger"
+ "gorm.io/gorm"
+)
+
+// @title Claret API
+// @version 1.0
+// @description The API for Claret, a tool for Memorial University students.
+// @host api.claretformun.com
+// @BasePath /
+func StartAPI(db *gorm.DB, rdb *redis.Client, enableLimit bool, limit string) {
+ r := gin.Default()
+
+ // TODO: maybe analytics for active users? redis?
+
+ if enableLimit {
+ r.Use(api.RateLimiterMiddleware(limit))
+ }
+
+ r.Use(cors.Default())
+
+ r.SetTrustedProxies([]string{
+ "127.0.0.1",
+
+ // cloudflare ipv4
+ "173.245.48.0/20",
+ "103.21.244.0/22",
+ "103.22.200.0/22",
+ "103.31.4.0/22",
+ "141.101.64.0/18",
+ "108.162.192.0/18",
+ "190.93.240.0/20",
+ "188.114.96.0/20",
+ "197.234.240.0/22",
+ "198.41.128.0/17",
+ "162.158.0.0/15",
+ "104.16.0.0/13",
+ "104.24.0.0/14",
+ "172.64.0.0/13",
+ "131.0.72.0/22",
+ })
+
+ r.GET("/", func(c *gin.Context) {
+ c.Redirect(http.StatusFound, "/swagger/index.html")
+ })
+ r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
+
+ r.GET("/health", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "alive": true,
+ })
+ })
+
+ r.GET("/semesters", func(c *gin.Context) {
+ api.SemestersHandler(c, db)
+ })
+
+ r.GET("/courses", func(c *gin.Context) {
+ api.CoursesHandler(c, db)
+ })
+
+ r.GET("/courses/:semester", func(c *gin.Context) {
+ api.CoursesHandler(c, db)
+ })
+
+ r.GET("/courses/:semester/:id", func(c *gin.Context) {
+ api.CoursesHandler(c, db)
+ })
+
+ r.GET("/times", func(c *gin.Context) {
+ api.TimesHandler(c, db)
+ })
+
+ r.GET("/times/:semester/:crn", func(c *gin.Context) {
+ api.TimesHandler(c, db)
+ })
+
+ r.GET("/instructors", func(c *gin.Context) {
+ api.InstructorsHandler(c, db)
+ })
+
+ r.GET("/instructors/:semester/:crn", func(c *gin.Context) {
+ api.InstructorsHandler(c, db)
+ })
+
+ r.GET("/rmp", func(c *gin.Context) {
+ api.RmpHandler(c, db)
+ })
+
+ r.GET("/rmp/:name", func(c *gin.Context) {
+ api.RmpHandler(c, db)
+ })
+
+ r.GET("/subjects", func(c *gin.Context) {
+ api.SubjectsHandler(c, db)
+ })
+
+ r.GET("/subjects/:semester", func(c *gin.Context) {
+ api.SubjectsHandler(c, db)
+ })
+
+ r.GET("/exams/:semester", func(c *gin.Context) {
+ api.ExamsHander(c, db)
+ })
+
+ r.GET("/exams", func(c *gin.Context) {
+ api.ExamsHander(c, db)
+ })
+
+ r.GET("/frontend", func(c *gin.Context) {
+ api.FrontendHandler(c, db, rdb)
+ })
+
+ r.GET("/frontend/:semester", func(c *gin.Context) {
+ api.FrontendHandler(c, db, rdb)
+ })
+
+ r.GET("/seats/:semester/:crn", func(c *gin.Context) {
+ api.SeatsHandler(c, db, rdb)
+ })
+
+ r.GET("/seats", func(c *gin.Context) {
+ api.SeatsHandler(c, db, rdb)
+ })
+
+ r.GET("/claret.ics", func(c *gin.Context) {
+ api.ICalHandler(c, db)
+ })
+
+ r.Run()
+}
diff --git a/Server/cmd/scrapers/main.go b/Server/cmd/scrapers/main.go
new file mode 100644
index 0000000..f5bfc0c
--- /dev/null
+++ b/Server/cmd/scrapers/main.go
@@ -0,0 +1,105 @@
+package scrapers
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/evaan/Claret/internal/scrapers"
+ "github.com/evaan/Claret/internal/util"
+ "github.com/redis/go-redis/v9"
+ "github.com/robfig/cron/v3"
+ "gorm.io/gorm"
+)
+
+func Scrape(db *gorm.DB, webhookUrl string, scrapeAll bool, rdb *redis.Client) {
+ startTime := time.Now()
+ coursesScraped := 0
+ logger := log.Default()
+ ctx := context.Background()
+
+ logger.Println("⭐ Scraping Started!")
+
+ var profs []string
+
+ for _, semester := range scrapers.GetSemesters(logger) {
+ if semester.ViewOnly && !scrapeAll {
+ continue
+ }
+ if db.Where("id = ?", semester.ID).Find(&util.Semester{}).RowsAffected > 0 {
+ if semester.ViewOnly {
+ continue
+ }
+ db.Delete(&semester)
+ }
+ logger.Println("📝 Processing Semester: " + semester.Name + " (" + strconv.Itoa(semester.ID) + ")")
+ db.Create(&semester)
+ for _, subject := range scrapers.GetSubjects(logger, semester.ID) {
+ db.FirstOrCreate(&subject)
+ logger.Println(" 📝 Processing " + subject.Name + " (" + subject.ID + ")")
+ courses, courseTimes, professors, courseInstructors := scrapers.GetCourses(logger, semester.ID, subject.ID)
+ for _, course := range courses {
+ db.Create(&course)
+ // db.Create(&util.CourseSeating{CourseKey: course.Key, Capacity: 0, Available: 0, Scraped: "Never"})
+ coursesScraped++
+ }
+ for _, time := range courseTimes {
+ db.Create(&time)
+ }
+ for _, professor := range professors {
+ db.FirstOrCreate(&professor)
+ profs = append(profs, professor.Name)
+ }
+ for _, instructor := range courseInstructors {
+ db.Create(&instructor)
+ }
+ }
+ logger.Println(" 📝 Processing Exams")
+ for _, exam := range scrapers.GetExams(semester.ID) {
+ db.Save(&exam)
+ }
+ rdb.Del(ctx, "frontend:"+strconv.Itoa(semester.ID))
+ }
+
+ logger.Println("⭐ RMP Scraping Started!")
+
+ profRatings := scrapers.RMP(logger, profs)
+ for _, rating := range profRatings {
+ db.Save(&rating)
+ }
+
+ scrapingTime := time.Since(startTime)
+
+ logger.Println("✅ Scrape Complete in " + fmt.Sprintf("%02d:%02d", int(scrapingTime.Minutes()), int(scrapingTime.Seconds())%60) + "!")
+ logger.Printf("🚀 Courses scraped: %d", coursesScraped)
+
+ if webhookUrl != "" {
+ logger.Println("🔔 Sending message to Discord")
+ params := fmt.Sprintf(`{"username":"Claret Scraper","embeds":[{"author":{"name":"Claret Scraper Report","url":"https://claretformun.com"},"timestamp":"%s","color":65280,"fields":[{"name":"Scraping Time","value":"%s"},{"name":"Courses Scraped","value":"%d"}]}]}`, time.Now().Format(time.RFC3339), fmt.Sprintf("%02d:%02d", int(scrapingTime.Minutes()), int(scrapingTime.Seconds())%60), coursesScraped)
+ r, err := http.NewRequest("POST", os.Getenv("SCRAPER_WEBHOOK_URL"), bytes.NewBuffer([]byte(params)))
+ if err != nil {
+ panic(err)
+ }
+ r.Header.Add("Content-Type", "application/json")
+ client := &http.Client{}
+ res, err := client.Do(r)
+ if err != nil {
+ panic(err)
+ }
+ defer res.Body.Close()
+ }
+}
+
+func Entrypoint(db *gorm.DB, webhookURL string, scrapeAll bool, rdb *redis.Client) {
+ c := cron.New()
+
+ Scrape(db, webhookURL, scrapeAll, rdb)
+
+ c.AddFunc("30 4 * * 1", func() { Scrape(db, webhookURL, scrapeAll, rdb) })
+ c.Start()
+}
diff --git a/Server/docs/docs.go b/Server/docs/docs.go
new file mode 100644
index 0000000..4c47d23
--- /dev/null
+++ b/Server/docs/docs.go
@@ -0,0 +1,805 @@
+// Package docs Code generated by swaggo/swag. DO NOT EDIT
+package docs
+
+import "github.com/swaggo/swag"
+
+const docTemplate = `{
+ "schemes": {{ marshal .Schemes }},
+ "swagger": "2.0",
+ "info": {
+ "description": "{{escape .Description}}",
+ "title": "{{.Title}}",
+ "contact": {},
+ "version": "{{.Version}}"
+ },
+ "host": "{{.Host}}",
+ "basePath": "{{.BasePath}}",
+ "paths": {
+ "/claret.ics": {
+ "get": {
+ "description": "Returns an iCal file containing all schedule items for selected courses",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "text/calendar"
+ ],
+ "summary": "Get iCal Calendar",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Numbers seperated by commas (i.e. 40983,40984)",
+ "name": "crn",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/courses": {
+ "get": {
+ "description": "Returns all courses for a specified semester",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get all courses",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course ID (i.e. ECE 3400)",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Number (i.e. 40983)",
+ "name": "crn",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/exams": {
+ "get": {
+ "description": "Returns all exams for a specified semester",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get all exams",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ExamTimeAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/frontend": {
+ "get": {
+ "description": "Returns all data (courses, prof ratings, subjects, seats, times, and exams) for a specified semester (or latest for no semester)",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Frontend",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/util.FrontendAPIResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/instructors": {
+ "get": {
+ "description": "Returns all instructors and instructor ratings from a course",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Course Instructors",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Number (i.e. 40983)",
+ "name": "crn",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ProfessorRatingAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/rmp": {
+ "get": {
+ "description": "Returns all instructor ratings",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Course Instructors",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Instructor Name",
+ "name": "name",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ProfessorRatingAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/seats": {
+ "get": {
+ "description": "Returns seats from a specified course, may take a few seconds if seats not cached",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Course Seats",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Number (i.e. 40983)",
+ "name": "crn",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/util.CourseSeating"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/semester": {
+ "get": {
+ "description": "Returns all semesters",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Semesters",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.Semester"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/subjects": {
+ "get": {
+ "description": "Returns all subjects",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Subjects",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.Subject"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/times": {
+ "get": {
+ "description": "Returns all times from course",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Course Times",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Number (i.e. 40983)",
+ "name": "crn",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseTimeAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "util.CourseAPI": {
+ "type": "object",
+ "properties": {
+ "campus": {
+ "type": "string"
+ },
+ "comment": {
+ "type": "string"
+ },
+ "credits": {
+ "type": "integer"
+ },
+ "crn": {
+ "type": "string"
+ },
+ "dateRange": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "instructor": {
+ "type": "string"
+ },
+ "level": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "registrationDates": {
+ "type": "string"
+ },
+ "section": {
+ "type": "string"
+ },
+ "subject": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "util.CourseFrontendAPI": {
+ "type": "object",
+ "properties": {
+ "campus": {
+ "type": "string"
+ },
+ "comment": {
+ "type": "string"
+ },
+ "credits": {
+ "type": "integer"
+ },
+ "crn": {
+ "type": "string"
+ },
+ "dateRange": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "instructor": {
+ "type": "string"
+ },
+ "level": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "registrationDates": {
+ "type": "string"
+ },
+ "section": {
+ "type": "string"
+ },
+ "subject": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "util.CourseSeating": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "seats": {
+ "$ref": "#/definitions/util.SeatingInfo"
+ },
+ "semester": {
+ "type": "string"
+ },
+ "waitlist": {
+ "$ref": "#/definitions/util.SeatingInfo"
+ }
+ }
+ },
+ "util.CourseSeatingResponse": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "seats": {
+ "$ref": "#/definitions/util.SeatingInfo"
+ },
+ "waitlist": {
+ "$ref": "#/definitions/util.SeatingInfo"
+ }
+ }
+ },
+ "util.CourseTimeAPI": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "dateRange": {
+ "type": "string"
+ },
+ "days": {
+ "type": "string"
+ },
+ "endTime": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "professorNames": {
+ "type": "string"
+ },
+ "startTime": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "util.CourseTimeFrontendAPI": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "dateRange": {
+ "type": "string"
+ },
+ "days": {
+ "type": "string"
+ },
+ "endTime": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "startTime": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "util.ErrorResponse": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "string"
+ }
+ }
+ },
+ "util.ExamTimeAPI": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "semester": {
+ "type": "integer"
+ },
+ "time": {
+ "type": "string"
+ }
+ }
+ },
+ "util.FrontendAPIResponse": {
+ "type": "object",
+ "properties": {
+ "courses": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseFrontendAPI"
+ }
+ },
+ "exams": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ExamTimeAPI"
+ }
+ },
+ "profs": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ProfessorRatingAPI"
+ }
+ },
+ "seatings": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseSeatingResponse"
+ }
+ },
+ "subjects": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.Subject"
+ }
+ },
+ "times": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseTimeFrontendAPI"
+ }
+ }
+ }
+ },
+ "util.ProfessorRatingAPI": {
+ "type": "object",
+ "properties": {
+ "difficulty": {
+ "type": "number"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "rating": {
+ "type": "number"
+ },
+ "ratings": {
+ "type": "integer"
+ },
+ "wouldRetake": {
+ "type": "number"
+ }
+ }
+ },
+ "util.SeatingInfo": {
+ "type": "object",
+ "properties": {
+ "actual": {
+ "type": "integer"
+ },
+ "capacity": {
+ "type": "integer"
+ },
+ "remaining": {
+ "type": "integer"
+ }
+ }
+ },
+ "util.Semester": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "latest": {
+ "type": "boolean"
+ },
+ "medicine": {
+ "type": "boolean"
+ },
+ "mi": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "viewOnly": {
+ "type": "boolean"
+ }
+ }
+ },
+ "util.Subject": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ }
+ }
+}`
+
+// SwaggerInfo holds exported Swagger Info so clients can modify it
+var SwaggerInfo = &swag.Spec{
+ Version: "",
+ Host: "",
+ BasePath: "",
+ Schemes: []string{},
+ Title: "",
+ Description: "",
+ InfoInstanceName: "swagger",
+ SwaggerTemplate: docTemplate,
+ LeftDelim: "{{",
+ RightDelim: "}}",
+}
+
+func init() {
+ swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
+}
diff --git a/Server/docs/swagger.json b/Server/docs/swagger.json
new file mode 100644
index 0000000..9b19015
--- /dev/null
+++ b/Server/docs/swagger.json
@@ -0,0 +1,776 @@
+{
+ "swagger": "2.0",
+ "info": {
+ "contact": {}
+ },
+ "paths": {
+ "/claret.ics": {
+ "get": {
+ "description": "Returns an iCal file containing all schedule items for selected courses",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "text/calendar"
+ ],
+ "summary": "Get iCal Calendar",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Numbers seperated by commas (i.e. 40983,40984)",
+ "name": "crn",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/courses": {
+ "get": {
+ "description": "Returns all courses for a specified semester",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get all courses",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course ID (i.e. ECE 3400)",
+ "name": "id",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Number (i.e. 40983)",
+ "name": "crn",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/exams": {
+ "get": {
+ "description": "Returns all exams for a specified semester",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get all exams",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ExamTimeAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/frontend": {
+ "get": {
+ "description": "Returns all data (courses, prof ratings, subjects, seats, times, and exams) for a specified semester (or latest for no semester)",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Frontend",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/util.FrontendAPIResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/instructors": {
+ "get": {
+ "description": "Returns all instructors and instructor ratings from a course",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Course Instructors",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Number (i.e. 40983)",
+ "name": "crn",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ProfessorRatingAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/rmp": {
+ "get": {
+ "description": "Returns all instructor ratings",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Course Instructors",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Instructor Name",
+ "name": "name",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ProfessorRatingAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/seats": {
+ "get": {
+ "description": "Returns seats from a specified course, may take a few seconds if seats not cached",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Course Seats",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Number (i.e. 40983)",
+ "name": "crn",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/util.CourseSeating"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/semester": {
+ "get": {
+ "description": "Returns all semesters",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Semesters",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.Semester"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/subjects": {
+ "get": {
+ "description": "Returns all subjects",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Subjects",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.Subject"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/times": {
+ "get": {
+ "description": "Returns all times from course",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "summary": "Get Course Times",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Semester ID (i.e. 202401)",
+ "name": "semester",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Course Registration Number (i.e. 40983)",
+ "name": "crn",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseTimeAPI"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/util.ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "util.CourseAPI": {
+ "type": "object",
+ "properties": {
+ "campus": {
+ "type": "string"
+ },
+ "comment": {
+ "type": "string"
+ },
+ "credits": {
+ "type": "integer"
+ },
+ "crn": {
+ "type": "string"
+ },
+ "dateRange": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "instructor": {
+ "type": "string"
+ },
+ "level": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "registrationDates": {
+ "type": "string"
+ },
+ "section": {
+ "type": "string"
+ },
+ "subject": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "util.CourseFrontendAPI": {
+ "type": "object",
+ "properties": {
+ "campus": {
+ "type": "string"
+ },
+ "comment": {
+ "type": "string"
+ },
+ "credits": {
+ "type": "integer"
+ },
+ "crn": {
+ "type": "string"
+ },
+ "dateRange": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "instructor": {
+ "type": "string"
+ },
+ "level": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "registrationDates": {
+ "type": "string"
+ },
+ "section": {
+ "type": "string"
+ },
+ "subject": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "util.CourseSeating": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "seats": {
+ "$ref": "#/definitions/util.SeatingInfo"
+ },
+ "semester": {
+ "type": "string"
+ },
+ "waitlist": {
+ "$ref": "#/definitions/util.SeatingInfo"
+ }
+ }
+ },
+ "util.CourseSeatingResponse": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "seats": {
+ "$ref": "#/definitions/util.SeatingInfo"
+ },
+ "waitlist": {
+ "$ref": "#/definitions/util.SeatingInfo"
+ }
+ }
+ },
+ "util.CourseTimeAPI": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "dateRange": {
+ "type": "string"
+ },
+ "days": {
+ "type": "string"
+ },
+ "endTime": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "professorNames": {
+ "type": "string"
+ },
+ "startTime": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "util.CourseTimeFrontendAPI": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "dateRange": {
+ "type": "string"
+ },
+ "days": {
+ "type": "string"
+ },
+ "endTime": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "startTime": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "util.ErrorResponse": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "string"
+ }
+ }
+ },
+ "util.ExamTimeAPI": {
+ "type": "object",
+ "properties": {
+ "crn": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "semester": {
+ "type": "integer"
+ },
+ "time": {
+ "type": "string"
+ }
+ }
+ },
+ "util.FrontendAPIResponse": {
+ "type": "object",
+ "properties": {
+ "courses": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseFrontendAPI"
+ }
+ },
+ "exams": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ExamTimeAPI"
+ }
+ },
+ "profs": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.ProfessorRatingAPI"
+ }
+ },
+ "seatings": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseSeatingResponse"
+ }
+ },
+ "subjects": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.Subject"
+ }
+ },
+ "times": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/util.CourseTimeFrontendAPI"
+ }
+ }
+ }
+ },
+ "util.ProfessorRatingAPI": {
+ "type": "object",
+ "properties": {
+ "difficulty": {
+ "type": "number"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "rating": {
+ "type": "number"
+ },
+ "ratings": {
+ "type": "integer"
+ },
+ "wouldRetake": {
+ "type": "number"
+ }
+ }
+ },
+ "util.SeatingInfo": {
+ "type": "object",
+ "properties": {
+ "actual": {
+ "type": "integer"
+ },
+ "capacity": {
+ "type": "integer"
+ },
+ "remaining": {
+ "type": "integer"
+ }
+ }
+ },
+ "util.Semester": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "latest": {
+ "type": "boolean"
+ },
+ "medicine": {
+ "type": "boolean"
+ },
+ "mi": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "viewOnly": {
+ "type": "boolean"
+ }
+ }
+ },
+ "util.Subject": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Server/docs/swagger.yaml b/Server/docs/swagger.yaml
new file mode 100644
index 0000000..616445d
--- /dev/null
+++ b/Server/docs/swagger.yaml
@@ -0,0 +1,511 @@
+definitions:
+ util.CourseAPI:
+ properties:
+ campus:
+ type: string
+ comment:
+ type: string
+ credits:
+ type: integer
+ crn:
+ type: string
+ dateRange:
+ type: string
+ id:
+ type: string
+ instructor:
+ type: string
+ level:
+ type: string
+ name:
+ type: string
+ registrationDates:
+ type: string
+ section:
+ type: string
+ subject:
+ type: string
+ type:
+ type: string
+ type: object
+ util.CourseFrontendAPI:
+ properties:
+ campus:
+ type: string
+ comment:
+ type: string
+ credits:
+ type: integer
+ crn:
+ type: string
+ dateRange:
+ type: string
+ id:
+ type: string
+ instructor:
+ type: string
+ level:
+ type: string
+ name:
+ type: string
+ registrationDates:
+ type: string
+ section:
+ type: string
+ subject:
+ type: string
+ type:
+ type: string
+ type: object
+ util.CourseSeating:
+ properties:
+ crn:
+ type: string
+ seats:
+ $ref: '#/definitions/util.SeatingInfo'
+ semester:
+ type: string
+ waitlist:
+ $ref: '#/definitions/util.SeatingInfo'
+ type: object
+ util.CourseSeatingResponse:
+ properties:
+ crn:
+ type: string
+ seats:
+ $ref: '#/definitions/util.SeatingInfo'
+ waitlist:
+ $ref: '#/definitions/util.SeatingInfo'
+ type: object
+ util.CourseTimeAPI:
+ properties:
+ crn:
+ type: string
+ dateRange:
+ type: string
+ days:
+ type: string
+ endTime:
+ type: string
+ location:
+ type: string
+ professorNames:
+ type: string
+ startTime:
+ type: string
+ type:
+ type: string
+ type: object
+ util.CourseTimeFrontendAPI:
+ properties:
+ crn:
+ type: string
+ dateRange:
+ type: string
+ days:
+ type: string
+ endTime:
+ type: string
+ location:
+ type: string
+ startTime:
+ type: string
+ type:
+ type: string
+ type: object
+ util.ErrorResponse:
+ properties:
+ error:
+ type: string
+ type: object
+ util.ExamTimeAPI:
+ properties:
+ crn:
+ type: string
+ location:
+ type: string
+ semester:
+ type: integer
+ time:
+ type: string
+ type: object
+ util.FrontendAPIResponse:
+ properties:
+ courses:
+ items:
+ $ref: '#/definitions/util.CourseFrontendAPI'
+ type: array
+ exams:
+ items:
+ $ref: '#/definitions/util.ExamTimeAPI'
+ type: array
+ profs:
+ items:
+ $ref: '#/definitions/util.ProfessorRatingAPI'
+ type: array
+ seatings:
+ items:
+ $ref: '#/definitions/util.CourseSeatingResponse'
+ type: array
+ subjects:
+ items:
+ $ref: '#/definitions/util.Subject'
+ type: array
+ times:
+ items:
+ $ref: '#/definitions/util.CourseTimeFrontendAPI'
+ type: array
+ type: object
+ util.ProfessorRatingAPI:
+ properties:
+ difficulty:
+ type: number
+ id:
+ type: integer
+ name:
+ type: string
+ rating:
+ type: number
+ ratings:
+ type: integer
+ wouldRetake:
+ type: number
+ type: object
+ util.SeatingInfo:
+ properties:
+ actual:
+ type: integer
+ capacity:
+ type: integer
+ remaining:
+ type: integer
+ type: object
+ util.Semester:
+ properties:
+ id:
+ type: integer
+ latest:
+ type: boolean
+ medicine:
+ type: boolean
+ mi:
+ type: boolean
+ name:
+ type: string
+ viewOnly:
+ type: boolean
+ type: object
+ util.Subject:
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ type: object
+info:
+ contact: {}
+paths:
+ /claret.ics:
+ get:
+ consumes:
+ - application/json
+ description: Returns an iCal file containing all schedule items for selected
+ courses
+ parameters:
+ - description: Semester ID (i.e. 202401)
+ in: query
+ name: semester
+ required: true
+ type: string
+ - description: Course Registration Numbers seperated by commas (i.e. 40983,40984)
+ in: query
+ name: crn
+ required: true
+ type: string
+ produces:
+ - text/calendar
+ responses:
+ "200":
+ description: OK
+ schema:
+ type: string
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Get iCal Calendar
+ /courses:
+ get:
+ consumes:
+ - application/json
+ description: Returns all courses for a specified semester
+ parameters:
+ - description: Semester ID (i.e. 202401)
+ in: query
+ name: semester
+ required: true
+ type: string
+ - description: Course ID (i.e. ECE 3400)
+ in: query
+ name: id
+ type: string
+ - description: Course Registration Number (i.e. 40983)
+ in: query
+ name: crn
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/util.CourseAPI'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Get all courses
+ /exams:
+ get:
+ consumes:
+ - application/json
+ description: Returns all exams for a specified semester
+ parameters:
+ - description: Semester ID (i.e. 202401)
+ in: query
+ name: semester
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/util.ExamTimeAPI'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Get all exams
+ /frontend:
+ get:
+ consumes:
+ - application/json
+ description: Returns all data (courses, prof ratings, subjects, seats, times,
+ and exams) for a specified semester (or latest for no semester)
+ parameters:
+ - description: Semester ID (i.e. 202401)
+ in: query
+ name: semester
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/util.FrontendAPIResponse'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Frontend
+ /instructors:
+ get:
+ consumes:
+ - application/json
+ description: Returns all instructors and instructor ratings from a course
+ parameters:
+ - description: Semester ID (i.e. 202401)
+ in: query
+ name: semester
+ required: true
+ type: string
+ - description: Course Registration Number (i.e. 40983)
+ in: query
+ name: crn
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/util.ProfessorRatingAPI'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Get Course Instructors
+ /rmp:
+ get:
+ consumes:
+ - application/json
+ description: Returns all instructor ratings
+ parameters:
+ - description: Instructor Name
+ in: query
+ name: name
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/util.ProfessorRatingAPI'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Get Course Instructors
+ /seats:
+ get:
+ consumes:
+ - application/json
+ description: Returns seats from a specified course, may take a few seconds if
+ seats not cached
+ parameters:
+ - description: Semester ID (i.e. 202401)
+ in: query
+ name: semester
+ required: true
+ type: string
+ - description: Course Registration Number (i.e. 40983)
+ in: query
+ name: crn
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/util.CourseSeating'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Get Course Seats
+ /semester:
+ get:
+ consumes:
+ - application/json
+ description: Returns all semesters
+ parameters:
+ - description: Semester ID (i.e. 202401)
+ in: query
+ name: semester
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/util.Semester'
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Get Semesters
+ /subjects:
+ get:
+ consumes:
+ - application/json
+ description: Returns all subjects
+ parameters:
+ - description: Semester ID (i.e. 202401)
+ in: query
+ name: semester
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/util.Subject'
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Get Subjects
+ /times:
+ get:
+ consumes:
+ - application/json
+ description: Returns all times from course
+ parameters:
+ - description: Semester ID (i.e. 202401)
+ in: query
+ name: semester
+ required: true
+ type: string
+ - description: Course Registration Number (i.e. 40983)
+ in: query
+ name: crn
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/util.CourseTimeAPI'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/util.ErrorResponse'
+ summary: Get Course Times
+swagger: "2.0"
diff --git a/Server/go.mod b/Server/go.mod
new file mode 100644
index 0000000..c822ed0
--- /dev/null
+++ b/Server/go.mod
@@ -0,0 +1,83 @@
+module github.com/evaan/Claret
+
+go 1.23.5
+
+require (
+ github.com/PuerkitoBio/goquery v1.5.1
+ github.com/gin-gonic/gin v1.10.0
+ github.com/gocolly/colly/v2 v2.1.0
+ github.com/joho/godotenv v1.5.1
+ github.com/redis/go-redis/v9 v9.7.3
+ github.com/robfig/cron/v3 v3.0.1
+ github.com/ulule/limiter/v3 v3.11.2
+ gorm.io/driver/postgres v1.5.11
+ gorm.io/gorm v1.25.12
+)
+
+require (
+ github.com/KyleBanks/depth v1.2.1 // indirect
+ github.com/PuerkitoBio/purell v1.2.1 // indirect
+ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+ github.com/andybalholm/cascadia v1.2.0 // indirect
+ github.com/antchfx/htmlquery v1.2.3 // indirect
+ github.com/antchfx/xmlquery v1.2.4 // indirect
+ github.com/antchfx/xpath v1.1.8 // indirect
+ github.com/bytedance/sonic v1.13.2 // indirect
+ github.com/bytedance/sonic/loader v0.2.4 // indirect
+ github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/cloudwego/base64x v0.1.5 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/gin-contrib/cors v1.7.5 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-openapi/jsonpointer v0.21.1 // indirect
+ github.com/go-openapi/jsonreference v0.21.0 // indirect
+ github.com/go-openapi/spec v0.21.0 // indirect
+ github.com/go-openapi/swag v0.23.1 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.26.0 // indirect
+ github.com/gobwas/glob v0.2.3 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
+ github.com/golang/protobuf v1.5.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgx/v5 v5.5.5 // indirect
+ github.com/jackc/puddle/v2 v2.2.1 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/kennygrant/sanitize v1.2.4 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lib/pq v1.10.9 // indirect
+ github.com/mailru/easyjson v0.9.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
+ github.com/swaggo/files v1.0.1 // indirect
+ github.com/swaggo/gin-swagger v1.6.0 // indirect
+ github.com/swaggo/swag v1.16.4 // indirect
+ github.com/temoto/robotstxt v1.1.1 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ golang.org/x/arch v0.16.0 // indirect
+ golang.org/x/crypto v0.37.0 // indirect
+ golang.org/x/net v0.39.0 // indirect
+ golang.org/x/sync v0.13.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/text v0.24.0 // indirect
+ golang.org/x/tools v0.32.0 // indirect
+ google.golang.org/appengine v1.6.6 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/Server/go.sum b/Server/go.sum
new file mode 100644
index 0000000..62a5844
--- /dev/null
+++ b/Server/go.sum
@@ -0,0 +1,328 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
+github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
+github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
+github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
+github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
+github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
+github.com/antchfx/htmlquery v1.2.3 h1:sP3NFDneHx2stfNXCKbhHFo8XgNjCACnU/4AO5gWz6M=
+github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0=
+github.com/antchfx/xmlquery v1.2.4 h1:T/SH1bYdzdjTMoz2RgsfVKbM5uWh3gjDYYepFqQmFv4=
+github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM=
+github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
+github.com/antchfx/xpath v1.1.8 h1:PcL6bIX42Px5usSx6xRYw/wjB3wYGkj0MJ9MBzEKVgk=
+github.com/antchfx/xpath v1.1.8/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
+github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
+github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
+github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
+github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
+github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
+github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
+github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
+github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
+github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoGs=
+github.com/gocolly/colly/v2 v2.1.0/go.mod h1:I2MuhsLjQ+Ex+IzK3afNS8/1qP3AedHOusRPcRdC5o0=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jawher/mow.cli v1.1.0/go.mod h1:aNaQlc7ozF3vw6IJ2dHjp2ZFiA4ozMIYY6PyuRJwlUg=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
+github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
+github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
+github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
+github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
+github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
+github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
+github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
+github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
+github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
+github.com/temoto/robotstxt v1.1.1 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA=
+github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA=
+github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
+golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
+golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
+gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
+gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/Server/internal/api/courses.go b/Server/internal/api/courses.go
new file mode 100644
index 0000000..f1b0cef
--- /dev/null
+++ b/Server/internal/api/courses.go
@@ -0,0 +1,46 @@
+package api
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// CoursesHandler godoc
+// @Summary Get all courses
+// @Description Returns all courses for a specified semester
+// @Accept json
+// @Produce json
+// @Param semester query string true "Semester ID (i.e. 202401)"
+// @Param id query string false "Course ID (i.e. ECE 3400)"
+// @Param crn query string false "Course Registration Number (i.e. 40983)"
+// @Success 200 {array} util.CourseAPI
+// @Failure 400 {object} util.ErrorResponse
+// @Failure 500 {object} util.ErrorResponse
+// @Router /courses [get]
+func CoursesHandler(c *gin.Context, db *gorm.DB) {
+ semester := util.GetParamOrQuery(c, "semester")
+ if semester == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "semester is a required parameter",
+ })
+ return
+ }
+
+ id := util.GetParamOrQuery(c, "id")
+ crn := strings.TrimSpace(c.Query("crn"))
+
+ courses := make([]util.CourseAPI, 0)
+ err := db.Raw("SELECT * FROM courses WHERE semester_id = ? AND (? = '' OR id LIKE '%' || ? || '%') AND (? = '' OR crn = ?)", semester, id, id, crn, crn).Scan(&courses).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, courses)
+}
diff --git a/Server/internal/api/exams.go b/Server/internal/api/exams.go
new file mode 100644
index 0000000..391e3fb
--- /dev/null
+++ b/Server/internal/api/exams.go
@@ -0,0 +1,42 @@
+package api
+
+import (
+ "net/http"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// ExamsHandler godoc
+// @Summary Get all exams
+// @Description Returns all exams for a specified semester
+// @Accept json
+// @Produce json
+// @Param semester query string true "Semester ID (i.e. 202401)"
+// @Success 200 {array} util.ExamTimeAPI
+// @Failure 400 {object} util.ErrorResponse
+// @Failure 500 {object} util.ErrorResponse
+// @Router /exams [get]
+func ExamsHander(c *gin.Context, db *gorm.DB) {
+ semester := util.GetParamOrQuery(c, "semester")
+ if semester == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "semester is a required parameter",
+ })
+ return
+ }
+
+ exams := make([]util.ExamTimeAPI, 0)
+ err := db.Raw(`SELECT *
+ FROM exam_times et JOIN courses c ON et.course_key = c.key
+ WHERE c.semester_id = ?`, semester).Scan(&exams).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, exams)
+}
diff --git a/Server/internal/api/frontend.go b/Server/internal/api/frontend.go
new file mode 100644
index 0000000..88c1160
--- /dev/null
+++ b/Server/internal/api/frontend.go
@@ -0,0 +1,155 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "github.com/redis/go-redis/v9"
+ "gorm.io/gorm"
+)
+
+// CoursesHandler godoc
+// @Summary Frontend
+// @Description Returns all data (courses, prof ratings, subjects, seats, times, and exams) for a specified semester (or latest for no semester)
+// @Accept json
+// @Produce json
+// @Param semester query string false "Semester ID (i.e. 202401)"
+// @Success 200 {object} util.FrontendAPIResponse
+// @Failure 400 {object} util.ErrorResponse
+// @Failure 500 {object} util.ErrorResponse
+// @Router /frontend [get]
+func FrontendHandler(c *gin.Context, db *gorm.DB, rdb *redis.Client) {
+ semesterStr := util.GetParamOrQuery(c, "semester")
+ var semester int
+ if semesterStr == "" {
+ err := db.Raw("SELECT id FROM semesters WHERE latest").Scan(&semester).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ semesterStr = strconv.Itoa(semester)
+ } else {
+ var err error
+ semester, err = strconv.Atoi(semesterStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+ }
+
+ response := util.FrontendAPIResponse{
+ Courses: make([]util.CourseFrontendAPI, 0),
+ Profs: make([]util.ProfessorRatingAPI, 0),
+ Subjects: make([]util.Subject, 0),
+ Times: make([]util.CourseTimeFrontendAPI, 0),
+ Exams: make([]util.ExamTimeAPI, 0),
+ }
+
+ ctx := context.Background()
+ cacheKey := "frontend:" + semesterStr
+ val, err := rdb.Get(ctx, cacheKey).Result()
+ if err == nil {
+ if err := json.Unmarshal([]byte(val), &response); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ } else {
+ if err != redis.Nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ err = db.Raw(`SELECT c.id, c.name, c.crn, c.section, c.credits, c.campus, c.date_range, c.subject_id, c.semester_id, c.comment, c.levels, c.registration_dates, c.types,
+ STRING_AGG(DISTINCT ci.professor_name, ', ') AS instructor FROM courses c LEFT JOIN course_instructors ci ON ci.course_key = c.key
+ WHERE c.semester_id = ? GROUP BY c.id, c.name, c.crn, c.section, c.credits, c.campus, c.date_range, c.subject_id, c.semester_id, c.comment,
+ c.levels, c.registration_dates, c.types;`, semester).Scan(&response.Courses).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ err = db.Raw(`SELECT DISTINCT pr.* FROM professor_ratings pr
+ JOIN course_instructors ci ON pr.professor_name = ci.professor_name
+ JOIN courses c ON ci.course_key = c.key
+ WHERE c.semester_id = ?`, semester).Scan(&response.Profs).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ err = db.Raw(`SELECT DISTINCT s.*
+ FROM subjects s
+ JOIN courses c ON s.id = c.subject_id
+ WHERE c.semester_id = ?`, semester).Scan(&response.Subjects).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ err = db.Raw(`SELECT DISTINCT
+ start_time, end_time, days, location, date_range,
+ type, course_crn, semester_id FROM course_times WHERE semester_id = ?`, semester).Scan(&response.Times).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ err = db.Raw(`SELECT *
+ FROM exam_times et
+ JOIN courses c ON et.course_key = c.key
+ WHERE c.semester_id = ?`, semester).Scan(&response.Exams).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ cacheData, err := json.Marshal(response)
+ if err == nil {
+ _ = rdb.Set(ctx, cacheKey, cacheData, 24*time.Hour).Err()
+ }
+ }
+
+ response.Seatings = make([]util.CourseSeatingResponse, 0)
+ var cursor uint64
+ for {
+ var keys []string
+ keys, cursor, err = rdb.Scan(ctx, cursor, "seats:"+semesterStr+":*", 0).Result()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ for _, key := range keys {
+ val, err := rdb.Get(ctx, key).Result()
+ if err == redis.Nil {
+ continue
+ } else if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ var seating util.CourseSeating
+ if err := json.Unmarshal([]byte(val), &seating); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ response.Seatings = append(response.Seatings, util.CourseSeatingResponse{
+ CRN: seating.CRN,
+ Seats: seating.Seats,
+ Waitlist: seating.Waitlist,
+ })
+ }
+
+ if cursor == 0 {
+ break
+ }
+ }
+
+ c.JSON(http.StatusOK, response)
+}
diff --git a/Server/internal/api/ical.go b/Server/internal/api/ical.go
new file mode 100644
index 0000000..02fa7bb
--- /dev/null
+++ b/Server/internal/api/ical.go
@@ -0,0 +1,126 @@
+package api
+
+import (
+ "fmt"
+ "net/http"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "github.com/lib/pq"
+ "gorm.io/gorm"
+)
+
+// ICalHandler godoc
+// @Summary Get iCal Calendar
+// @Description Returns an iCal file containing all schedule items for selected courses
+// @Accept json
+// @Produce text/calendar
+// @Param semester query string true "Semester ID (i.e. 202401)"
+// @Param crn query string true "Course Registration Numbers seperated by commas (i.e. 40983,40984)"
+// @Success 200 {object} string
+// @Failure 400 {object} util.ErrorResponse
+// @Failure 500 {object} util.ErrorResponse
+// @Router /claret.ics [get]
+func ICalHandler(c *gin.Context, db *gorm.DB) {
+ semesterStr := c.Query("semester")
+ crns := c.Query("crns")
+ if semesterStr == "" || crns == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "semester and crns are required parameters",
+ })
+ return
+ }
+
+ semester, err := strconv.Atoi(semesterStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ var builder strings.Builder
+
+ // vcalendar header
+ builder.WriteString("BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//claretformun.com//Claret Schedule Builder/EN\nCALSCALE:GREGORIAN\nMETHOD:PUBLISH\nX-WR-CALNAME:MUN Courses (via Claret)\nX-WR-TIMEZONE:America/St_Johns\n")
+
+ courseTimes := make([]util.CourseTimeICal, 0)
+ err = db.Raw(`
+ SELECT
+ ct.course_key, ct.course_crn, ct.semester_id, ct.start_time, ct.end_time, ct.days, ct.date_range, ct.location, c.id AS course_id, c.name AS course_name,
+ STRING_AGG(DISTINCT ci.professor_name, ', ' ORDER BY ci.professor_name) AS instructor_names
+ FROM course_times ct
+ JOIN courses c ON c.crn = ct.course_crn AND c.semester_id = ct.semester_id
+ LEFT JOIN course_instructors ci ON ci.course_key = ct.course_key
+ WHERE ct.course_crn = ANY (?) AND ct.semester_id = ?
+ AND NOT (ct.start_time = '00:00' AND ct.end_time = '00:01')
+ AND ct.date_range != ''
+ AND ct.days IS NOT NULL
+ GROUP BY
+ ct.course_key, ct.course_crn, ct.semester_id, ct.start_time, ct.end_time,
+ ct.days, ct.date_range, ct.location, c.id, c.name`, pq.Array(strings.Split(crns, ",")), semester).Scan(&courseTimes).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ re := regexp.MustCompile(`(\d+)(st|nd|rd|th)`)
+ nst, err := time.LoadLocation("America/St_Johns")
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ for _, courseTime := range courseTimes {
+ dateRange := strings.Split(re.ReplaceAllString(courseTime.DateRange, ""), " - ")
+ if len(dateRange) != 2 {
+ continue // there is no reason it shouldn't be structured like this
+ }
+ startDate, err := time.Parse("Jan 2, 2006", dateRange[0])
+ if err != nil {
+ fmt.Println("Error parsing time:", err)
+ continue
+ }
+ endDate, err := time.Parse("Jan 2, 2006", dateRange[1])
+ if err != nil {
+ fmt.Println("Error parsing time:", err)
+ continue
+ }
+ startTime, err := time.Parse("15:04", courseTime.StartTime)
+ if err != nil {
+ fmt.Println("Error parsing time:", err)
+ continue
+ }
+ endTime, err := time.Parse("15:04", courseTime.EndTime)
+ if err != nil {
+ fmt.Println("Error parsing time:", err)
+ continue
+ }
+ firstTime := util.EarliestClassDate(startDate, *courseTime.Days)
+ lastTime := util.LatestClassDate(endDate, *courseTime.Days)
+ builder.WriteString(fmt.Sprintf(
+ "BEGIN:VEVENT\nUID:%s@claretformun.com\nDTSTAMP:%s\nDTSTART;TZID=America/St_Johns:%s\nDTEND;TZID=America/St_Johns:%s\nRRULE:FREQ=WEEKLY;BYDAY=%s;UNTIL=%s\nSUMMARY:%s\nLOCATION:%s\nDESCRIPTION:%s\nEND:VEVENT\n",
+ courseTime.CourseKey,
+ time.Now().UTC().Format("20060102T150405Z"),
+ time.Date(firstTime.Year(), firstTime.Month(), firstTime.Day(), startTime.Hour(), startTime.Minute(), 0, 0, nst).Format("20060102T150405"),
+ time.Date(firstTime.Year(), firstTime.Month(), firstTime.Day(), endTime.Hour(), endTime.Minute(), 0, 0, nst).Format("20060102T150405"),
+ util.ICalRepeatDates(*courseTime.Days),
+ time.Date(lastTime.Year(), lastTime.Month(), lastTime.Day(), 23, 59, 59, 0, nst).Format("20060102T150405Z"),
+ courseTime.CourseID+" - "+courseTime.CourseName,
+ courseTime.Location,
+ "Instructor(s): "+courseTime.InstructorNames+" - Generated with Claret",
+ ))
+ }
+
+ builder.WriteString("END:VCALENDAR\n")
+
+ c.String(http.StatusOK, builder.String())
+}
diff --git a/Server/internal/api/instructors.go b/Server/internal/api/instructors.go
new file mode 100644
index 0000000..d29893c
--- /dev/null
+++ b/Server/internal/api/instructors.go
@@ -0,0 +1,42 @@
+package api
+
+import (
+ "net/http"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// InstructorsHandler godoc
+// @Summary Get Course Instructors
+// @Description Returns all instructors and instructor ratings from a course
+// @Accept json
+// @Produce json
+// @Param semester query string true "Semester ID (i.e. 202401)"
+// @Param crn query string true "Course Registration Number (i.e. 40983)"
+// @Success 200 {array} util.ProfessorRatingAPI
+// @Failure 400 {object} util.ErrorResponse
+// @Failure 500 {object} util.ErrorResponse
+// @Router /instructors [get]
+func InstructorsHandler(c *gin.Context, db *gorm.DB) {
+ semester := util.GetParamOrQuery(c, "semester")
+ crn := util.GetParamOrQuery(c, "crn")
+ if crn == "" || semester == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "semester and crn are required parameters",
+ })
+ return
+ }
+
+ ratings := make([]util.ProfessorRatingAPI, 0)
+ err := db.Raw("SELECT ci.professor_name, pr.rating, pr.difficulty, pr.would_retake, pr.rating_count FROM course_instructors ci LEFT JOIN professor_ratings pr ON ci.professor_name = pr.professor_name WHERE ci.course_key = ?", semester+crn).Scan(&ratings).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, ratings)
+}
diff --git a/Server/internal/api/middleware.go b/Server/internal/api/middleware.go
new file mode 100644
index 0000000..cbf1247
--- /dev/null
+++ b/Server/internal/api/middleware.go
@@ -0,0 +1,46 @@
+package api
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/ulule/limiter/v3"
+ "github.com/ulule/limiter/v3/drivers/store/memory"
+)
+
+func RateLimiterMiddleware(limit string) gin.HandlerFunc {
+ rate, err := limiter.NewRateFromFormatted(limit)
+ if err != nil {
+ panic(err)
+ }
+
+ store := memory.NewStore()
+ instance := limiter.New(store, rate)
+
+ return func(c *gin.Context) {
+ key := c.ClientIP()
+
+ context, err := instance.Get(c, key)
+ if err != nil {
+ fmt.Printf("Error on rate limiter: %s\n", err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.Header("RateLimit-Limit", strconv.FormatInt(context.Limit, 10))
+ c.Header("RateLimit-Remaining", strconv.FormatInt(context.Remaining, 10))
+ c.Header("RateLimit-Reset", strconv.FormatInt(context.Reset, 10))
+
+ if context.Reached {
+ c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
+ "error": "rate limit exceeded",
+ })
+ }
+
+ c.Next()
+ }
+}
diff --git a/Server/internal/api/rmp.go b/Server/internal/api/rmp.go
new file mode 100644
index 0000000..d81c242
--- /dev/null
+++ b/Server/internal/api/rmp.go
@@ -0,0 +1,35 @@
+package api
+
+import (
+ "net/http"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// RmpHandler godoc
+// @Summary Get Course Instructors
+// @Description Returns all instructor ratings
+// @Accept json
+// @Produce json
+// @Param name query string false "Instructor Name"
+// @Success 200 {array} util.ProfessorRatingAPI
+// @Failure 400 {object} util.ErrorResponse
+// @Failure 500 {object} util.ErrorResponse
+// @Router /rmp [get]
+func RmpHandler(c *gin.Context, db *gorm.DB) {
+ ratings := make([]util.ProfessorRatingAPI, 0)
+
+ name := util.GetParamOrQuery(c, "name")
+
+ err := db.Raw("SELECT * FROM professor_ratings WHERE professor_name LIKE ?", "%"+name+"%").Scan(&ratings).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, ratings)
+}
diff --git a/Server/internal/api/seats.go b/Server/internal/api/seats.go
new file mode 100644
index 0000000..3b7b09f
--- /dev/null
+++ b/Server/internal/api/seats.go
@@ -0,0 +1,72 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/evaan/Claret/internal/scrapers"
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "github.com/redis/go-redis/v9"
+ "gorm.io/gorm"
+)
+
+// SeatsHandler godoc
+// @Summary Get Course Seats
+// @Description Returns seats from a specified course, may take a few seconds if seats not cached
+// @Accept json
+// @Produce json
+// @Param semester query string true "Semester ID (i.e. 202401)"
+// @Param crn query string true "Course Registration Number (i.e. 40983)"
+// @Success 200 {object} util.CourseSeating
+// @Failure 400 {object} util.ErrorResponse
+// @Failure 500 {object} util.ErrorResponse
+// @Router /seats [get]
+func SeatsHandler(c *gin.Context, db *gorm.DB, rdb *redis.Client) {
+ semester := util.GetParamOrQuery(c, "semester")
+ crn := util.GetParamOrQuery(c, "crn")
+ if semester == "" || crn == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "semester and crn are required parameters",
+ })
+ return
+ }
+
+ ctx := context.Background()
+
+ val, err := rdb.Get(ctx, "seats:"+semester+":"+crn).Result()
+ if err != nil && err != redis.Nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ } else if err != redis.Nil {
+ response := util.CourseSeating{}
+ if err := json.Unmarshal([]byte(val), &response); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, response)
+ return
+ }
+
+ response := scrapers.GetSeats(semester, crn)
+
+ c.JSON(http.StatusOK, response)
+
+ responseJson, err := json.Marshal(response)
+ if err != nil {
+ fmt.Println("Error marshalling data to put into Redis database:", err)
+ return
+ }
+ err = rdb.Set(ctx, "seats:"+semester+":"+crn, responseJson, time.Hour).Err()
+ if err != nil {
+ fmt.Println("Error adding data to Redis database:", err)
+ }
+ rdb.Del(ctx, "frontend:"+semester)
+}
diff --git a/Server/internal/api/semesters.go b/Server/internal/api/semesters.go
new file mode 100644
index 0000000..b2afdd7
--- /dev/null
+++ b/Server/internal/api/semesters.go
@@ -0,0 +1,32 @@
+package api
+
+import (
+ "net/http"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// SemestersHandler godoc
+// @Summary Get Semesters
+// @Description Returns all semesters
+// @Accept json
+// @Produce json
+// @Param semester query string false "Semester ID (i.e. 202401)"
+// @Success 200 {array} util.Semester
+// @Failure 500 {object} util.ErrorResponse
+// @Router /semester [get]
+func SemestersHandler(c *gin.Context, db *gorm.DB) {
+ semesters := make([]util.Semester, 0)
+
+ err := db.Raw("SELECT * FROM semesters WHERE (? = 'false' OR latest = true) AND (? = 'false' OR mi = true) AND (? = 'false' OR medicine = true)", util.GetParamOrQueryWithDefault(c, "latest", "false"), util.GetParamOrQueryWithDefault(c, "mi", "false"), util.GetParamOrQueryWithDefault(c, "medicine", "false")).Scan(&semesters).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, semesters)
+}
diff --git a/Server/internal/api/subjects.go b/Server/internal/api/subjects.go
new file mode 100644
index 0000000..91f210f
--- /dev/null
+++ b/Server/internal/api/subjects.go
@@ -0,0 +1,35 @@
+package api
+
+import (
+ "net/http"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// SubjectsHandler godoc
+// @Summary Get Subjects
+// @Description Returns all subjects
+// @Accept json
+// @Produce json
+// @Param semester query string false "Semester ID (i.e. 202401)"
+// @Success 200 {array} util.Subject
+// @Failure 500 {object} util.ErrorResponse
+// @Router /subjects [get]
+func SubjectsHandler(c *gin.Context, db *gorm.DB) {
+ subjects := make([]util.Subject, 0)
+
+ err := db.Raw(`SELECT DISTINCT s.*
+ FROM subjects s
+ JOIN courses c ON s.id = c.subject_id
+ WHERE (? = '' OR c.semester_id = ?)`, util.GetParamOrQuery(c, "semester")).Scan(&subjects).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, subjects)
+}
diff --git a/Server/internal/api/times.go b/Server/internal/api/times.go
new file mode 100644
index 0000000..f761135
--- /dev/null
+++ b/Server/internal/api/times.go
@@ -0,0 +1,51 @@
+package api
+
+import (
+ "net/http"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// TimesHandler godoc
+// @Summary Get Course Times
+// @Description Returns all times from course
+// @Accept json
+// @Produce json
+// @Param semester query string true "Semester ID (i.e. 202401)"
+// @Param crn query string true "Course Registration Number (i.e. 40983)"
+// @Success 200 {array} util.CourseTimeAPI
+// @Failure 400 {object} util.ErrorResponse
+// @Failure 500 {object} util.ErrorResponse
+// @Router /times [get]
+func TimesHandler(c *gin.Context, db *gorm.DB) {
+ semester := util.GetParamOrQuery(c, "semester")
+ crn := util.GetParamOrQuery(c, "crn")
+ if crn == "" || semester == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "semester and crn are required parameters",
+ })
+ return
+ }
+
+ times := make([]util.CourseTimeAPI, 0)
+ err := db.Raw(`SELECT
+ ct.start_time, ct.end_time, ct.days, ct.location, ct.date_range,
+ ct.type, ct.course_key, ct.course_crn, ct.semester_id,
+ STRING_AGG(DISTINCT ci.professor_name, ', ' ORDER BY ci.professor_name) AS professor_names
+ FROM course_times ct
+ LEFT JOIN course_instructors ci ON ct.course_key = ci.course_key
+ WHERE ct.semester_id = ? AND ct.course_crn = ?
+ GROUP BY
+ ct.start_time, ct.end_time, ct.days, ct.location, ct.date_range,
+ ct.type, ct.course_key, ct.course_crn, ct.semester_id`, semester, crn).Scan(×).Error
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, times)
+}
diff --git a/Server/internal/scrapers/courses.go b/Server/internal/scrapers/courses.go
new file mode 100644
index 0000000..130d783
--- /dev/null
+++ b/Server/internal/scrapers/courses.go
@@ -0,0 +1,244 @@
+package scrapers
+
+import (
+ "log"
+ "os"
+ "slices"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gocolly/colly/v2"
+)
+
+func ParseCourse(logger *log.Logger, e *colly.HTMLElement, semester int, subject string) (util.Course, []util.CourseTime, []util.Professor, []util.CourseInstructor) {
+ title := e.Text
+ body := e.DOM.Parent().Next().Text()
+
+ semesterStr := strconv.Itoa(semester)
+
+ var course util.Course
+
+ course.SemesterID = semester
+ course.SubjectID = subject
+
+ segments := strings.Split(title, " - ")
+
+ n := len(segments)
+ course.Section = segments[n-1]
+ course.ID = segments[n-2]
+ course.CRN = segments[n-3]
+ course.Key = semesterStr + course.CRN
+ course.Name = strings.Join(segments[:n-3], " - ")
+
+ for i, line := range strings.Split(body, "\n") {
+ line = strings.TrimSpace(line)
+ if line != "" {
+ if i == 2 {
+ if !strings.HasPrefix(line, "Associated Term") {
+ course.Comment = &line
+ }
+ }
+ if strings.HasPrefix(line, "Levels:") {
+ course.Levels = strings.TrimSpace(strings.TrimPrefix(line, "Levels:"))
+ } else if strings.HasPrefix(line, "Registration Dates:") {
+ course.RegistrationDates = strings.TrimSpace(strings.TrimPrefix(line, "Registration Dates: "))
+ }
+ if strings.HasSuffix(line, "Credits") { // get credits from course
+ course.Credits = util.GetCredits(logger, line)
+ } else if strings.HasSuffix(line, "Campus") { // get course campus
+ course.Campus = strings.TrimSuffix(line, " Campus")
+ }
+ }
+ }
+
+ var schedule []string
+
+ e.DOM.Parent().Next().Find("table.datadisplaytable").First().Find("td.dddefault").Each(func(i int, sel *goquery.Selection) {
+ schedule = append(schedule, strings.TrimSpace(strings.TrimPrefix(sel.Text(), "(P)")))
+ })
+
+ var courseTimes []util.CourseTime
+ var professors []util.Professor
+ var courseInstructors []util.CourseInstructor
+
+ types := make([]string, 0)
+
+ // iterate through course time table
+ for i := range len(schedule) / 7 {
+ var courseTime util.CourseTime
+ courseTime.CourseKey = course.Key
+ // get time, some courses are TBA rather than "12:00 am - 12:01 am" because banner is an amazing piece of software
+ if schedule[i*7+1] == "TBA" {
+ courseTime.StartTime = "00:00"
+ courseTime.EndTime = "00:01"
+ courseTime.CourseCRN = course.CRN
+ courseTime.SemesterID = course.SemesterID
+ } else {
+ times := strings.Split(schedule[i*7+1], " - ")
+ startTime, err := time.Parse("3:04 pm", times[0])
+ if err != nil {
+ logger.Printf("Error scraping course time: %s\n", err.Error())
+ util.SendErrorToWebhook(os.Getenv("SCRAPER_WEBHOOK_URL"), err)
+ continue
+ }
+ courseTime.StartTime = startTime.Format("15:04")
+ endTime, err := time.Parse("3:04 pm", times[1])
+ if err != nil {
+ logger.Printf("Error scraping course time: %s\n", err.Error())
+ util.SendErrorToWebhook(os.Getenv("SCRAPER_WEBHOOK_URL"), err)
+ continue
+ }
+ courseTime.EndTime = endTime.Format("15:04")
+ courseTime.CourseCRN = course.CRN
+ courseTime.SemesterID = course.SemesterID
+ }
+ if schedule[i*7+2] != "" {
+ days := schedule[i*7+2]
+ courseTime.Days = &days
+ }
+ courseTime.Location = util.ReplaceBuildingName(schedule[i*7+3])
+ courseTime.DateRange = schedule[i*7+4]
+ if course.DateRange == nil {
+ dateRange := courseTime.DateRange
+ course.DateRange = &dateRange
+ }
+ courseTime.Type = schedule[i*7+5]
+ if !slices.Contains(types, schedule[i*7+5]) {
+ types = append(types, schedule[i*7+5])
+ }
+ instructors := schedule[i*7+6]
+ if instructors != "TBA" {
+ for _, instructor := range strings.Split(instructors, ", ") {
+ professors = append(professors, util.Professor{Name: instructor})
+ courseInstructors = append(courseInstructors, util.CourseInstructor{CourseKey: course.Key, ProfessorName: instructor})
+ }
+ }
+
+ courseTimes = append(courseTimes, courseTime)
+ }
+
+ if len(types) == 0 {
+ course.Types = "No Activity"
+ } else {
+ course.Types = strings.Join(types, ", ")
+ }
+
+ return course, courseTimes, professors, courseInstructors
+}
+
+func GetCoursesWithCourse(logger *log.Logger, semester int, subject string, course string) ([]util.Course, []util.CourseTime, []util.Professor, []util.CourseInstructor) {
+ c := colly.NewCollector()
+
+ semesterStr := strconv.Itoa(semester)
+
+ var courses []util.Course
+ var courseTimes []util.CourseTime
+ var professors []util.Professor
+ var courseInstructors []util.CourseInstructor
+
+ c.OnHTML("th.ddtitle", func(e *colly.HTMLElement) {
+ course, times, profs, instructors := ParseCourse(logger, e, semester, subject)
+ courses = append(courses, course)
+ courseTimes = append(courseTimes, times...)
+ professors = append(professors, profs...)
+ courseInstructors = append(courseInstructors, instructors...)
+ })
+
+ err := c.PostRaw("https://selfservice.mun.ca/direct/bwckschd.p_get_crse_unsec", util.MapToBytes(map[string]any{
+ "term_in": semesterStr,
+ "sel_subj": []string{"dummy", subject},
+ "sel_day": "dummy",
+ "sel_schd": []string{"dummy", "%"},
+ "sel_insm": []string{"dummy", "%"},
+ "sel_camp": []string{"dummy", "%"},
+ "sel_levl": []string{"dummy", "%"},
+ "sel_sess": []string{"dummy", "%"},
+ "sel_instr": []string{"dummy", "%"},
+ "sel_ptrm": []string{"dummy", "%"},
+ "sel_attr": []string{"dummy", "%"},
+ "sel_crse": course,
+ "sel_title": "",
+ "sel_from_cred": "",
+ "sel_to_cred": "",
+ "begin_hh": "0",
+ "begin_mi": "0",
+ "begin_ap": "a",
+ "end_hh": "0",
+ "end_mi": "0",
+ "end_ap": "a",
+ }))
+ if err != nil {
+ logger.Printf("Error getting courses: %s\n", err.Error())
+ }
+
+ c.Wait()
+
+ return courses, courseTimes, util.Unique(professors), courseInstructors
+}
+
+func GetCourses(logger *log.Logger, semester int, subject string) ([]util.Course, []util.CourseTime, []util.Professor, []util.CourseInstructor) {
+ c := colly.NewCollector()
+
+ semesterStr := strconv.Itoa(semester)
+
+ var courses []util.Course
+ var courseTimes []util.CourseTime
+ var professors []util.Professor
+ var courseInstructors []util.CourseInstructor
+
+ c.OnHTML("th.ddtitle", func(e *colly.HTMLElement) {
+ course, times, profs, instructors := ParseCourse(logger, e, semester, subject)
+ courses = append(courses, course)
+ courseTimes = append(courseTimes, times...)
+ professors = append(professors, profs...)
+ courseInstructors = append(courseInstructors, instructors...)
+ })
+
+ err := c.PostRaw("https://selfservice.mun.ca/direct/bwckschd.p_get_crse_unsec", util.MapToBytes(map[string]any{
+ "term_in": semesterStr,
+ "sel_subj": []string{"dummy", subject},
+ "sel_day": "dummy",
+ "sel_schd": []string{"dummy", "%"},
+ "sel_insm": []string{"dummy", "%"},
+ "sel_camp": []string{"dummy", "%"},
+ "sel_levl": []string{"dummy", "%"},
+ "sel_sess": []string{"dummy", "%"},
+ "sel_instr": []string{"dummy", "%"},
+ "sel_ptrm": []string{"dummy", "%"},
+ "sel_attr": []string{"dummy", "%"},
+ "sel_crse": "",
+ "sel_title": "",
+ "sel_from_cred": "",
+ "sel_to_cred": "",
+ "begin_hh": "0",
+ "begin_mi": "0",
+ "begin_ap": "a",
+ "end_hh": "0",
+ "end_mi": "0",
+ "end_ap": "a",
+ }))
+ if err != nil {
+ logger.Printf("Error getting courses: %s\n", err.Error())
+ }
+
+ c.Wait()
+
+ if len(courses) >= 101 {
+ courses = nil
+ courseTimes = nil
+ professors = nil
+ courseInstructors = nil
+ for i := 0; i <= 9; i++ {
+ courses1, times, profs, instructors := GetCoursesWithCourse(logger, semester, subject, strconv.Itoa(i))
+ courses = append(courses, courses1...)
+ courseTimes = append(courseTimes, times...)
+ professors = append(professors, profs...)
+ courseInstructors = append(courseInstructors, instructors...)
+ }
+ }
+
+ return courses, courseTimes, util.Unique(professors), courseInstructors
+}
diff --git a/Scraper/exam.go b/Server/internal/scrapers/exams.go
similarity index 57%
rename from Scraper/exam.go
rename to Server/internal/scrapers/exams.go
index bbdcf07..34a9617 100644
--- a/Scraper/exam.go
+++ b/Server/internal/scrapers/exams.go
@@ -1,24 +1,18 @@
-package main
+package scrapers
import (
"strconv"
"github.com/PuerkitoBio/goquery"
- "github.com/gocolly/colly"
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gocolly/colly/v2"
)
-type ExamTime struct {
- Crn string `gorm:"notNull"`
- SemesterID int `gorm:"column:semester;not null"`
- Semester Semester `gorm:"constraint:OnDelete:CASCADE;"`
- Identifier string `gorm:"primaryKey"`
- Time string `gorm:"notNull"`
- Location string `gorm:"notNull"`
-}
-
-func exams(semester int) {
+func GetExams(semester int) []util.ExamTime {
c := colly.NewCollector()
+ exams := make([]util.ExamTime, 0)
+
c.OnHTML("table.bordertable", func(e *colly.HTMLElement) {
e.DOM.Find("tr").Each(func(i int, s *goquery.Selection) {
selection := s.Find("td.dbdefault")
@@ -26,10 +20,10 @@ func exams(semester int) {
return
}
crn := selection.Next().Next().Next().First().Text()
- db.Save(&ExamTime{
- Crn: crn,
+ exams = append(exams, util.ExamTime{
+ CRN: crn,
SemesterID: semester,
- Identifier: crn + strconv.Itoa(semester),
+ CourseKey: strconv.Itoa(semester) + crn,
Time: selection.Last().Prev().Text(),
Location: selection.Last().Text(),
})
@@ -38,4 +32,6 @@ func exams(semester int) {
c.Visit("https://selfservice.mun.ca/direct/swkgexm.P_Query_Exam?p_term_code=" + strconv.Itoa(semester) + "&p_title=")
c.Wait()
+
+ return exams
}
diff --git a/Scraper/rmp.go b/Server/internal/scrapers/rmp.go
similarity index 56%
rename from Scraper/rmp.go
rename to Server/internal/scrapers/rmp.go
index 891ff86..3519e48 100644
--- a/Scraper/rmp.go
+++ b/Server/internal/scrapers/rmp.go
@@ -1,33 +1,19 @@
-package main
+package scrapers
import (
"bytes"
- "database/sql"
"encoding/json"
"fmt"
"io"
+ "log"
"net/http"
- "slices"
+ "os"
"strings"
- _ "github.com/joho/godotenv/autoload"
+ "github.com/evaan/Claret/internal/util"
)
-type School struct {
- Id string `json:"id"`
- Name string `json:"name"`
-}
-
-type Professor struct {
- Name string `gorm:"primaryKey,not null"`
- Rating float64 `gorm:"not null"`
- Id int `gorm:"not null"`
- Difficulty float64 `gorm:"not null"`
- RatingCount int `gorm:"not null"`
- WouldRetake float64 `gorm:"not null"`
-}
-
-type RateMyProfNode struct {
+type Node struct {
Difficulty float64 `json:"avgDifficulty"`
Rating float64 `json:"avgRating"`
Department string `json:"department"`
@@ -41,7 +27,7 @@ type RateMyProfNode struct {
}
type Edge struct {
- Professor RateMyProfNode `json:"node"`
+ Professor Node `json:"node"`
}
type PageInfo struct {
@@ -68,16 +54,28 @@ type Root struct {
type MatchedProf struct {
MunName string
- RmpProf Professor
+ RmpProf util.ProfessorRating
Distance float64
}
-func rmp() {
- logger.Println("⭐ RMP Scraping Started!")
-
+func RMP(logger *log.Logger, professors []string) []util.ProfessorRating {
cursor := "null"
hasNextPage := true
+ var finalRatings []util.ProfessorRating
+
+ seen := make(map[string]struct{})
+ var munProfs []string
+ for _, p := range professors {
+ p = strings.TrimSpace(p)
+ if p != "" {
+ if _, ok := seen[p]; !ok {
+ seen[p] = struct{}{}
+ munProfs = append(munProfs, p)
+ }
+ }
+ }
+
for hasNextPage {
body := []byte(fmt.Sprintf(`{
"query": "query TeacherSearchPaginationQuery($cursor: String, $query: TeacherSearchQuery!) { search: newSearch { teachers(query: $query, first: 1000, after: $cursor) { didFallback edges { cursor node { ...TeacherCard_teacher id __typename } } pageInfo { hasNextPage endCursor } resultCount filters { field options { value id } } } } } fragment TeacherCard_teacher on Teacher { id legacyId avgRating numRatings ...CardFeedback_teacher ...CardSchool_teacher ...CardName_teacher ...TeacherBookmark_teacher } fragment CardFeedback_teacher on Teacher { wouldTakeAgainPercent avgDifficulty } fragment CardSchool_teacher on Teacher { department school { name id } } fragment CardName_teacher on Teacher { firstName lastName } fragment TeacherBookmark_teacher on Teacher { id isSaved }",
@@ -92,16 +90,18 @@ func rmp() {
}`, cursor))
req, err := http.NewRequest("POST", "https://www.ratemyprofessors.com/graphql", bytes.NewBuffer(body))
- req.Header.Add("Authorization", "Basic dGVzdDp0ZXN0")
if err != nil {
- logger.Fatal(err)
+ logger.Printf("Error scraping RMP: %s\n", err.Error())
+ util.SendErrorToWebhook(os.Getenv("SCRAPER_WEBHOOK_URL"), err)
+ return finalRatings
}
+ req.Header.Add("Authorization", "Basic dGVzdDp0ZXN0")
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
- logger.Println("❌ RMP Failed scraping due to: " + err.Error())
- return
+ logger.Println("❌ RMP failed to fetch:", err)
+ return finalRatings
}
defer res.Body.Close()
@@ -111,53 +111,36 @@ func rmp() {
}
var root Root
- err = json.Unmarshal(resBody, &root)
- if err != nil {
- fmt.Println("Error unmarshalling JSON:", err)
- return
+ if err := json.Unmarshal(resBody, &root); err != nil {
+ logger.Println("❌ Error unmarshalling JSON:", err)
+ return finalRatings
}
- var profs []string
- rows, err := db.Raw("select distinct instructor from courses").Rows()
- if err != nil {
- panic(err)
- }
- for rows.Next() {
- var prof sql.NullString
- rows.Scan(&prof)
- if prof.Valid && (prof.String != "TBA" && !slices.Contains(profs, prof.String)) {
- profs = append(profs, strings.Split(prof.String, ", ")...)
+ for _, edge := range root.Data.Search.Teachers.Edges {
+ prof := edge.Professor
+ if prof.Ratings == 0 {
+ continue
}
- }
-
- var munNames []string
- var rmpProfs []string
- rmpMap := make(map[string]Professor)
- for _, edge := range root.Data.Search.Teachers.Edges {
- if edge.Professor.Ratings > 0 {
- prof := edge.Professor
- matchedProf := closestName(profs, prof.FirstName+" "+prof.LastName)
- if matchedProf.Name != "" {
- if !slices.Contains(munNames, matchedProf.Name) {
- munNames = append(munNames, matchedProf.Name)
- }
- rmpProfs = append(rmpProfs, prof.FirstName+" "+prof.LastName)
- rmpMap[prof.FirstName+" "+prof.LastName] = Professor{prof.FirstName + " " + prof.LastName, prof.Rating, prof.LegacyID, prof.Difficulty, prof.Ratings, prof.RetakePercentage}
- }
+ rmpName := strings.TrimSpace(prof.FirstName + " " + prof.LastName)
+ match := util.ClosestName(munProfs, rmpName)
+ if match.Name == "" {
+ continue
}
- }
- for _, prof := range munNames {
- matchedProf := closestName(rmpProfs, prof)
- matchedProf1 := rmpMap[matchedProf.Name]
- matchedProf1.Name = prof
- db.Save(&matchedProf1)
+ finalRatings = append(finalRatings, util.ProfessorRating{
+ ProfessorName: match.Name,
+ Rating: prof.Rating,
+ ID: prof.LegacyID,
+ Difficulty: prof.Difficulty,
+ RatingCount: prof.Ratings,
+ WouldRetake: prof.RetakePercentage,
+ })
}
cursor = root.Data.Search.Teachers.Info.Cursor
hasNextPage = root.Data.Search.Teachers.Info.HasNextPage
}
- logger.Println("✅ RMP Scrape Complete!")
+ return finalRatings
}
diff --git a/Server/internal/scrapers/seats.go b/Server/internal/scrapers/seats.go
new file mode 100644
index 0000000..8b28e2e
--- /dev/null
+++ b/Server/internal/scrapers/seats.go
@@ -0,0 +1,43 @@
+package scrapers
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gocolly/colly/v2"
+)
+
+func GetSeats(semester string, crn string) util.CourseSeating {
+ seating := util.CourseSeating{Semester: semester, CRN: crn}
+
+ c := colly.NewCollector()
+
+ c.OnHTML("table.datadisplaytable[summary=\"This layout table is used to present the seating numbers.\"] > tbody", func(e *colly.HTMLElement) {
+ e.ForEach("td.dddefault", func(i int, item *colly.HTMLElement) {
+ itemInt, err := strconv.Atoi(item.Text)
+ if err != nil {
+ fmt.Println("Error parsing seat table item", err)
+ return
+ }
+ switch i {
+ case 0:
+ seating.Seats.Capacity = itemInt
+ case 1:
+ seating.Seats.Actual = itemInt
+ case 2:
+ seating.Seats.Remaining = itemInt
+ case 3:
+ seating.Waitlist.Capacity = itemInt
+ case 4:
+ seating.Waitlist.Actual = itemInt
+ case 5:
+ seating.Waitlist.Remaining = itemInt
+ }
+ })
+ })
+
+ c.Visit("https://selfservice.mun.ca/direct/bwckschd.p_disp_detail_sched?term_in=" + semester + "&crn_in=" + crn)
+
+ return seating
+}
diff --git a/Server/internal/scrapers/semesters.go b/Server/internal/scrapers/semesters.go
new file mode 100644
index 0000000..fc707f6
--- /dev/null
+++ b/Server/internal/scrapers/semesters.go
@@ -0,0 +1,51 @@
+package scrapers
+
+import (
+ "log"
+ "strconv"
+ "strings"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gocolly/colly/v2"
+)
+
+func GetSemesters(logger *log.Logger) []util.Semester {
+ c := colly.NewCollector()
+
+ var semesters []util.Semester
+ foundLatest := false
+
+ c.OnHTML("select[name=p_term]", func(e *colly.HTMLElement) {
+ e.DOM.Find("option").Each(func(i int, s *goquery.Selection) {
+ name := s.Text()
+ if name != "None" {
+ id, exists := s.Attr("value")
+ if exists {
+ idInt, err := strconv.Atoi(id)
+ if err != nil {
+ logger.Printf("Error scraping semester: %s\n", err.Error())
+ } else {
+ semesters = append(semesters, util.Semester{
+ ID: idInt,
+ Name: strings.Replace(s.Text(), " (View only)", "", 1),
+ Latest: !foundLatest && !strings.Contains(name, "M"),
+ Medicine: strings.Contains(name, "Medicine"),
+ MI: strings.Contains(name, "MI"),
+ ViewOnly: strings.Contains(name, "(View only)"),
+ })
+ if !foundLatest && !strings.Contains(s.Text(), "M") {
+ foundLatest = true
+ }
+ }
+ }
+ }
+ })
+ })
+
+ c.Visit("https://selfservice.mun.ca/direct/bwckschd.p_disp_dyn_sched")
+
+ c.Wait()
+
+ return semesters
+}
diff --git a/Server/internal/scrapers/subjects.go b/Server/internal/scrapers/subjects.go
new file mode 100644
index 0000000..bfee72e
--- /dev/null
+++ b/Server/internal/scrapers/subjects.go
@@ -0,0 +1,42 @@
+package scrapers
+
+import (
+ "log"
+ "strconv"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/evaan/Claret/internal/util"
+ "github.com/gocolly/colly/v2"
+)
+
+func GetSubjects(logger *log.Logger, semester int) []util.Subject {
+ var subjects []util.Subject
+
+ c := colly.NewCollector()
+
+ c.OnHTML("select[name=sel_subj]", func(e *colly.HTMLElement) {
+ e.DOM.Find("option").Each(func(i int, s *goquery.Selection) {
+ if s.Text() != "All" {
+ id, exists := s.Attr("value")
+ if exists {
+ subjects = append(subjects, util.Subject{
+ ID: id,
+ Name: s.Text(),
+ })
+ }
+ }
+ })
+ })
+
+ err := c.PostRaw("https://selfservice.mun.ca/direct/bwckgens.p_proc_term_date", util.MapToBytes(map[string]any{
+ "p_calling_proc": "bwckschd.p_disp_dyn_sched",
+ "p_term": strconv.Itoa(semester),
+ }))
+ if err != nil {
+ logger.Printf("Error getting subjects: %s\n", err.Error())
+ }
+
+ c.Wait()
+
+ return subjects
+}
diff --git a/Scraper/jwd.go b/Server/internal/util/jwd.go
similarity index 93%
rename from Scraper/jwd.go
rename to Server/internal/util/jwd.go
index 8c6b5e4..b88cb2d 100644
--- a/Scraper/jwd.go
+++ b/Server/internal/util/jwd.go
@@ -1,11 +1,10 @@
-package main
+package util
import (
"math"
)
-//https://rosettacode.org/wiki/Jaro-Winkler_distance#Go
-
+// https://rosettacode.org/wiki/Jaro-Winkler_distance#Go
func jaroSim(str1, str2 string) float64 {
if len(str1) == 0 && len(str2) == 0 {
return 1
@@ -96,7 +95,7 @@ type NameAndDistance struct {
Distance float64
}
-func closestName(names []string, name string) NameAndDistance {
+func ClosestName(names []string, name string) NameAndDistance {
closest := &NameAndDistance{Name: "", Distance: 2}
for _, prof := range names {
diff --git a/Server/internal/util/types.go b/Server/internal/util/types.go
new file mode 100644
index 0000000..903c387
--- /dev/null
+++ b/Server/internal/util/types.go
@@ -0,0 +1,202 @@
+package util
+
+type Semester struct {
+ ID int `gorm:"primaryKey" json:"id"`
+ Name string `gorm:"not null" json:"name"`
+ Latest bool `gorm:"not null" json:"latest"`
+ Medicine bool `gorm:"not null" json:"medicine"`
+ MI bool `gorm:"not null" json:"mi"`
+ ViewOnly bool `gorm:"not null" json:"viewOnly"`
+}
+
+type Subject struct {
+ ID string `gorm:"primaryKey;not null" json:"id"`
+ Name string `gorm:"not null" json:"name"`
+}
+
+type Course struct {
+ Key string `gorm:"primaryKey"`
+ ID string `gorm:"not null"`
+ Name string `gorm:"not null"`
+ CRN string `gorm:"not null"`
+ Section string `gorm:"not null"`
+ Credits int `gorm:"not null"`
+ Campus string `gorm:"not null"`
+ DateRange *string
+ SubjectID string `gorm:"not null"`
+ Subject Subject `gorm:"constraint:OnDelete:CASCADE;"`
+ SemesterID int `gorm:"not null"`
+ Semester Semester `gorm:"constraint:OnDelete:CASCADE;"`
+ Comment *string
+ Levels string `gorm:"not null"`
+ RegistrationDates string `json:"registrationDates"`
+ Types string `gorm:"not null"`
+}
+
+type CourseSeating struct {
+ Semester string `json:"semester"`
+ CRN string `json:"crn"`
+ Seats SeatingInfo `json:"seats"`
+ Waitlist SeatingInfo `json:"waitlist"`
+}
+
+type CourseSeatingResponse struct {
+ CRN string `json:"crn"`
+ Seats SeatingInfo `json:"seats"`
+ Waitlist SeatingInfo `json:"waitlist"`
+}
+
+type SeatingInfo struct {
+ Capacity int `json:"capacity"`
+ Actual int `json:"actual"`
+ Remaining int `json:"remaining"`
+}
+
+type CourseAPI struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ CRN string `json:"crn"`
+ Section string `json:"section"`
+ Credits int `json:"credits"`
+ Campus string `json:"campus"`
+ DateRange *string `json:"dateRange"`
+ SubjectId string `json:"subject"`
+ Comment *string `json:"comment"`
+ Levels string `json:"level"`
+ RegistrationDates string `json:"registrationDates"`
+ Types string `json:"type"`
+ Instructor string `json:"instructor"`
+}
+
+type CourseFrontendAPI struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ CRN string `json:"crn"`
+ Section string `json:"section"`
+ Credits int `json:"credits"`
+ Campus string `json:"campus"`
+ DateRange *string `json:"dateRange"`
+ SubjectId string `json:"subject"`
+ Comment *string `json:"comment"`
+ Levels string `json:"level"`
+ RegistrationDates string `json:"registrationDates"`
+ Types string `json:"type"`
+ Instructor string `gorm:"column:instructor" json:"instructor"`
+}
+
+type CourseTime struct {
+ ID int `gorm:"primaryKey;autoIncrement"`
+ StartTime string `gorm:"not null"`
+ EndTime string `gorm:"not null"`
+ Days *string
+ Location string `gorm:"not null"`
+ DateRange string `gorm:"not null"`
+ Type string `gorm:"not null"`
+ CourseKey string `gorm:"not null"`
+ CourseCRN string
+ Course Course `gorm:"constraint:OnDelete:CASCADE;"`
+ SemesterID int
+ Semester Semester `gorm:"constraint:OnDelete:CASCADE;"`
+}
+
+type CourseTimeAPI struct {
+ StartTime string `json:"startTime"`
+ EndTime string `json:"endTime"`
+ Days *string `json:"days"`
+ Location string `json:"location"`
+ DateRange string `json:"dateRange"`
+ Type string `json:"type"`
+ CourseCRN string `json:"crn"`
+ ProfessorNames *string `json:"professorNames"`
+}
+
+type CourseTimeICal struct {
+ ID int
+ StartTime string
+ EndTime string
+ Days *string
+ Location string
+ DateRange string
+ Type string
+ CourseKey string
+ CourseCRN string
+ SemesterID int
+ CourseID string
+ CourseName string
+ InstructorNames string
+}
+
+type CourseTimeFrontendAPI struct {
+ StartTime string `json:"startTime"`
+ EndTime string `json:"endTime"`
+ Days *string `json:"days"`
+ Location string `json:"location"`
+ DateRange string `json:"dateRange"`
+ Type string `json:"type"`
+ CourseCRN string `json:"crn"`
+}
+
+type Professor struct {
+ Name string `gorm:"primaryKey"`
+}
+
+type CourseInstructor struct {
+ ID int `gorm:"primaryKey;autoIncrement"`
+ ProfessorName string `gorm:"not null"`
+ Professor Professor `gorm:"constraint:OnDelete:CASCADE;"`
+ CourseKey string `gorm:"not null"`
+ Course Course `gorm:"constraint:OnDelete:CASCADE;"`
+}
+
+type CourseInstructorAPI struct {
+ ProfessorName string `json:"name"`
+ CRN string `json:"crn"`
+}
+
+type ProfessorRating struct {
+ ProfessorName string `gorm:"primaryKey;not null" json:"name"`
+ Professor Professor `gorm:"constraint:OnDelete:CASCADE;"`
+ Rating float64 `gorm:"not null" json:"rating"`
+ ID int `gorm:"not null" json:"id"`
+ Difficulty float64 `gorm:"not null" json:"difficulty"`
+ RatingCount int `gorm:"not null" json:"ratings"`
+ WouldRetake float64 `gorm:"not null" json:"wouldRetake"`
+}
+
+type ProfessorRatingAPI struct {
+ ProfessorName string `json:"name"`
+ Rating float64 `json:"rating"`
+ ID int `json:"id"`
+ Difficulty float64 `json:"difficulty"`
+ RatingCount int `json:"ratings"`
+ WouldRetake float64 `json:"wouldRetake"`
+}
+
+type FrontendAPIResponse struct {
+ Courses []CourseFrontendAPI `json:"courses"`
+ Profs []ProfessorRatingAPI `json:"profs"`
+ Subjects []Subject `json:"subjects"`
+ Seatings []CourseSeatingResponse `json:"seatings"`
+ Times []CourseTimeFrontendAPI `json:"times"`
+ Exams []ExamTimeAPI `json:"exams"`
+}
+
+type ExamTime struct {
+ CRN string `gorm:"not null"`
+ SemesterID int `gorm:"column:semester;not null"`
+ Semester Semester `gorm:"constraint:OnDelete:CASCADE;"`
+ CourseKey string `gorm:"primaryKey;not null"`
+ Course Course `gorm:"constraint:OnDelete:CASCADE;"`
+ Time string `gorm:"not null"`
+ Location string `gorm:"not null"`
+}
+
+type ExamTimeAPI struct {
+ CRN string `json:"crn"`
+ Time string `json:"time"`
+ Location string `json:"location"`
+}
+
+type ErrorResponse struct {
+ Error string `json:"error"`
+}
diff --git a/Server/internal/util/util.go b/Server/internal/util/util.go
new file mode 100644
index 0000000..31ce9be
--- /dev/null
+++ b/Server/internal/util/util.go
@@ -0,0 +1,235 @@
+package util
+
+import (
+ "bytes"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "slices"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+func MapToBytes(data map[string]any) []byte {
+ if len(data) == 0 {
+ return []byte{}
+ }
+
+ var b bytes.Buffer
+
+ for key, value := range data {
+ escapedKey := url.QueryEscape(key)
+
+ switch v := value.(type) {
+ case string:
+ b.WriteString(escapedKey)
+ b.WriteByte('=')
+ b.WriteString(url.QueryEscape(v))
+ b.WriteByte('&')
+ case []string:
+ for _, item := range v {
+ b.WriteString(escapedKey)
+ b.WriteByte('=')
+ b.WriteString(url.QueryEscape(item))
+ b.WriteByte('&')
+ }
+ }
+ }
+
+ buf := b.Bytes()
+
+ return buf[:len(buf)-1]
+}
+
+var buildingCodeMap = map[string]string{
+ "Arts and Administration Bldg": "A",
+ "Henrietta Harvey Bldg": "HH",
+ "Business Administration Bldg": "BN",
+ "INCO Innovation Centre": "IIC",
+ "Biotechnology Bldg": "BT",
+ "St. John's College": "J",
+ "Chemistry - Physics Bldg": "C",
+ "Core Science Facility": "CSF",
+ "M. O. Morgan Bldg": "MU",
+ "Computing Services": "CS",
+ "Physical Education Bldg": "PE",
+ "G. A. Hickman Bldg": "ED",
+ "Queen's College": "QC",
+ "Queen Elizabeth II Library": "L",
+ "S. J. Carew Bldg.": "EN",
+ "Science Bldg": "S",
+ "Alexander Murray Bldg": "ER",
+ "Health Sciences Centre": "H",
+ "Coughlan College": "CL",
+ "Marine Institute": "MI",
+ "Center for Nursing Studies": "N",
+ "Arts and Science (SWGC)": "AS",
+ "Fine Arts (SWGC)": "FA",
+ "Forest Centre": "FC",
+ "Library/Computing (SWGC)": "LC",
+ "Western Memorial Hospital": "WMH",
+}
+
+func ReplaceBuildingName(location string) string {
+ for longName, shortCode := range buildingCodeMap {
+ if strings.Contains(location, longName) {
+ return strings.Replace(location, longName, shortCode, 1)
+ }
+ }
+ return location
+}
+
+func GetCredits(logger *log.Logger, line string) int {
+ var credits []int
+ for _, segment := range strings.Split(strings.TrimSuffix(line, " Credits"), " ") {
+ if segment != "OR" && strings.TrimSpace(segment) != "" {
+ credit, err := strconv.ParseFloat(segment, 64)
+ if err != nil {
+ logger.Printf("Error parsing credits: %s\n", err.Error())
+ SendErrorToWebhook(os.Getenv("SCRAPER_WEBHOOK_URL"), err)
+ continue
+ }
+ credits = append(credits, int(credit))
+ }
+ }
+ return slices.Max(credits)
+}
+
+func Unique[T comparable](input []T) []T {
+ seen := make(map[T]struct{})
+ result := make([]T, 0, len(input))
+ for _, v := range input {
+ if _, exists := seen[v]; !exists {
+ seen[v] = struct{}{}
+ result = append(result, v)
+ }
+ }
+ return result
+}
+
+func GetEnvAsBool(key string) bool {
+ output, err := strconv.ParseBool(os.Getenv(key))
+ if err != nil {
+ fmt.Println("Error parsing environment variable:", err)
+ return false
+ }
+ return output
+}
+
+func GetEnvAsInt(key string) int {
+ output, err := strconv.Atoi(os.Getenv(key))
+ if err != nil {
+ fmt.Println("Error parsing environment variable:", err)
+ return 0
+ }
+ return output
+}
+
+func GetParamOrQuery(c *gin.Context, key string) string {
+ if query := strings.TrimSpace(c.Query(key)); query != "" {
+ return query
+ }
+ return strings.TrimSpace(c.Param(key))
+}
+
+func GetParamOrQueryWithDefault(c *gin.Context, key string, fallback string) string {
+ if query := strings.TrimSpace(c.Query(key)); query != "" {
+ return query
+ } else if param := strings.TrimSpace(c.Param(key)); param != "" {
+ return param
+ } else {
+ return fallback
+ }
+}
+
+func SendErrorToWebhook(webhookUrl string, err error) {
+ params := fmt.Sprintf(`{"username":"Claret Scraper","embeds":[{"author":{"name":"Claret Scraper Error","url":"https://claretformun.com"},"timestamp":"%s","color":16711680,"fields":[{"name":"Error","value":"%s"}]}]}`, time.Now().Format(time.RFC3339), err.Error())
+ r, err := http.NewRequest("POST", os.Getenv("SCRAPER_WEBHOOK_URL"), bytes.NewBuffer([]byte(params)))
+ if err != nil {
+ panic(err)
+ }
+ r.Header.Add("Content-Type", "application/json")
+ client := &http.Client{}
+ res, err := client.Do(r)
+ if err != nil {
+ panic(err)
+ }
+ defer res.Body.Close()
+}
+
+var runeToWeekday = map[rune]time.Weekday{
+ 'M': time.Monday,
+ 'T': time.Tuesday,
+ 'W': time.Wednesday,
+ 'R': time.Thursday,
+ 'F': time.Friday,
+ 'S': time.Saturday,
+ 'U': time.Sunday,
+}
+
+var runeToICal = map[rune]string{
+ 'M': "MO",
+ 'T': "TU",
+ 'W': "WE",
+ 'R': "TH",
+ 'F': "FR",
+ 'S': "SA",
+ 'U': "SU",
+}
+
+func EarliestClassDate(start time.Time, compact string) time.Time {
+ var earliest time.Time
+
+ for _, char := range strings.ToUpper(compact) {
+ targetDay, ok := runeToWeekday[char]
+ if !ok {
+ continue
+ }
+
+ offset := (int(targetDay) - int(start.Weekday()) + 7) % 7
+ match := start.AddDate(0, 0, offset)
+
+ if earliest.IsZero() || match.Before(earliest) {
+ earliest = match
+ }
+ }
+
+ return earliest
+}
+
+func LatestClassDate(start time.Time, compact string) time.Time {
+ var latest time.Time
+
+ for _, char := range strings.ToUpper(compact) {
+ targetDay, ok := runeToWeekday[char]
+ if !ok {
+ continue
+ }
+
+ offset := (int(targetDay) - int(start.Weekday()) + 7) % 7
+ match := start.AddDate(0, 0, -offset)
+
+ if latest.IsZero() || match.After(latest) {
+ latest = match
+ }
+ }
+
+ return latest
+}
+
+func ICalRepeatDates(input string) string {
+ var output []string
+
+ for _, char := range strings.ToUpper(input) {
+ if val, ok := runeToICal[char]; ok {
+ output = append(output, val)
+ }
+ }
+
+ return strings.Join(output, ",")
+}
diff --git a/Server/main.go b/Server/main.go
new file mode 100644
index 0000000..8bf072d
--- /dev/null
+++ b/Server/main.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+ "log"
+ "os"
+
+ "github.com/evaan/Claret/cmd/api"
+ "github.com/evaan/Claret/cmd/scrapers"
+ "github.com/evaan/Claret/internal/util"
+ _ "github.com/joho/godotenv/autoload"
+ "github.com/redis/go-redis/v9"
+ "gorm.io/driver/postgres"
+ "gorm.io/gorm"
+)
+
+func main() {
+ logger := log.Default()
+
+ logger.Println("👋 Claret")
+
+ db, err := gorm.Open(postgres.Open(os.Getenv("POSTGRES_URL")), &gorm.Config{})
+ if err != nil {
+ logger.Fatal(err)
+ }
+ logger.Println("💿 Connected to PostgreSQL Database!")
+
+ db.AutoMigrate(&util.Semester{})
+ db.AutoMigrate(&util.Subject{})
+ db.AutoMigrate(&util.Course{})
+ db.AutoMigrate(&util.CourseTime{})
+ db.AutoMigrate(&util.Professor{})
+ db.AutoMigrate(&util.CourseInstructor{})
+ db.AutoMigrate(&util.ProfessorRating{})
+ db.AutoMigrate(&util.ExamTime{})
+
+ rdb := redis.NewClient(&redis.Options{
+ Addr: os.Getenv("REDIS_URL"),
+ Username: os.Getenv("REDIS_USERNAME"),
+ Password: os.Getenv("REDIS_PASSWORD"),
+ DB: util.GetEnvAsInt("REDIS_CACHE_DB"),
+ })
+
+ if util.GetEnvAsBool("SCRAPER_ENABLED") {
+ go scrapers.Entrypoint(db, os.Getenv("API_WEBHOOK_URL"), util.GetEnvAsBool("SCRAPER_ALL"), rdb)
+ }
+
+ if util.GetEnvAsBool("API_ENABLED") {
+ logger.Println("💿 Connected to Redis Database!")
+ go api.StartAPI(db, rdb, util.GetEnvAsBool("API_RATE_LIMIT_ENABLED"), os.Getenv("API_RATE_LIMIT"))
+ }
+
+ select {}
+}
diff --git a/compose.yml b/compose.yml
index 0b91a43..e4567a7 100644
--- a/compose.yml
+++ b/compose.yml
@@ -34,16 +34,31 @@ services:
volumes:
- ./postgres-data:/var/lib/postgresql/data
- api:
- container_name: api
+ redis:
+ image: redis:alpine
restart: always
- build: ./API
+ volumes:
+ - ./redis-data:/data
+
+ server:
+ container_name: server
+ restart: always
+ build: ./Server
depends_on:
- postgres
+ - redis
ports:
- "8080:8080"
environment:
- DB_URL: "postgresql://postgres:admin@postgres:5432/claret"
+ POSTGRES_URL: "postgresql://postgres:admin@postgres:5432/claret"
+ REDIS_URL: "redis:6379"
+ SCRAPER_ENABLED: "true"
+ SCRAPER_ALL: "true"
+ SCRAPER_WEBHOOK_URL: ${WEBHOOK_URL}
+ API_ENABLED: "true"
+ GIN_MODE: "release"
+ API_RATE_LIMIT_ENABLED: "true"
+ API_RATE_LIMIT: "120-M"
PORT: 8080
TZ: America/St_Johns
labels:
@@ -52,62 +67,3 @@ services:
- traefik.http.routers.api.rule=Host(`api.claretformun.com`)
- traefik.http.routers.api.entrypoints=websecure
- traefik.http.routers.api.tls=true
-
- scraper:
- container_name: scraper
- restart: always
- build: ./Scraper
- depends_on:
- - postgres
- environment:
- DB_URL: "postgresql://postgres:admin@postgres:5432/claret"
- WEBHOOK_URL: ${WEBHOOK_URL}
- TZ: America/St_Johns
-
- icsserver:
- container_name: icsServer
- restart: always
- build: ./ICSServer
- depends_on:
- - postgres
- ports:
- - "8000:8000"
- environment:
- DB_URL: "postgresql://postgres:admin@postgres:5432/claret"
- PORT: 8000
- TZ: America/St_Johns
- BANNER_TZ: America/St_Johns
- labels:
- - traefik.enable=true
- - traefik.port=8000
- - traefik.http.routers.ics.rule=Host(`ics.claretformun.com`)
- - traefik.http.routers.ics.entrypoints=websecure
- - traefik.http.routers.ics.tls=true
-
- nginx-prometheus-exporter:
- image: nginx/nginx-prometheus-exporter:latest
- container_name: nginx-prometheus-exporter
- restart: always
- command:
- - --nginx.scrape-uri=http://nginx:80/stub_status
- ports:
- - "9113:9113"
- depends_on:
- - nginx
- labels:
- - "traefik.enable=false"
-
- # discordbot:
- # container_name: discordBot
- # restart: always
- # build:
- # context: .
- # dockerfile: DiscordBot.Dockerfile
- # depends_on:
- # - postgres
- # - claretapi
- # environment:
- # TOKEN: ${DISCORD_BOT_TOKEN}
- # API_URL: https://api.claretformun.com
- # GUILD_ID: ${DISCORD_GUILD_ID}
- # DB_URL: "postgresql://postgres:admin@postgres:5432/claret"
\ No newline at end of file