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 ( -

- - -
+
+ + +
); } @@ -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) => ( -
+
-
))}
- @@ -414,40 +215,20 @@ export default function Schedule() { Sharing/Exporting - - -
); -} +} \ 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 (
-
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")}

+

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...

+ + } 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