From 774383ec3f48717ad31d921da809aeb5a01021c8 Mon Sep 17 00:00:00 2001 From: TheSilentSage Date: Tue, 11 Jun 2024 19:26:21 +0530 Subject: [PATCH 1/7] fix: merged slot for lab while fetching timetable --- .../internal/models/timetable.go | 63 ++++++++++++------- .../internal/utils/timetableDetection.go | 6 -- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/vitty-backend-api/internal/models/timetable.go b/vitty-backend-api/internal/models/timetable.go index 62d04f1..deeb9a9 100644 --- a/vitty-backend-api/internal/models/timetable.go +++ b/vitty-backend-api/internal/models/timetable.go @@ -29,10 +29,12 @@ func (t Timetable) GetDaySlots(day time.Weekday) map[string][]Slot { resp := make(map[string][]Slot) var data []Slot daySlots := DailySlots[day.String()] + labSlot := "" var err error // Theory slots for _, slot := range t.Slots { + if slot.Type == "Theory" && slices.Contains(daySlots["Theory"], slot.Slot) { index := slices.Index(daySlots["Theory"], slot.Slot) slot.StartTime, err = time.ParseInLocation(STD_REF_TIME, TheoryTimings[index].StartTime, time.Local) @@ -51,19 +53,27 @@ func (t Timetable) GetDaySlots(day time.Weekday) map[string][]Slot { data = append(data, slot) } else if slot.Type == "Lab" && slices.Contains(daySlots["Lab"], slot.Slot) { index := slices.Index(daySlots["Lab"], slot.Slot) - slot.StartTime, err = time.ParseInLocation(STD_REF_TIME, LabTimings[index].StartTime, time.Local) - if err != nil { - log.Println("Error parsing time: ", err) - return nil - } - slot.EndTime, err = time.ParseInLocation(STD_REF_TIME, LabTimings[index].EndTime, time.Local) + if labSlot == "" { + labSlot += slot.Slot + "+" + continue + } else { + slot.StartTime, err = time.ParseInLocation(STD_REF_TIME, LabTimings[index-1].StartTime, time.Local) + if err != nil { + log.Println("Error parsing time: ", err) + return nil + } - if err != nil { - log.Println("Error parsing time: ", err) - return nil + slot.EndTime, err = time.ParseInLocation(STD_REF_TIME, LabTimings[index].EndTime, time.Local) + if err != nil { + log.Println("Error parsing time: ", err) + return nil + } + slot.Slot = labSlot + slot.Slot + labSlot = "" + data = append(data, slot) } - data = append(data, slot) + } } resp[day.String()] = data @@ -72,9 +82,11 @@ func (t Timetable) GetDaySlots(day time.Weekday) map[string][]Slot { func (t Timetable) GetDaywiseTimetable() map[string][]Slot { resp := make(map[string][]Slot) + labSlot := "" for _, slot := range t.Slots { for day, value := range DailySlots { + if slices.Contains(value["Theory"], slot.Slot) { index := slices.Index(value["Theory"], slot.Slot) var err error @@ -91,18 +103,27 @@ func (t Timetable) GetDaywiseTimetable() map[string][]Slot { resp[day] = append(resp[day], slot) } else if slices.Contains(value["Lab"], slot.Slot) { index := slices.Index(value["Lab"], slot.Slot) - var err error - slot.StartTime, err = time.ParseInLocation(STD_REF_TIME, LabTimings[index].StartTime, time.Local) - if err != nil { - log.Println("Error parsing time: ", err) - return nil - } - slot.EndTime, err = time.ParseInLocation(STD_REF_TIME, LabTimings[index].EndTime, time.Local) - if err != nil { - log.Println("Error parsing time: ", err) - return nil + + if labSlot == "" { + labSlot += slot.Slot + "+" + continue + } else { + var err error + slot.StartTime, err = time.ParseInLocation(STD_REF_TIME, LabTimings[index-1].StartTime, time.Local) + if err != nil { + log.Println("Error parsing time: ", err) + return nil + } + slot.EndTime, err = time.ParseInLocation(STD_REF_TIME, LabTimings[index].EndTime, time.Local) + if err != nil { + log.Println("Error parsing time: ", err) + return nil + } + + slot.Slot = labSlot + slot.Slot + labSlot = "" + resp[day] = append(resp[day], slot) } - resp[day] = append(resp[day], slot) } } } diff --git a/vitty-backend-api/internal/utils/timetableDetection.go b/vitty-backend-api/internal/utils/timetableDetection.go index 19ad91d..17bd513 100644 --- a/vitty-backend-api/internal/utils/timetableDetection.go +++ b/vitty-backend-api/internal/utils/timetableDetection.go @@ -48,12 +48,6 @@ func DetectTimetable(text string) ([]TimetableSlotV1, error) { } if len(Slots) == 0 { - return Slots, nil - } - - var err error - - if err != nil { return Slots, errors.New("error in detecting timetable") } From 6c6ca8a17bb3b921b03fd063d8d61fd768c45980 Mon Sep 17 00:00:00 2001 From: TheSilentSage Date: Fri, 21 Jun 2024 14:08:35 +0530 Subject: [PATCH 2/7] feat: empty classrooms list --- vitty-backend-api/api/v2/userHandler.go | 25 ++++++ .../cli/commands/timetableCommands.go | 90 ++++++++++++++++++- vitty-backend-api/cli/commands/utils.go | 42 +++++++++ .../internal/database/initialize.go | 4 +- vitty-backend-api/internal/models/slots.go | 4 + 5 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 vitty-backend-api/cli/commands/utils.go diff --git a/vitty-backend-api/api/v2/userHandler.go b/vitty-backend-api/api/v2/userHandler.go index a0a5a76..7997098 100644 --- a/vitty-backend-api/api/v2/userHandler.go +++ b/vitty-backend-api/api/v2/userHandler.go @@ -1,7 +1,10 @@ package v2 import ( + "encoding/json" "fmt" + "log" + "os" "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/serializers" @@ -19,6 +22,7 @@ func userHandler(api fiber.Router) { group.Get("/suggested", getSuggestedUsers) group.Get("/:username", getUser) group.Delete("/:username", deleteUser) + group.Get("/emptyClassRooms", getEmptyClassRooms) } func searchUsers(c *fiber.Ctx) error { @@ -83,3 +87,24 @@ func deleteUser(c *fiber.Ctx) error { "detail": "User deleted successfully", }) } + +func getEmptyClassRooms(c *fiber.Ctx) error { + file, err := os.Open("./data/freeClasses.json") + if err != nil { + log.Printf("Error opening file: %v", err) + return c.Status(fiber.StatusInternalServerError).SendString("Please contact vitty support") + } + defer file.Close() + + var freeClasses interface{} + decoder := json.NewDecoder(file) + err = decoder.Decode(&freeClasses) + if err != nil { + log.Fatalf("Error decoding JSON: %v", err) + } + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) + } + + return c.Status(fiber.StatusOK).JSON(freeClasses) +} diff --git a/vitty-backend-api/cli/commands/timetableCommands.go b/vitty-backend-api/cli/commands/timetableCommands.go index daac625..1ec5265 100644 --- a/vitty-backend-api/cli/commands/timetableCommands.go +++ b/vitty-backend-api/cli/commands/timetableCommands.go @@ -1,7 +1,9 @@ package commands import ( + "encoding/json" "fmt" + "os" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" @@ -22,6 +24,12 @@ var TimetableCommands = []*cli.Command{ Usage: "Fix slot times", Action: fixSlotTimes, }, + { + Name: "empty-rooms", + Aliases: []string{"er"}, + Usage: "Shows empty classrooms", + Action: getEmptyRooms, + }, } func parseTimetable(c *cli.Context) error { @@ -37,7 +45,7 @@ func parseTimetable(c *cli.Context) error { fmt.Println("Parsed data: ") fmt.Println(timetableV1) - fmt.Println("\n\n") + fmt.Print("\n\n") var timetableSlots []models.Slot for _, slot := range timetableV1 { @@ -73,3 +81,83 @@ func fixSlotTimes(c *cli.Context) error { } return nil } + +func getEmptyRooms(c *cli.Context) error { + reset := "\033[0m" + red := "\033[31m" + green := "\033[32m" + cyan := "\033[36m " + + fmt.Print(cyan, "Initiating ", reset) + fmt.Print("Extracting Class details... ") + + err := database.DB.Exec(` + Drop table IF EXISTS joinData; + CREATE TABLE joinData ( + class text, + slots JSONB + ); + + INSERT INTO joinData (class, slots) + SELECT + elems.data->>'venue' AS venue, + jsonb_agg( DISTINCT elems.data->>'slot') AS slots + FROM + timetables, + jsonb_array_elements(timetables.slots::jsonb) AS elems(data) + GROUP BY + elems.data->>'venue'; + `).Error + + if err != nil { + fmt.Println(red, "Failed") + fmt.Println("Error: ", err, reset) + } + + fmt.Println(green, "Complete", reset) + + fmt.Print(cyan, "Initiating ", reset) + fmt.Print("Looking for empty classes... ") + + emptyClassRoomsJson := make(map[string]interface{}) + + for _, slot := range models.TimetableSlots { + freeClasses, err := findEmptyClassRooms(slot) + + if err != nil { + fmt.Println(red, "Failed") + fmt.Printf("Slot %s was not able to be processed\nError: %s %s", slot, err, reset) + } + + emptyClassRoomsJson[slot] = freeClasses + } + + fmt.Println(green, "Complete", reset) + fmt.Print(cyan, "Initiating ", reset) + fmt.Print("Saving result... ") + + jsonData, err := json.Marshal(emptyClassRoomsJson) + if err != nil { + fmt.Println("Error encoding JSON:", err) + } + + err = database.DB.Exec(` + Drop table joindata; + `).Error + + if err != nil { + fmt.Println(red, "Failed") + fmt.Println("Error: ", err, reset) + } + + err = os.WriteFile("./data/freeClasses.json", jsonData, 0644) + + if err != nil { + fmt.Println(red, "Failed") + fmt.Println("Error: ", err, reset) + } + + fmt.Println(green, "Complete", reset) + + return nil +} diff --git a/vitty-backend-api/cli/commands/utils.go b/vitty-backend-api/cli/commands/utils.go new file mode 100644 index 0000000..d0b1223 --- /dev/null +++ b/vitty-backend-api/cli/commands/utils.go @@ -0,0 +1,42 @@ +package commands + +import ( + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" +) + +func findEmptyClassRooms(slot string) ([]string, error) { + var freeClasses []string + total := 0 + offset := 0 + limit := 1000 + + query := database.DB. + Table("joindata"). + Where("NOT (slots @> '[\"?\"]')", database.DB.Raw(slot)). + Where("slots::text !~ '\\[\"L.*\"\\]'") + + err := query. + Select("COUNT(class)"). + Scan(&total).Error + + if err != nil { + return freeClasses, err + } + + for total >= 0 { + err := query. + Select("class"). + Limit(limit). + Offset(offset). + Find(&freeClasses).Error + + if err != nil { + return freeClasses, err + } + + total -= limit + offset += limit + } + + return freeClasses, nil +} diff --git a/vitty-backend-api/internal/database/initialize.go b/vitty-backend-api/internal/database/initialize.go index 4a4d834..14bb4b0 100644 --- a/vitty-backend-api/internal/database/initialize.go +++ b/vitty-backend-api/internal/database/initialize.go @@ -18,7 +18,9 @@ func Connect(debug string, dbUrls string) { Logger: logger.Default.LogMode(logger.Info), }) } else { - DB, err = gorm.Open(postgres.Open(dbUrls), &gorm.Config{}) + DB, err = gorm.Open(postgres.Open(dbUrls), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) } if err != nil { diff --git a/vitty-backend-api/internal/models/slots.go b/vitty-backend-api/internal/models/slots.go index 72dbb9d..ffdfcdd 100644 --- a/vitty-backend-api/internal/models/slots.go +++ b/vitty-backend-api/internal/models/slots.go @@ -1,5 +1,9 @@ package models +var TimetableSlots = []string{"A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2", "E1", "E2", "F1", "F2", "G1", "G2", + "TA1", "TA2", "TAA1", "TAA2", "TB1", "TB2", "TBB2", "TC1", "TC2", "TCC1", "TCC2", "TD1", "TD2", "TDD2", "TE1", "TE2", "TF1", "TF2", + "TG1", "TG2", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11", "W21", "W22", "X11", "X12", "X21", "Y11", "Y12", "Y21", "Z21"} + var DailySlots = map[string]map[string][]string{ "Monday": { "Theory": {"A1", "F1", "D1", "TB1", "TG1", "A2", "F2", "D2", "TB2", "TG2", "V3"}, From 6d2e84cb6b66d0c349fd0a750aa6b0837aeb8f4b Mon Sep 17 00:00:00 2001 From: TheSilentSage <83112995+TheSilentSage@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:35:43 +0530 Subject: [PATCH 3/7] feat: users can save course-wise notes * fix: merged slot for lab while fetching timetable * feat: users can save course-wise notes --------- Co-authored-by: Dev Keshwani --- vitty-backend-api/api/serializers/notes.go | 19 +++++ vitty-backend-api/api/v2/initialize.go | 1 + vitty-backend-api/api/v2/noteHandler.go | 85 +++++++++++++++++++ vitty-backend-api/go.mod | 6 +- vitty-backend-api/go.sum | 12 +++ .../internal/models/initialize.go | 1 + vitty-backend-api/internal/models/notes.go | 41 +++++++++ vitty-backend-api/internal/utils/users.go | 10 +++ 8 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 vitty-backend-api/api/serializers/notes.go create mode 100644 vitty-backend-api/api/v2/noteHandler.go create mode 100644 vitty-backend-api/internal/models/notes.go diff --git a/vitty-backend-api/api/serializers/notes.go b/vitty-backend-api/api/serializers/notes.go new file mode 100644 index 0000000..549f69a --- /dev/null +++ b/vitty-backend-api/api/serializers/notes.go @@ -0,0 +1,19 @@ +package serializers + +import "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + +func NotesSerializer(notes []models.Notes) []map[string]interface{} { + var result []map[string]interface{} + + for _, note := range notes { + out := map[string]interface{}{ + "note_id": note.NoteID, + "note_name": note.NoteName, + "note_content": note.NoteContent, + "course_id": note.CourseID, + "course_name": note.Courses.CourseName, + } + result = append(result, out) + } + return result +} diff --git a/vitty-backend-api/api/v2/initialize.go b/vitty-backend-api/api/v2/initialize.go index 256c0ab..2d14391 100644 --- a/vitty-backend-api/api/v2/initialize.go +++ b/vitty-backend-api/api/v2/initialize.go @@ -10,4 +10,5 @@ func V2Handler(api fiber.Router) { userHandler(group) timetableHandler(group) friendHandler(group) + noteHandler(group) } diff --git a/vitty-backend-api/api/v2/noteHandler.go b/vitty-backend-api/api/v2/noteHandler.go new file mode 100644 index 0000000..e1ce5f3 --- /dev/null +++ b/vitty-backend-api/api/v2/noteHandler.go @@ -0,0 +1,85 @@ +package v2 + +import ( + "strings" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/serializers" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/utils" + "github.com/gofiber/fiber/v2" +) + +func noteHandler(api fiber.Router) { + group := api.Group("/notes") + group.Use(middleware.JWTAuthMiddleware) + group.Get("/:courseId", getNotes) + group.Post("/save", saveNote) + group.Delete("/:noteId?", deleteNote) +} + +func getNotes(c *fiber.Ctx) error { + var notes models.Notes + courseId := c.Params("courseId") + notes.CourseID = courseId + notes.UserName = c.Locals("user").(models.User).Username + err, userNotes := notes.GetNotesByCourseId() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Notes fetch failed", + }) + } + + if len(userNotes) == 0 { + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": "No notes to display", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": serializers.NotesSerializer(userNotes), + }) +} + +func saveNote(c *fiber.Ctx) error { + var note models.Notes + + if err := c.BodyParser(¬e); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + }) + } + + if !strings.Contains(note.NoteID, "note_") || len(note.NoteID) < 32 { + note.NoteID = utils.UUIDWithPrefix("note") + } + + note.SaveNote() + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "Note Saved Successfully", + }) +} + +func deleteNote(c *fiber.Ctx) error { + var note models.Notes + + noteId := c.Params("noteId") + if noteId == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Note id is missing", + }) + } + + note.NoteID = noteId + note.UserName = c.Locals("user").(models.User).Username + + err := note.DeleteNote() + + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "Note deleted successfully", + }) +} diff --git a/vitty-backend-api/go.mod b/vitty-backend-api/go.mod index 0836a17..baa69a0 100644 --- a/vitty-backend-api/go.mod +++ b/vitty-backend-api/go.mod @@ -6,13 +6,17 @@ require ( firebase.google.com/go v3.13.0+incompatible github.com/gofiber/fiber/v2 v2.46.0 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/google/uuid v1.3.0 + github.com/joho/godotenv v1.5.1 github.com/labstack/echo/v4 v4.11.2 + github.com/labstack/gommon v0.4.0 github.com/urfave/cli/v2 v2.25.6 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/oauth2 v0.11.0 google.golang.org/api v0.138.0 gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 + gorm.io/plugin/soft_delete v1.2.1 ) require ( @@ -30,7 +34,6 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/s2a-go v0.1.5 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -39,7 +42,6 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.16.3 // indirect - github.com/labstack/gommon v0.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect diff --git a/vitty-backend-api/go.sum b/vitty-backend-api/go.sum index f68de46..b72dc6e 100644 --- a/vitty-backend-api/go.sum +++ b/vitty-backend-api/go.sum @@ -96,8 +96,12 @@ github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= 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.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE= @@ -113,6 +117,8 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA= +github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= @@ -301,7 +307,13 @@ 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.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= +gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc= +gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c= +gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ= gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU= +gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk= 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= diff --git a/vitty-backend-api/internal/models/initialize.go b/vitty-backend-api/internal/models/initialize.go index 61f7665..79130e3 100644 --- a/vitty-backend-api/internal/models/initialize.go +++ b/vitty-backend-api/internal/models/initialize.go @@ -11,6 +11,7 @@ func InitializeModels() { "User": &User{}, "Timetable": &Timetable{}, "Friend Requests": &FriendRequest{}, + "Notes": &Notes{}, "Courses": &Courses{}, } diff --git a/vitty-backend-api/internal/models/notes.go b/vitty-backend-api/internal/models/notes.go new file mode 100644 index 0000000..cc4d1d3 --- /dev/null +++ b/vitty-backend-api/internal/models/notes.go @@ -0,0 +1,41 @@ +package models + +import ( + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" + "gorm.io/gorm" +) + +type Notes struct { + NoteID string `json:"note_id,omitempty" gorm:"unique"` + NoteName string `json:"note_name" gorm:"primaryKey"` + UserName string `json:"user_name" gorm:"primaryKey"` + CourseID string `json:"course_id"` + NoteContent string `json:"note_content"` + User User `gorm:"foreignKey:UserName;references:Username;constraint:OnDelete:CASCADE"` + Courses Courses ` gorm:"foreignKey:CourseID;references:CourseId;constraint:OnDelete:CASCADE"` +} + +func (n *Notes) SaveNote() error { + result := database.DB.Save(n) + + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return result.Error +} + +func (n *Notes) DeleteNote() error { + result := database.DB.Where("user_name = ?", n.UserName).Delete(&n) + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return result.Error +} + +func (n *Notes) GetNotesByCourseId() (error, []Notes) { + var notes []Notes + err := database.DB.Where(n).Preload("Courses").Find(¬es).Error + return err, notes +} diff --git a/vitty-backend-api/internal/utils/users.go b/vitty-backend-api/internal/utils/users.go index fab413f..5cc6457 100644 --- a/vitty-backend-api/internal/utils/users.go +++ b/vitty-backend-api/internal/utils/users.go @@ -1,8 +1,11 @@ package utils import ( + "strings" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + "github.com/google/uuid" ) func CheckUserExists(username string) bool { @@ -58,3 +61,10 @@ func ValidateUsername(username string) (bool, string) { return true, "" } + +func UUIDWithPrefix(prefix string) string { + id := uuid.New().String() + id = prefix + "_" + id + id = strings.ReplaceAll(id, "-", "") + return id +} From 50007538dff76ad275066f6b6f530a0636bcec57 Mon Sep 17 00:00:00 2001 From: TheSilentSage <83112995+TheSilentSage@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:42:58 +0530 Subject: [PATCH 4/7] feat: users can add reminders * fix: merged slot for lab while fetching timetable * feat: users can add reminders * feat: added debug configuration for jobs * chore: updated env files --------- Co-authored-by: Dev Keshwani --- .../api/serializers/reminders.go | 18 +++ vitty-backend-api/api/v2/initialize.go | 1 + vitty-backend-api/api/v2/reminderHandler.go | 135 ++++++++++++++++++ vitty-backend-api/cmd/root.go | 14 ++ vitty-backend-api/example.env/example.local | 11 +- .../example.env/example.production | 4 +- vitty-backend-api/internal/jobs/jobs.go | 60 ++++++++ .../internal/models/initialize.go | 1 + .../internal/models/reminders.go | 68 +++++++++ vitty-backend-api/internal/utils/jobs.go | 41 ++++++ 10 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 vitty-backend-api/api/serializers/reminders.go create mode 100644 vitty-backend-api/api/v2/reminderHandler.go create mode 100644 vitty-backend-api/internal/jobs/jobs.go create mode 100644 vitty-backend-api/internal/models/reminders.go create mode 100644 vitty-backend-api/internal/utils/jobs.go diff --git a/vitty-backend-api/api/serializers/reminders.go b/vitty-backend-api/api/serializers/reminders.go new file mode 100644 index 0000000..2d8afa4 --- /dev/null +++ b/vitty-backend-api/api/serializers/reminders.go @@ -0,0 +1,18 @@ +package serializers + +import "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + +func RemindersSerializer(reminders []models.Reminders) []map[string]interface{} { + var result []map[string]interface{} + + for _, reminder := range reminders { + out := map[string]interface{}{ + "reminder_id": reminder.ReminderId, + "reminder_name": reminder.ReminderName, + "reminder_content": reminder.ReminderContent, + "reminder_time": reminder.ReminderTime, + } + result = append(result, out) + } + return result +} diff --git a/vitty-backend-api/api/v2/initialize.go b/vitty-backend-api/api/v2/initialize.go index 2d14391..b542492 100644 --- a/vitty-backend-api/api/v2/initialize.go +++ b/vitty-backend-api/api/v2/initialize.go @@ -10,5 +10,6 @@ func V2Handler(api fiber.Router) { userHandler(group) timetableHandler(group) friendHandler(group) + reminderHandler(group) noteHandler(group) } diff --git a/vitty-backend-api/api/v2/reminderHandler.go b/vitty-backend-api/api/v2/reminderHandler.go new file mode 100644 index 0000000..20092fd --- /dev/null +++ b/vitty-backend-api/api/v2/reminderHandler.go @@ -0,0 +1,135 @@ +package v2 + +import ( + "strings" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/serializers" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/utils" + "github.com/gofiber/fiber/v2" + "github.com/labstack/gommon/log" +) + +func reminderHandler(api fiber.Router) { + group := api.Group("/reminders") + group.Use(middleware.JWTAuthMiddleware) + group.Get("/", getReminders) + group.Post("/", createReminder) + group.Patch("/", updateReminder) + group.Delete("/:reminderId?", deleteReminder) +} + +func getReminders(c *fiber.Ctx) error { + var reminder models.Reminders + + username := c.Locals("user").(models.User).Username + reminder.UserName = username + + err, reminders := reminder.GetReminders() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Reminders not fetched", + }) + } + + if len(reminders) == 0 { + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "No reminders found", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": serializers.RemindersSerializer(reminders), + }) +} + +func createReminder(c *fiber.Ctx) error { + var reminder models.Reminders + + if err := c.BodyParser(&reminder); err != nil { + log.Error(err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body ", + }) + } + + if reminder.ReminderName == nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Reminder name is required", + }) + } + + if reminder.ReminderContent == nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Reminder content is required", + }) + } + + if !strings.Contains(reminder.ReminderId, "rem_") || len(reminder.ReminderId) < 32 { + reminder.ReminderId = utils.UUIDWithPrefix("rem") + } + + username := c.Locals("user").(models.User).Username + reminder.UserName = username + reminder.CreateReminder() + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "Reminder Saved Successfully", + }) +} + +func updateReminder(c *fiber.Ctx) error { + var reminder models.Reminders + + if err := c.BodyParser(&reminder); err != nil { + log.Error(err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body ", + }) + } + + if reminder.ReminderId == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Reminder id is required", + }) + } + + err := reminder.UpdateReminder() + if err != nil { + log.Error(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "Reminder not updated", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "Reminder Saved Successfully", + }) +} + +func deleteReminder(c *fiber.Ctx) error { + var reminder models.Reminders + + reminderId := c.Params("reminderId") + + if reminderId == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "Reminder id is missing", + }) + } + + reminder.ReminderId = reminderId + username := c.Locals("user").(models.User).Username + reminder.UserName = username + + err := reminder.DeleteReminder() + + if err != nil { + log.Info(err.Error()) + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "Reminder deleted successfully", + }) +} diff --git a/vitty-backend-api/cmd/root.go b/vitty-backend-api/cmd/root.go index d1410b4..7ed6e65 100644 --- a/vitty-backend-api/cmd/root.go +++ b/vitty-backend-api/cmd/root.go @@ -8,7 +8,9 @@ import ( vittyCli "github.com/GDGVIT/vitty-backend/vitty-backend-api/cli" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/auth" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/jobs" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/utils" "github.com/gofiber/fiber/v2" "github.com/urfave/cli/v2" ) @@ -37,6 +39,10 @@ type Env struct { google_client_id string google_client_secret string google_redirect_uri string + + // Job Variable + isRunJobs string + jobTime jobs.JobTime } // Method to create a new VittyApp @@ -55,6 +61,11 @@ func (v *VittyApp) setEnv() { v.env.google_client_id = os.Getenv("GOOGLE_CLIENT_ID") v.env.google_client_secret = os.Getenv("GOOGLE_CLIENT_SECRET") v.env.google_redirect_uri = os.Getenv("GOOGLE_REDIRECT_URI") + + //Jobs + v.env.isRunJobs = os.Getenv("RUN_JOBS") + v.env.jobTime = utils.ParseJobTimes() + } // Method to initialize CLI app @@ -94,6 +105,9 @@ func (v *VittyApp) init() { auth.InitializeGoogleOauth(v.env.google_client_id, v.env.google_client_secret, v.env.google_redirect_uri) auth.InitializeFirebaseApp() + // Initialize jobs + jobs.InitializeJobs(v.env.isRunJobs, v.env.debug, v.env.jobTime) + // Initialize Web app v.initWebApp() diff --git a/vitty-backend-api/example.env/example.local b/vitty-backend-api/example.env/example.local index a442b56..3820765 100644 --- a/vitty-backend-api/example.env/example.local +++ b/vitty-backend-api/example.env/example.local @@ -15,4 +15,13 @@ POSTGRES_PORT=5432 # Auth Variables # --------------------------------------------------------------- OAUTH_CALLBACK_URL=http://localhost:3000/auth/callback -JWT_SECRET=secret \ No newline at end of file +JWT_SECRET=secret + +# Job Variables +# --------------------------------------------------------------- +RUN_JOBS=false +#Works only if debug is true +DAILY_JOB_MIN=0 +DAILY_JOB_SEC=10 +WEEKLY_JOB_MIN=0 +WEEKLY_JOB_SEC=20 \ No newline at end of file diff --git a/vitty-backend-api/example.env/example.production b/vitty-backend-api/example.env/example.production index 3a60c3a..1e832d4 100644 --- a/vitty-backend-api/example.env/example.production +++ b/vitty-backend-api/example.env/example.production @@ -15,4 +15,6 @@ POSTGRES_PORT=5432 # Auth Variables # --------------------------------------------------------------- OAUTH_CALLBACK_URL=https:///auth/callback -JWT_SECRET=secret \ No newline at end of file +JWT_SECRET=secret +# --------------------------------------------------------------- +RUN_JOBS=false \ No newline at end of file diff --git a/vitty-backend-api/internal/jobs/jobs.go b/vitty-backend-api/internal/jobs/jobs.go new file mode 100644 index 0000000..c93c717 --- /dev/null +++ b/vitty-backend-api/internal/jobs/jobs.go @@ -0,0 +1,60 @@ +package jobs + +import ( + "log" + "time" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" +) + +type JobTime struct { + DAILY_JOB_MIN int + DAILY_JOB_SEC int + WEEKLY_JOB_MIN int + WEEKLY_JOB_SEC int +} + +func InitializeJobs(isRunJobs string, debug string, jobTimes JobTime) { + if isRunJobs == "true" { + if debug == "true" { + + timeDailyJob := time.Duration(jobTimes.DAILY_JOB_MIN)*time.Minute + time.Duration(jobTimes.DAILY_JOB_SEC)*time.Second + timeWeeklyJob := time.Duration(jobTimes.WEEKLY_JOB_MIN)*time.Minute + time.Duration(jobTimes.WEEKLY_JOB_SEC)*time.Second + go startDailyJob(timeDailyJob) + go startWeeklyJob(timeWeeklyJob) + } else { + go startDailyJob(24 * time.Hour) + go startWeeklyJob(7 * 24 * time.Hour) + } + log.Println("Jobs are running") + } else { + log.Println("Jobs disabled") + } + +} + +var reminder models.Reminders + +func startDailyJob(jobTime time.Duration) { + ticker := time.NewTicker(jobTime) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + reminder.SoftDeleteExpiredReminders() + } + } +} + +func startWeeklyJob(jobTime time.Duration) { + ticker := time.NewTicker(jobTime) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + reminder.CleanupOldReminders() + } + } +} diff --git a/vitty-backend-api/internal/models/initialize.go b/vitty-backend-api/internal/models/initialize.go index 79130e3..a6df1bd 100644 --- a/vitty-backend-api/internal/models/initialize.go +++ b/vitty-backend-api/internal/models/initialize.go @@ -11,6 +11,7 @@ func InitializeModels() { "User": &User{}, "Timetable": &Timetable{}, "Friend Requests": &FriendRequest{}, + "Reminders": &Reminders{}, "Notes": &Notes{}, "Courses": &Courses{}, } diff --git a/vitty-backend-api/internal/models/reminders.go b/vitty-backend-api/internal/models/reminders.go new file mode 100644 index 0000000..e5b8158 --- /dev/null +++ b/vitty-backend-api/internal/models/reminders.go @@ -0,0 +1,68 @@ +package models + +import ( + "time" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" + "gorm.io/gorm" + "gorm.io/plugin/soft_delete" +) + +type Reminders struct { + ReminderId string `json:"reminder_id,omitempty" gorm:"unique"` + ReminderName *string `json:"reminder_name" gorm:"primaryKey"` + UserName string `json:"user_name" gorm:"primaryKey;foreignKey:Username;constraint:OnDelete:CASCADE"` + ReminderContent *string `json:"reminder_content"` + ReminderTime *time.Time `json:"reminder_time,omitempty"` + User User `gorm:"foreignKey:UserName;references:Username;constraint:OnDelete:CASCADE"` + DeletedAt soft_delete.DeletedAt `json:"-,omitempty" gorm:"index"` +} + +func (r *Reminders) CreateReminder() error { + err := database.DB.Create(&r).Error + return err +} + +func (r *Reminders) GetReminders() (error, []Reminders) { + var reminders []Reminders + err := database.DB.Where(&r).Find(&reminders).Error + return err, reminders +} + +func (r *Reminders) UpdateReminder() error { + updateInteface := make(map[string]interface{}) + + if r.ReminderName != nil { + updateInteface["reminder_name"] = *r.ReminderName + } + + if r.ReminderContent != nil { + updateInteface["reminder_content"] = *r.ReminderContent + } + + if r.ReminderTime != nil { + updateInteface["reminder_time"] = *r.ReminderTime + } + + err := database.DB.Model(&Reminders{}).Where("reminder_id = ?", r.ReminderId).UpdateColumns(updateInteface).Error + + return err +} + +func (r *Reminders) DeleteReminder() error { + result := database.DB.Where("reminder_id = ? AND username like ?", r.ReminderId, r.UserName).Delete(&r) + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return result.Error +} + +func (r *Reminders) SoftDeleteExpiredReminders() { + now := time.Now() + database.DB.Model(&Reminders{}).Where("reminder_time < ?", now).Delete(&Reminders{}) +} + +func (r *Reminders) CleanupOldReminders() { + database.DB.Unscoped().Where("deleted_at < NOW() - INTERVAL '7 days'").Delete(&Reminders{}) +} diff --git a/vitty-backend-api/internal/utils/jobs.go b/vitty-backend-api/internal/utils/jobs.go new file mode 100644 index 0000000..7ddec52 --- /dev/null +++ b/vitty-backend-api/internal/utils/jobs.go @@ -0,0 +1,41 @@ +package utils + +import ( + "log" + "os" + "strconv" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/jobs" +) + +func ParseJobTimes() jobs.JobTime { + var err error + var jobTime jobs.JobTime + + jobTime.DAILY_JOB_MIN, err = strconv.Atoi(os.Getenv("DAILY_JOB_MIN")) + + if err != nil { + log.Fatal(err, "JOB_DAILY_MIN_PARSE_ERR") + } + + jobTime.DAILY_JOB_SEC, err = strconv.Atoi(os.Getenv("DAILY_JOB_SEC")) + + if err != nil { + log.Fatal(err, "JOB_DAILY_SEC_PARSE_ERR") + } + + jobTime.WEEKLY_JOB_MIN, err = strconv.Atoi(os.Getenv("WEEKLY_JOB_MIN")) + + if err != nil { + log.Fatal(err, "JOB_WEEKLY_MIN_PARSE_ERR") + } + + jobTime.WEEKLY_JOB_SEC, err = strconv.Atoi(os.Getenv("WEEKLY_JOB_SEC")) + + if err != nil { + log.Fatal(err, "JOB_WEEKLY_SEC_PARSE_ERR") + } + + log.Println("Parse Done") + return jobTime +} From eee88e4e232b770ba98b463ce5e067850f901148 Mon Sep 17 00:00:00 2001 From: TheSilentSage <83112995+TheSilentSage@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:44:35 +0530 Subject: [PATCH 5/7] feat: added option for users to hide timetable from friends * fix: merged slot for lab while fetching timetable * docs: updated postman documentation * feat: added option for users to hide timetable from friends --------- Co-authored-by: Dev Keshwani --- vitty-backend-api/api/v2/friendHandler.go | 37 ++++++++++++- vitty-backend-api/api/v2/userHandler.go | 24 ++++++++- .../internal/models/initialize.go | 1 + .../internal/models/userFriends.go | 10 ++++ vitty-backend-api/internal/models/users.go | 52 +++++++++++++++++-- 5 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 vitty-backend-api/internal/models/userFriends.go diff --git a/vitty-backend-api/api/v2/friendHandler.go b/vitty-backend-api/api/v2/friendHandler.go index e4e3ace..fe412dd 100644 --- a/vitty-backend-api/api/v2/friendHandler.go +++ b/vitty-backend-api/api/v2/friendHandler.go @@ -1,6 +1,8 @@ package v2 import ( + "log" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/serializers" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" @@ -22,8 +24,9 @@ func friendHandler(api fiber.Router) { friendGroup.Use(middleware.JWTAuthMiddleware) friendGroup.Get("/:username", getFriends) friendGroup.Delete("/:username", removeFriend) + friendGroup.Post("/ghost/:username", becomeGhost) + friendGroup.Post("/alive/:username", becomeAlive) } - func getFriendRequests(c *fiber.Ctx) error { request_user := c.Locals("user").(models.User) return c.Status(fiber.StatusOK).JSON(serializers.FriendRequestsSerializer(request_user.GetFriendRequests(), request_user)) @@ -205,3 +208,35 @@ func removeFriend(c *fiber.Ctx) error { "detail": "Friend removed successfully", }) } + +func becomeGhost(c *fiber.Ctx) error { + request_user := c.Locals("user").(models.User) + username := c.Params("username") + + err := request_user.BecomeGost(username) + + if err != nil { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": "profile hidden", + }) +} + +func becomeAlive(c *fiber.Ctx) error { + request_user := c.Locals("user").(models.User) + username := c.Params("username") + + err := request_user.BecomeAlive(username) + + if err != nil { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": "profile visible", + }) +} diff --git a/vitty-backend-api/api/v2/userHandler.go b/vitty-backend-api/api/v2/userHandler.go index a0a5a76..52e2694 100644 --- a/vitty-backend-api/api/v2/userHandler.go +++ b/vitty-backend-api/api/v2/userHandler.go @@ -1,7 +1,9 @@ package v2 import ( + "errors" "fmt" + "log" "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/serializers" @@ -9,6 +11,7 @@ import ( "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/utils" "github.com/gofiber/fiber/v2" + "gorm.io/gorm" ) func userHandler(api fiber.Router) { @@ -60,10 +63,29 @@ func getUser(c *fiber.Ctx) error { fmt.Println("Friends", user.IsFriendsWith(request_user)) if (user.Username == request_user.Username) || - (user.IsFriendsWith(request_user)) || (request_user.Role == "admin") { return c.Status(fiber.StatusOK).JSON(serializers.UserSerializer(user, request_user)) } + + if user.IsFriendsWith(request_user) { + err, isGhosted := request_user.IsGhosted(username) + + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) + + } + + if isGhosted || errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "code": "1811", + "error": "", + }) + } + + return c.Status(fiber.StatusOK).JSON(serializers.UserSerializer(user, request_user)) + } + return c.Status(fiber.StatusOK).JSON(serializers.UserCardSerializer(user, request_user)) } diff --git a/vitty-backend-api/internal/models/initialize.go b/vitty-backend-api/internal/models/initialize.go index a6df1bd..58650be 100644 --- a/vitty-backend-api/internal/models/initialize.go +++ b/vitty-backend-api/internal/models/initialize.go @@ -9,6 +9,7 @@ import ( func InitializeModels() { MODELS := map[string]interface{}{ "User": &User{}, + "UserFriends": &UserFriends{}, "Timetable": &Timetable{}, "Friend Requests": &FriendRequest{}, "Reminders": &Reminders{}, diff --git a/vitty-backend-api/internal/models/userFriends.go b/vitty-backend-api/internal/models/userFriends.go new file mode 100644 index 0000000..504697a --- /dev/null +++ b/vitty-backend-api/internal/models/userFriends.go @@ -0,0 +1,10 @@ +package models + +import "time" + +type UserFriends struct { + UserUsername string `gorm:"primaryKey"` + FriendUsername string `gorm:"primaryKey"` + Hide bool `gorm:"default:false"` + UpdatedAt *time.Time `gorm:"default:CURRENT_TIMESTAMP"` +} diff --git a/vitty-backend-api/internal/models/users.go b/vitty-backend-api/internal/models/users.go index 65621dd..e9bf7fe 100644 --- a/vitty-backend-api/internal/models/users.go +++ b/vitty-backend-api/internal/models/users.go @@ -27,11 +27,8 @@ func (u *User) GetCurrentStatus() map[string]interface{} { now := time.Now() currTime := time.Date(0, 1, 1, now.Hour(), now.Minute(), now.Second(), 0, time.Local) // Remove date part - fmt.Println("Current time: ", currTime) daySlots := u.GetTimeTable().GetDaySlots(time.Now().Weekday()) - fmt.Println("Day slots: ", daySlots) for _, slot := range daySlots[time.Now().Weekday().String()] { - fmt.Println("Slot: ", slot) if slot.StartTime.Before(currTime) && slot.EndTime.After(currTime) { return map[string]interface{}{ "status": "class", @@ -65,6 +62,55 @@ func (u *User) IsFriendsWith(user User) bool { return count != 0 } +func (u *User) BecomeGost(friendUserName string) error { + var userFriend UserFriends + + userFriend.UserUsername = u.Username + userFriend.FriendUsername = friendUserName + + err := database.DB.Model(&UserFriends{}).Where(&userFriend).UpdateColumn("hide", true).Error + return err +} + +func (u *User) BecomeAlive(friendUserName string) error { + var userFriend UserFriends + userFriend.UserUsername = u.Username + userFriend.FriendUsername = friendUserName + userFriend.Hide = false + + err := database.DB.Model(&UserFriends{}).Where(&userFriend).UpdateColumn("hide", false).Error + return err +} + +func (u *User) IsGhosted(friendUserName string) (error, bool) { + var userFriend UserFriends + + userFriend.UserUsername = u.Username + userFriend.FriendUsername = friendUserName + + threshold := time.Now().Add(-8*time.Hour - 48*time.Minute) + + fmt.Println(threshold) + + err := database.DB.Model(&UserFriends{}).Where("updated_at < ?", threshold).First(&userFriend).Error + + if err != nil { + return err, false + } + + if userFriend.Hide { + return nil, userFriend.Hide + } + + userFriend.UserUsername = friendUserName + userFriend.FriendUsername = u.Username + userFriend.UpdatedAt = nil + + err = database.DB.Model(&UserFriends{}).Where(&userFriend).First(&userFriend).Error + + return err, userFriend.Hide +} + func (u *User) HasSentFriendRequest(user User) bool { var count int64 database.DB.Model(&FriendRequest{}).Where("from_username = ? AND to_username = ?", u.Username, user.Username).Count(&count) From 6b6d05e5564ab0c1e06928f508cfec38c8711131 Mon Sep 17 00:00:00 2001 From: TheSilentSage <83112995+TheSilentSage@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:49:02 +0530 Subject: [PATCH 6/7] feat: users can form circles and find leisure timings * fix: merged slot for lab while fetching timetable * feat: users can form circles and find leisure timings --------- Co-authored-by: Dev Keshwani --- vitty-backend-api/api/serializers/circles.go | 52 ++ vitty-backend-api/api/v2/circleHandler.go | 534 ++++++++++++++++++ vitty-backend-api/api/v2/initialize.go | 1 + .../internal/database/initialize.go | 7 +- .../internal/models/circleRequest.go | 88 +++ vitty-backend-api/internal/models/circles.go | 161 ++++++ .../internal/models/initialize.go | 3 + .../internal/models/user-circles.go | 137 +++++ 8 files changed, 981 insertions(+), 2 deletions(-) create mode 100644 vitty-backend-api/api/serializers/circles.go create mode 100644 vitty-backend-api/api/v2/circleHandler.go create mode 100644 vitty-backend-api/internal/models/circleRequest.go create mode 100644 vitty-backend-api/internal/models/circles.go create mode 100644 vitty-backend-api/internal/models/user-circles.go diff --git a/vitty-backend-api/api/serializers/circles.go b/vitty-backend-api/api/serializers/circles.go new file mode 100644 index 0000000..6df33ba --- /dev/null +++ b/vitty-backend-api/api/serializers/circles.go @@ -0,0 +1,52 @@ +package serializers + +import "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + +func CirclesListSerializer(ucj []models.UsersCirclesJoin) []map[string]interface{} { + var result []map[string]interface{} + + for _, userCircle := range ucj { + + out := map[string]interface{}{ + "circle_id": userCircle.CID, + "circle_role": userCircle.CircleRole, + "circle_name": userCircle.Circles.CircleName, + } + result = append(result, out) + } + + return result + +} + +func CircleRequestsSerializer(circleRequests []models.CircleRequest) []map[string]interface{} { + var result []map[string]interface{} + + for _, circleRequest := range circleRequests { + out := map[string]interface{}{ + "from_username": circleRequest.FromUsername, + "to_username": circleRequest.ToUsername, + "circle_id": circleRequest.CID, + "circle_name": circleRequest.Circles.CircleName, + } + result = append(result, out) + } + + return result +} + +func UsersListCircleSerializer(users []models.User) []map[string]interface{} { + var result []map[string]interface{} + + for _, user := range users { + out := map[string]interface{}{ + "username": user.Username, + "name": user.Name, + "picture": user.Picture, + "email": user.Email, + } + result = append(result, out) + } + + return result +} diff --git a/vitty-backend-api/api/v2/circleHandler.go b/vitty-backend-api/api/v2/circleHandler.go new file mode 100644 index 0000000..76336aa --- /dev/null +++ b/vitty-backend-api/api/v2/circleHandler.go @@ -0,0 +1,534 @@ +package v2 + +import ( + "errors" + "log" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/serializers" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/models" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/utils" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func circleHandler(api fiber.Router) { + group := api.Group("/circles") + group.Use(middleware.JWTAuthMiddleware) + group.Get("/", getCircles) + group.Get("/:circleId", getUsersOfCircle) + group.Get("/leisure/:circleId", getLeisureTime) + group.Get("/requests/received", getReceivedCircleRequests) + group.Get("/requests/sent", getSentCircleRequests) + group.Post("/create/:circleName", createCircle) + group.Post("/sendRequest/:circleId/:username", sendCircleRequestToUser) + group.Post("/acceptRequest/:circleId", acceptCircleRequest) + group.Post("/declineRequest/:circleId", declineCircleRequest) + group.Patch("/", updateCircleName) + group.Delete("/:circleId", deleteCircle) + group.Delete("/remove/:circleId/:username", removeUserFromCircle) + group.Delete("/leave/:circleId", leaveCircle) + group.Delete("/unsendRequest/:circleId/:username", unsendCircleRequestToUser) +} + +func getCircles(c *fiber.Ctx) error { + var user_circle models.UsersCirclesJoin + + username := c.Locals("user").(models.User).Username + + user_circle.Uname = username + + err, circles := user_circle.GetCirclesofUser() + + if err != nil { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "circles not fetched", + }) + } + + if len(circles) == 0 { + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": "You are not part of any circle", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": serializers.CirclesListSerializer(circles), + }) +} + +func getUsersOfCircle(c *fiber.Ctx) error { + var userCircle models.UsersCirclesJoin + + circleId := c.Params("circleId") + username := c.Locals("user").(models.User).Username + + userCircle.CID = circleId + userCircle.Uname = username + err, isPartOfCircle := userCircle.IsUserOfCircle() + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "invalid circle id", + }) + } + + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "user authorization could not be verified", + }) + } + + if isPartOfCircle { + userCircle.Uname = "" + err, users := userCircle.GetUsersofCircle() + + if err == nil { + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": serializers.UsersListCircleSerializer(users), + }) + } + } + + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "users list fetch failed", + }) +} + +func getLeisureTime(c *fiber.Ctx) error { + var circle models.Circles + + circleId := c.Params("circleId") + + circle.CircleId = circleId + + err, circleSlotMap := circle.GetCircleSlots() + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "invalid circle id", + }) + } + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "leisure times couldn't be fetched", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": circleSlotMap, + }) + +} + +func getReceivedCircleRequests(c *fiber.Ctx) error { + var circleRequest models.CircleRequest + + username := c.Locals("user").(models.User).Username + + err, circleRequests := circleRequest.GetReceivedRequests(username) + + if err != nil { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "requests not able to be fetched", + }) + } + + if len(circleRequests) == 0 { + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": "your inbox is empty", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": serializers.CircleRequestsSerializer(circleRequests), + }) + +} + +func getSentCircleRequests(c *fiber.Ctx) error { + var circleRequest models.CircleRequest + + username := c.Locals("user").(models.User).Username + + err, circleRequests := circleRequest.GetSentRequests(username) + + if err != nil { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "requests not able to be fetched", + }) + } + + if len(circleRequests) == 0 { + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": "your outbox is empty", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": serializers.CircleRequestsSerializer(circleRequests), + }) + +} + +func createCircle(c *fiber.Ctx) error { + var circle models.Circles + var user_circle models.UsersCirclesJoin + + circleName := c.Params("circleName") + username := c.Locals("user").(models.User).Username + + if circleName == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } else { + circle.CircleId = utils.UUIDWithPrefix("circle") + circle.CircleName = circleName + circle.Uname = username + } + + err := circle.CreateCircle() + if err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return c.Status(fiber.StatusConflict).JSON(fiber.Map{ + "detail": "circle name already exists", + }) + } + + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "circle creation failed", + }) + } + + request_user := c.Locals("user").(models.User) + + user_circle.CID = circle.CircleId + user_circle.CircleRole = "admin" + user_circle.Uname = request_user.Username + + err = user_circle.AddUserToCircle() + + if err != nil { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "user error circle creation failed", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "circle created successfully", + }) +} + +func sendCircleRequestToUser(c *fiber.Ctx) error { + var circleRequest models.CircleRequest + var usersCirclesJoin models.UsersCirclesJoin + + circleId := c.Params("circleId") + from_user := c.Locals("user").(models.User).Username + to_user := c.Params("username") + + usersCirclesJoin.CID = circleId + usersCirclesJoin.Uname = from_user + + err, isCircleAdmin := usersCirclesJoin.IsUserCircleAdmin() + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "circle does not exist", + }) + } + + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "circle search failed", + }) + } + + if !isCircleAdmin { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "detail": "you cannot send requests", + }) + } + + circleRequest.CID = circleId + circleRequest.FromUsername = from_user + circleRequest.ToUsername = to_user + + err = circleRequest.CreateRequest() + if err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "request pending", + }) + } + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "request not sent", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "request sent successfully", + }) +} + +func acceptCircleRequest(c *fiber.Ctx) error { + var circleRequest models.CircleRequest + + circleId := c.Params("circleId") + to_user := c.Locals("user").(models.User).Username + + circleRequest.CID = circleId + circleRequest.ToUsername = to_user + + err := circleRequest.AcceptRequest() + + if err != nil { + + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "you do not have any requests to accept", + }) + } + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "request not accepted", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "request accepted successfully", + }) +} + +func updateCircleName(c *fiber.Ctx) error { + var circle models.Circles + + err := c.BodyParser(&circle) + if err != nil { + log.Println(err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + + err = circle.UpdateCircleName(circle.CircleName) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "circle name not updated", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "circle name updated", + }) +} + +func deleteCircle(c *fiber.Ctx) error { + var circle models.Circles + + circleId := c.Params("circleId") + circle.CircleId = circleId + + err := circle.DeleteCircle() + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "circle was not deleted", + }) + } + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "circle deleted successfully", + }) +} + +func removeUserFromCircle(c *fiber.Ctx) error { + var ucj models.UsersCirclesJoin + + circleId := c.Params("circleId") + username := c.Params("username") + requestUser := c.Locals("user").(models.User) + + ucj.CID = circleId + ucj.Uname = requestUser.Username + + err, isCircleAdmin := ucj.IsUserCircleAdmin() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "circle does not exist", + }) + } + + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "circle search failed", + }) + } + + if !isCircleAdmin { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "detail": "you cannot remove user", + }) + } + + ucj.Uname = username + err = ucj.DeleteUserFromCircle() + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "user not removed from the circle", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "user removed from circle", + }) +} + +func leaveCircle(c *fiber.Ctx) error { + var ucj models.UsersCirclesJoin + + circleId := c.Params("circleId") + username := c.Locals("user").(models.User).Username + + ucj.CID = circleId + ucj.Uname = username + + err, isCircleAdmin := ucj.IsUserCircleAdmin() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "circle does not exist", + }) + } + + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "circle search failed", + }) + } + + err = ucj.DeleteUserFromCircle() + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "you didn't leave the circle", + }) + } + + if isCircleAdmin { + ucj.Uname = "" + err, users := ucj.GetUsersofCircle() + if err != nil { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "you didn't leave the circle;admin error", + }) + } + + if len(users) != 0 { + var circle models.Circles + circle.CircleId = circleId + err = circle.UpdateCircleUsername(users[0].Username) + if err != nil { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "you didn't leave the circle;admin update error", + }) + } + ucj.Uname = users[0].Username + err = ucj.UpdateUserCircleRole("admin") + if err != nil { + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "you didn't leave the circle;admin update error", + }) + } + } + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "you left the circle ", + }) +} + +func unsendCircleRequestToUser(c *fiber.Ctx) error { + var circleRequest models.CircleRequest + + circleId := c.Params("circleId") + from_user := c.Locals("user").(models.User).Username + to_user := c.Params("username") + + circleRequest.CID = circleId + circleRequest.FromUsername = from_user + circleRequest.ToUsername = to_user + + err := circleRequest.DeleteRequest() + + if err != nil { + + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "request could not be unsent", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "request unsent successfully", + }) +} + +func declineCircleRequest(c *fiber.Ctx) error { + var circleRequest models.CircleRequest + + circleId := c.Params("circleId") + to_user := c.Locals("user").(models.User).Username + + circleRequest.CID = circleId + circleRequest.ToUsername = to_user + + err := circleRequest.DeclineRequest() + if err != nil { + + if errors.Is(err, gorm.ErrRecordNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.ErrBadRequest) + } + + log.Println(err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "detail": "request not declined", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "detail": "request declined successfully", + }) +} diff --git a/vitty-backend-api/api/v2/initialize.go b/vitty-backend-api/api/v2/initialize.go index b542492..b2ebc28 100644 --- a/vitty-backend-api/api/v2/initialize.go +++ b/vitty-backend-api/api/v2/initialize.go @@ -10,6 +10,7 @@ func V2Handler(api fiber.Router) { userHandler(group) timetableHandler(group) friendHandler(group) + circleHandler(group) reminderHandler(group) noteHandler(group) } diff --git a/vitty-backend-api/internal/database/initialize.go b/vitty-backend-api/internal/database/initialize.go index 4a4d834..1ff54cd 100644 --- a/vitty-backend-api/internal/database/initialize.go +++ b/vitty-backend-api/internal/database/initialize.go @@ -15,10 +15,13 @@ func Connect(debug string, dbUrls string) { if debug == "true" { DB, err = gorm.Open(postgres.Open(dbUrls), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), + Logger: logger.Default.LogMode(logger.Info), + TranslateError: true, }) } else { - DB, err = gorm.Open(postgres.Open(dbUrls), &gorm.Config{}) + DB, err = gorm.Open(postgres.Open(dbUrls), &gorm.Config{ + TranslateError: true, + }) } if err != nil { diff --git a/vitty-backend-api/internal/models/circleRequest.go b/vitty-backend-api/internal/models/circleRequest.go new file mode 100644 index 0000000..6588e23 --- /dev/null +++ b/vitty-backend-api/internal/models/circleRequest.go @@ -0,0 +1,88 @@ +package models + +import ( + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" + "gorm.io/gorm" +) + +type CircleRequest struct { + FromUsername string `gorm:"primaryKey"` + ToUsername string `gorm:"primaryKey"` + CID string `gorm:"primaryKey"` + From User `gorm:"constraint:OnDelete:CASCADE;foreignKey:FromUsername;references:Username"` + To User `gorm:"constraint:OnDelete:CASCADE;foreignKey:ToUsername;references:Username"` + Circles Circles `gorm:"constraint:OnDelete:CASCADE;foreignKey:CID;references:CircleId;constraint:OnDelete:CASCADE"` +} + +func (cr *CircleRequest) CreateRequest() error { + err := database.DB.Create(&cr).Error + return err +} + +func (cr *CircleRequest) AcceptRequest() error { + var userCircle UsersCirclesJoin + + userCircle.CID = cr.CID + userCircle.CircleRole = "member" + userCircle.Uname = cr.ToUsername + + err, circleRequest := cr.GetRequestByCircleId() + + if circleRequest.ToUsername == "" { + return gorm.ErrRecordNotFound + } else if err != nil { + return err + } + + err = userCircle.AddUserToCircle() + + if err != nil { + return err + } + + err = cr.DeleteRequest() + + if err != nil { + return err + } + + return nil + +} + +func (cr *CircleRequest) DeclineRequest() error { + result := database.DB.Where(cr).Delete(&CircleRequest{}) + + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return result.Error +} + +func (cr *CircleRequest) GetRequestByCircleId() (error, CircleRequest) { + var circleRequest CircleRequest + err := database.DB.Where(cr).Find(&circleRequest).Error + return err, circleRequest +} + +func (cr *CircleRequest) GetReceivedRequests(username string) (error, []CircleRequest) { + var circleRequests []CircleRequest + err := database.DB.Where("to_username = ?", username).Preload("Circles").Find(&circleRequests).Error + return err, circleRequests +} + +func (cr *CircleRequest) GetSentRequests(username string) (error, []CircleRequest) { + var circleRequests []CircleRequest + err := database.DB.Where("from_username = ?", username).Preload("Circles").Find(&circleRequests).Error + return err, circleRequests +} + +func (cr *CircleRequest) DeleteRequest() error { + result := database.DB.Where(&cr).Delete(&CircleRequest{}) + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return result.Error +} diff --git a/vitty-backend-api/internal/models/circles.go b/vitty-backend-api/internal/models/circles.go new file mode 100644 index 0000000..ff8763f --- /dev/null +++ b/vitty-backend-api/internal/models/circles.go @@ -0,0 +1,161 @@ +package models + +import ( + "encoding/json" + "log" + "strings" + "time" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" + "gorm.io/gorm" +) + +type Circles struct { + CircleId string `json:"circle_id" gorm:"unique"` + CircleName string `json:"circle_name" gorm:"primaryKey"` + Uname string `json:"omitempty" gorm:"primaryKey"` + CircleSlots string + User User `gorm:"foreignKey:Uname;references:Username;"` +} + +func (c *Circles) CreateCircle() error { + err := database.DB.Create(c).Error + return err +} + +func (c *Circles) GetCircleByCircleId() error { + err := database.DB.Where(c).First(&c).Error + return err +} + +func (c *Circles) GetCircleSlots() (error, map[string]string) { + err := database.DB.Where(c).First(&c).Error + return err, jsonToCircleSlots(c.CircleSlots) +} + +func (c *Circles) UpdateCircleName(circleName string) error { + result := database.DB.Model(&Circles{}).Where("circle_id like ?", c.CircleId).Update("circle_name", circleName) + + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return result.Error +} + +func (c *Circles) UpdateCircleUsername(username string) error { + err := database.DB.Model(&Circles{}).Where(c).Update("uname", username).Error + return err +} + +func (c *Circles) updateCircleSlots(circleSlotMap map[string]string) error { + jsonString := circleSlotsToJson(circleSlotMap) + err := database.DB.Model(&Circles{}).Where(c).Update("circle_slots", jsonString).Error + return err +} + +func (c *Circles) DeleteCircle() error { + result := database.DB.Where(&c).Delete(&Circles{}) + + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return result.Error +} + +func (c *Circles) ComputeCircleSlots(user User, recompute bool) error { + + resultMap := make(map[string]string) + dayWiseSlots := user.GetTimeTable().GetDaywiseTimetable() + + var err error + var circleSlotMap map[string]string + + if !recompute { + err, circleSlotMap = c.GetCircleSlots() + + if err != nil { + return err + } + } + + for day := time.Monday; day <= time.Friday; day++ { + var result []string + var dupEightSlot bool + + referenceTable := make(map[string]struct{}) + + for _, slot := range dayWiseSlots[day.String()] { + referenceTime := slot.StartTime.Format("15:04") + referenceTable[referenceTime] = struct{}{} + } + + timings := append(TheoryTimings, LabTimings...) + + if c.CircleSlots == "" { + for _, timing := range timings { + timeOfTiming := strings.Split(timing.StartTime, "T")[1] + if _, found := referenceTable[timeOfTiming]; !found { + if strings.Contains(timeOfTiming, "08:00") { + if dupEightSlot { + continue + } + dupEightSlot = true + } + + result = append(result, timeOfTiming) + } + } + + } else { + + timings := strings.Split(circleSlotMap[day.String()], ",") + + for _, timing := range timings { + if _, found := referenceTable[timing]; !found { + if strings.Contains(timing, "08:00") { + if dupEightSlot { + continue + } + dupEightSlot = true + } + + result = append(result, timing) + } + } + + } + resultMap[day.String()] = strings.Join(result, ",") + + } + + c.updateCircleSlots(resultMap) + + return nil + +} + +// Helper Functions + +func circleSlotsToJson(circleSlotMap map[string]string) string { + jsonBytes, err := json.Marshal(circleSlotMap) + + if err != nil { + log.Println(err) + } + return string(jsonBytes) +} + +func jsonToCircleSlots(jsonString string) map[string]string { + resultMap := make(map[string]string) + if jsonString == "" { + return resultMap + } + + if err := json.Unmarshal([]byte(jsonString), &resultMap); err != nil { + log.Fatal(err) + } + + return resultMap +} diff --git a/vitty-backend-api/internal/models/initialize.go b/vitty-backend-api/internal/models/initialize.go index 58650be..40260a4 100644 --- a/vitty-backend-api/internal/models/initialize.go +++ b/vitty-backend-api/internal/models/initialize.go @@ -12,6 +12,9 @@ func InitializeModels() { "UserFriends": &UserFriends{}, "Timetable": &Timetable{}, "Friend Requests": &FriendRequest{}, + "Circles": &Circles{}, + "UserCirclesJoin": &UsersCirclesJoin{}, + "CircleRequests": &CircleRequest{}, "Reminders": &Reminders{}, "Notes": &Notes{}, "Courses": &Courses{}, diff --git a/vitty-backend-api/internal/models/user-circles.go b/vitty-backend-api/internal/models/user-circles.go new file mode 100644 index 0000000..b7b85ab --- /dev/null +++ b/vitty-backend-api/internal/models/user-circles.go @@ -0,0 +1,137 @@ +package models + +import ( + "log" + + "github.com/GDGVIT/vitty-backend/vitty-backend-api/internal/database" + "gorm.io/gorm" +) + +type UsersCirclesJoin struct { + CID string `gorm:"primaryKey"` + Uname string `gorm:"primaryKey"` + CircleRole string + Circles Circles `gorm:"foreignKey:CID;references:CircleId;constraint:OnDelete:CASCADE"` + User User `gorm:"foreignKey:Uname;references:Username"` +} + +func (ucj *UsersCirclesJoin) AddUserToCircle() error { + err := database.DB.Create(ucj).Error + if err != nil { + return err + } + + var user User + user.Username = ucj.Uname + + err = ucj.Circles.ComputeCircleSlots(user, false) + + return err +} + +func (ucj *UsersCirclesJoin) UpdateUserCircleRole(role string) error { + err := database.DB.Model(&UsersCirclesJoin{}).Where(&ucj).Update("circle_role", role).Error + return err +} + +func (ucj *UsersCirclesJoin) DeleteUserFromCircle() error { + + err := database.DB.Delete(ucj).Error + + if err != nil { + return err + } + + ucj.Uname = "" + err, users := ucj.GetUsersofCircle() + + if err != nil { + return err + } + + if len(users) == 0 { + var circle Circles + circle.CircleId = ucj.CID + + err := circle.DeleteCircle() + return err + } else { + for _, user := range users { + ucj.Circles.CircleSlots = "" + ucj.Circles.CircleId = ucj.CID + err = ucj.Circles.ComputeCircleSlots(user, true) + if err != nil { + return err + } + } + } + + return nil +} + +func (ucj *UsersCirclesJoin) GetCircleofUserByCID() (error, UsersCirclesJoin) { + var circleUser UsersCirclesJoin + + result := database.DB.Where(&ucj).Preload("Circles").Find(&circleUser) + log.Println(result.Error) + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound, circleUser + } + return result.Error, circleUser +} + +func (ucj *UsersCirclesJoin) GetCirclesofUser() (error, []UsersCirclesJoin) { + var circleUsers []UsersCirclesJoin + + err := database.DB.Where(&ucj).Preload("Circles").Find(&circleUsers).Error + if err != nil { + return err, nil + } + + return err, circleUsers +} + +func (ucj *UsersCirclesJoin) GetUsersofCircle() (error, []User) { + var users []User + var circleUsers []UsersCirclesJoin + + err := database.DB.Where(&ucj).Preload("User").Find(&circleUsers).Error + if err != nil { + return err, nil + } + + for _, circleUser := range circleUsers { + users = append(users, circleUser.User) + } + + return err, users +} + +func (ucj *UsersCirclesJoin) IsUserCircleAdmin() (error, bool) { + + err, userCircle := ucj.GetCircleofUserByCID() + + if err != nil { + return err, false + } + + if userCircle.CircleRole == "admin" { + return nil, true + } + + return nil, false +} + +func (ucj *UsersCirclesJoin) IsUserOfCircle() (error, bool) { + + err, userCircle := ucj.GetCircleofUserByCID() + if err != nil { + return err, false + } + + if userCircle.Uname == ucj.Uname { + return nil, true + } + + return nil, false +} From b94aeeb52de50b10e5ebaeb7781558c2ea2a8f96 Mon Sep 17 00:00:00 2001 From: TheSilentSage Date: Sat, 17 May 2025 16:51:54 +0530 Subject: [PATCH 7/7] feat: admin can initialize empty classrooms file --- Dockerfile | 1 + vitty-backend-api/admin/pkg/handler.go | 43 ++++++++++++++----- vitty-backend-api/admin/pkg/service.go | 2 +- vitty-backend-api/api/v2/initialize.go | 2 + vitty-backend-api/api/v2/userHandler.go | 27 ++++++++++-- .../cli/commands/timetableCommands.go | 7 +-- vitty-backend-api/go.mod | 1 + vitty-backend-api/go.sum | 2 + 8 files changed, 66 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index ac58114..ac29354 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN go build -o bin/vitty FROM alpine:3.15 AS runner WORKDIR /usr/src/app +RUN mkdir ./data COPY --from=builder /usr/src/app/bin/vitty ./bin/vitty diff --git a/vitty-backend-api/admin/pkg/handler.go b/vitty-backend-api/admin/pkg/handler.go index 2ecd2bd..217afdb 100644 --- a/vitty-backend-api/admin/pkg/handler.go +++ b/vitty-backend-api/admin/pkg/handler.go @@ -7,7 +7,11 @@ import ( "net/http" "reflect" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" + "github.com/GDGVIT/vitty-backend/vitty-backend-api/cli/commands" + "github.com/gofiber/fiber/v2" "github.com/labstack/echo/v4" + "github.com/urfave/cli/v2" ) type TemplateRenderer struct { @@ -22,17 +26,20 @@ func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c return t.templates.ExecuteTemplate(w, name, data) } -func AdminHandler(app *echo.Echo) { - group := app.Group("") - group.Use(JWTMiddleware) - group.GET("", GetModelsView) - group.GET("/:model", GetModelView) - group.GET("/:model/create", CreateItemView) - group.POST("/:model/create", CreateItem) - group.GET("/:model/:id", GetItemView) - group.GET("/:model/:id/edit", UpdateItemView) - group.PUT("/:model/:id", UpdateItem) - group.DELETE("/:model/:id", DeleteItem) +func AdminHandler(app fiber.Router) { + group := app.Group("/admin") + group.Use(middleware.JWTAuthMiddleware) + group.Use(middleware.IsAdminMiddleware) + group.Post("/empty-classrooms/seed", seedEmptyClassrooms) + + // group.Get("", GetModelsView) + // group.Get("/:model", GetModelView) + // group.Get("/:model/create", CreateItemView) + // group.Post("/:model/create", CreateItem) + // group.Get("/:model/:id", GetItemView) + // group.Get("/:model/:id/edit", UpdateItemView) + // group.Put("/:model/:id", UpdateItem) + // group.Delete("/:model/:id", DeleteItem) } func GetModelsView(c echo.Context) error { @@ -212,4 +219,18 @@ func DeleteItem(c echo.Context) error { return c.JSON(http.StatusOK, map[string]interface{}{ "message": "Item deleted", }) + +} + +func seedEmptyClassrooms(c *fiber.Ctx) error { + err := commands.GenerateEmptyRooms(&cli.Context{}) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "classrooms seed failed", + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "message": "classrooms has been initialized", + }) } diff --git a/vitty-backend-api/admin/pkg/service.go b/vitty-backend-api/admin/pkg/service.go index 738b7f1..f1de54f 100644 --- a/vitty-backend-api/admin/pkg/service.go +++ b/vitty-backend-api/admin/pkg/service.go @@ -42,7 +42,7 @@ func (s *AdminSvc) init() { "message": "Hello World", }) }) - AdminHandler(s.WebApp) + // AdminHandler(s.WebApp) } func (s *AdminSvc) Register(model ModelInterface) { diff --git a/vitty-backend-api/api/v2/initialize.go b/vitty-backend-api/api/v2/initialize.go index 256c0ab..70215ef 100644 --- a/vitty-backend-api/api/v2/initialize.go +++ b/vitty-backend-api/api/v2/initialize.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/GDGVIT/vitty-backend/vitty-backend-api/admin/pkg" "github.com/gofiber/fiber/v2" ) @@ -10,4 +11,5 @@ func V2Handler(api fiber.Router) { userHandler(group) timetableHandler(group) friendHandler(group) + pkg.AdminHandler(group) } diff --git a/vitty-backend-api/api/v2/userHandler.go b/vitty-backend-api/api/v2/userHandler.go index 7997098..3f0bac0 100644 --- a/vitty-backend-api/api/v2/userHandler.go +++ b/vitty-backend-api/api/v2/userHandler.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "strings" "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/middleware" "github.com/GDGVIT/vitty-backend/vitty-backend-api/api/serializers" @@ -20,9 +21,9 @@ func userHandler(api fiber.Router) { group.Get("/", getUsers) group.Get("/search", searchUsers) group.Get("/suggested", getSuggestedUsers) + group.Get("/emptyClassRooms", getEmptyClassRooms) group.Get("/:username", getUser) group.Delete("/:username", deleteUser) - group.Get("/emptyClassRooms", getEmptyClassRooms) } func searchUsers(c *fiber.Ctx) error { @@ -89,14 +90,24 @@ func deleteUser(c *fiber.Ctx) error { } func getEmptyClassRooms(c *fiber.Ctx) error { + filterSlot := strings.ToUpper(c.Query("slot")) + + if filterSlot == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "detail": "please mention the slot", + }) + } + file, err := os.Open("./data/freeClasses.json") if err != nil { log.Printf("Error opening file: %v", err) - return c.Status(fiber.StatusInternalServerError).SendString("Please contact vitty support") + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "please contact vitty support", + }) } defer file.Close() - var freeClasses interface{} + var freeClasses map[string]interface{} decoder := json.NewDecoder(file) err = decoder.Decode(&freeClasses) if err != nil { @@ -106,5 +117,13 @@ func getEmptyClassRooms(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).JSON(fiber.ErrInternalServerError) } - return c.Status(fiber.StatusOK).JSON(freeClasses) + response := freeClasses[filterSlot] + + if response == nil { + response = "" + } + + return c.Status(fiber.StatusOK).JSON(map[string]interface{}{ + filterSlot: response, + }) } diff --git a/vitty-backend-api/cli/commands/timetableCommands.go b/vitty-backend-api/cli/commands/timetableCommands.go index 1ec5265..316e390 100644 --- a/vitty-backend-api/cli/commands/timetableCommands.go +++ b/vitty-backend-api/cli/commands/timetableCommands.go @@ -27,8 +27,8 @@ var TimetableCommands = []*cli.Command{ { Name: "empty-rooms", Aliases: []string{"er"}, - Usage: "Shows empty classrooms", - Action: getEmptyRooms, + Usage: "Generates empty classrooms file", + Action: GenerateEmptyRooms, }, } @@ -82,7 +82,7 @@ func fixSlotTimes(c *cli.Context) error { return nil } -func getEmptyRooms(c *cli.Context) error { +func GenerateEmptyRooms(c *cli.Context) error { reset := "\033[0m" red := "\033[31m" green := "\033[32m" @@ -155,6 +155,7 @@ func getEmptyRooms(c *cli.Context) error { if err != nil { fmt.Println(red, "Failed") fmt.Println("Error: ", err, reset) + return err } fmt.Println(green, "Complete", reset) diff --git a/vitty-backend-api/go.mod b/vitty-backend-api/go.mod index 0836a17..b6caa78 100644 --- a/vitty-backend-api/go.mod +++ b/vitty-backend-api/go.mod @@ -6,6 +6,7 @@ require ( firebase.google.com/go v3.13.0+incompatible github.com/gofiber/fiber/v2 v2.46.0 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/joho/godotenv v1.5.1 github.com/labstack/echo/v4 v4.11.2 github.com/urfave/cli/v2 v2.25.6 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 diff --git a/vitty-backend-api/go.sum b/vitty-backend-api/go.sum index f68de46..14f92ed 100644 --- a/vitty-backend-api/go.sum +++ b/vitty-backend-api/go.sum @@ -98,6 +98,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD 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/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE=