diff --git a/docker-compose.yml b/docker-compose.yml index 968f180..2df09a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,22 +42,25 @@ services: build: dockerfile: Dockerfile context: ./frontend - target: prod + target: dev args: - COMMIT_HASH=${COMMIT_HASH} # open port 8000 + volumes: + - ./frontend/src/:/app/src + - ./frontend/public/:/app/public ports: - "8000:8000" rproxy: - image: docker.io/bitnami/nginx:1.28 + image: docker.io/bitnami/nginx:latest volumes: - ./reverseproxy.local.conf:/opt/bitnami/nginx/conf/nginx.conf depends_on: - htwkalender-data-manager - htwkalender-frontend ports: - - "8080:8080" + - "80:80" volumes: pb_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 5cdcaa3..f8a2c2a 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -39,9 +39,12 @@ RUN echo "COMMIT_HASH=$COMMIT_HASH" >> .env.commit RUN npm install COPY . ./ +# execute the npm run dev command +CMD ["npm", "run", "dev"] + # production stage # https://hub.docker.com/r/bitnami/nginx -> always run as non-root user -FROM docker.io/bitnami/nginx:1.28 AS prod +FROM docker.io/bitnami/nginx:latest AS prod # copy build files from build container COPY ./nginx.conf /opt/bitnami/nginx/conf/nginx.conf diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ce1e327..9951268 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "@vueuse/core": "^13.0.0", "country-flag-emoji-polyfill": "^0.1.8", "pinia": "^3.0.1", + "pocketbase": "^0.26.3", "primeflex": "^3.3.1", "primeicons": "^7.0.0", "primevue": "^3.53.1", @@ -5593,6 +5594,12 @@ "@vue/devtools-kit": "^7.7.2" } }, + "node_modules/pocketbase": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.3.tgz", + "integrity": "sha512-5deUKRoEczpxxuHzwr6/DHVmgbggxylEVig8CKN+MjvtYxPUqX/C6puU0yaR2yhTi8zrh7J9s7Ty+qBGwVzWOQ==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6c62dd3..094e868 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@vueuse/core": "^13.0.0", "country-flag-emoji-polyfill": "^0.1.8", "pinia": "^3.0.1", + "pocketbase": "^0.26.3", "primeflex": "^3.3.1", "primeicons": "^7.0.0", "primevue": "^3.53.1", diff --git a/frontend/src/api/createFeed.ts b/frontend/src/api/createFeed.ts index ce2ba84..414c6d9 100644 --- a/frontend/src/api/createFeed.ts +++ b/frontend/src/api/createFeed.ts @@ -44,6 +44,37 @@ export async function createIndividualFeed(modules: Module[]): Promise { } } +export async function createProfessorFeed(modules: Module[]): Promise { + if (import.meta.env.SSR) { + return ""; + } + try { + // Dynamic import to avoid circular dependencies or issues if pb is not initialized + const { pb } = await import("../service/pocketbase"); + + const response = await fetch("/api/professor/feed", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": pb.authStore.token, + }, + body: JSON.stringify(modules), + }); + + if ( + response.status === 429 || + response.status === 500 || + response.status != 200 + ) { + return Promise.reject(response.statusText); + } + + return await response.json(); + } catch (error) { + return Promise.reject(error); + } +} + interface FeedResponse { modules: FeedModule[]; collectionId: string; diff --git a/frontend/src/api/fetchProfessorModules.ts b/frontend/src/api/fetchProfessorModules.ts new file mode 100644 index 0000000..932cc0e --- /dev/null +++ b/frontend/src/api/fetchProfessorModules.ts @@ -0,0 +1,40 @@ +export interface ApiModule { + uuid: string; + name: string; + course: string; + eventType?: string; + prof: string; + semester: string; + confidenceScore?: number; +} + +export async function fetchProfessorModules(): Promise { + if (import.meta.env.SSR) { + return []; + } + try { + const { pb } = await import("../service/pocketbase"); + + if (!pb.authStore.isValid || !pb.authStore.token) { + return Promise.reject(new Error("User is not authenticated")); + } + + const response = await fetch("/api/professor/modules", { + method: "GET", + headers: { + "Authorization": pb.authStore.token, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const error = new Error(`HTTP error! status: ${response.status}`); + Object.assign(error, { status: response.status }); + throw error; + } + + return await response.json(); + } catch (error) { + return Promise.reject(error); + } +} diff --git a/frontend/src/components/MenuBar.vue b/frontend/src/components/MenuBar.vue index 6e4649d..bcce07f 100644 --- a/frontend/src/components/MenuBar.vue +++ b/frontend/src/components/MenuBar.vue @@ -17,61 +17,117 @@ along with this program. If not, see . --> + + + + diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 719f22f..32ab09b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -79,6 +79,7 @@ export default defineConfig({ watch: { usePolling: true, }, + allowedHosts: ["localhost", "127.0.0.1", "::1", "cal.htwk-leipzig.de", "htwkalender-frontend"], proxy: { "/api": { target: "http://localhost:8090/api", diff --git a/reverseproxy.local.conf b/reverseproxy.local.conf index bb05c34..42a12cf 100644 --- a/reverseproxy.local.conf +++ b/reverseproxy.local.conf @@ -25,8 +25,8 @@ http { limit_req_zone $ratelimit_key zone=createFeed:10m rate=1r/m; server { - listen 8080; - listen [::]:8080; + listen 80; + listen [::]:80; http2 on; location /api/feed { diff --git a/services/common/genproto/modules/feeds.pb.go b/services/common/genproto/modules/feeds.pb.go index 7dfdaf7..28ba68a 100644 --- a/services/common/genproto/modules/feeds.pb.go +++ b/services/common/genproto/modules/feeds.pb.go @@ -31,6 +31,9 @@ type Feed struct { Created string `protobuf:"bytes,4,opt,name=created,proto3" json:"created,omitempty"` Updated string `protobuf:"bytes,5,opt,name=updated,proto3" json:"updated,omitempty"` Deleted bool `protobuf:"varint,6,opt,name=deleted,proto3" json:"deleted,omitempty"` + Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` // "prof" or "student" + User string `protobuf:"bytes,8,opt,name=user,proto3" json:"user,omitempty"` // User ID relation + UserEmail string `protobuf:"bytes,9,opt,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"` // User Email for filtering } func (x *Feed) Reset() { @@ -107,6 +110,27 @@ func (x *Feed) GetDeleted() bool { return false } +func (x *Feed) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Feed) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *Feed) GetUserEmail() string { + if x != nil { + return x.UserEmail + } + return "" +} + type GetFeedRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -204,7 +228,7 @@ func (x *GetFeedResponse) GetFeed() *Feed { var File_feeds_proto protoreflect.FileDescriptor var file_feeds_proto_rawDesc = []byte{ - 0x0a, 0x0b, 0x66, 0x65, 0x65, 0x64, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9c, 0x01, + 0x0a, 0x0b, 0x66, 0x65, 0x65, 0x64, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe3, 0x01, 0x0a, 0x04, 0x46, 0x65, 0x65, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, @@ -214,19 +238,23 @@ var file_feeds_proto_rawDesc = []byte{ 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x20, 0x0a, 0x0e, - 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x2c, - 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x19, 0x0a, 0x04, 0x66, 0x65, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x05, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x52, 0x04, 0x66, 0x65, 0x65, 0x64, 0x32, 0x3d, 0x0a, 0x0b, - 0x46, 0x65, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x47, - 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x12, 0x0f, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x1c, 0x5a, 0x1a, 0x68, - 0x74, 0x77, 0x6b, 0x61, 0x6c, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x75, 0x73, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x45, 0x6d, + 0x61, 0x69, 0x6c, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x2c, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x66, 0x65, 0x65, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x05, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x52, 0x04, 0x66, + 0x65, 0x65, 0x64, 0x32, 0x3d, 0x0a, 0x0b, 0x46, 0x65, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x12, 0x0f, 0x2e, + 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, + 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x42, 0x1c, 0x5a, 0x1a, 0x68, 0x74, 0x77, 0x6b, 0x61, 0x6c, 0x65, 0x6e, 0x64, 0x65, + 0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/services/common/professor/matching.go b/services/common/professor/matching.go new file mode 100644 index 0000000..ec9a761 --- /dev/null +++ b/services/common/professor/matching.go @@ -0,0 +1,175 @@ +package professor + +import ( + "fmt" + "strings" +) + +// ExtractNameFromEmail extracts the first and last name from a professor's email. +// Expected format: firstname.lastname@htwk-leipzig.de +func ExtractNameFromEmail(email string) (firstName, lastName string, err error) { + parts := strings.Split(email, "@") + if len(parts) != 2 { + return "", "", fmt.Errorf("invalid email format") + } + + nameParts := strings.Split(parts[0], ".") + if len(nameParts) < 2 { + return "", "", fmt.Errorf("email does not contain dot separator") + } + + // Extract first and last name + firstName = nameParts[0] + lastName = nameParts[len(nameParts)-1] + + // Capitalize first letter + if len(firstName) > 0 { + firstName = strings.ToUpper(firstName[:1]) + firstName[1:] + } + if len(lastName) > 0 { + lastName = strings.ToUpper(lastName[:1]) + lastName[1:] + } + + return firstName, lastName, nil +} + +// CalculateConfidenceScore returns a score from 0.0 to 1.0 indicating how confident we are +// that this professor string matches the given first and last name +// 1.0 = perfect match (both first and last name exact) +// 0.7-0.9 = good match (last name exact, first name fuzzy or present) +// 0.4-0.6 = possible match (last name fuzzy or partial) +// 0.1-0.3 = weak match (last name substring) +// 0.0 = no match +func CalculateConfidenceScore(profString, firstName, lastName string) float64 { + // Normalize the professor string: remove common titles and split into words + profString = strings.ToLower(profString) + + // Remove common titles + titles := []string{"prof.", "dr.", "arch.", "ing.", "dipl.", "m.sc.", "b.sc.", "ph.d."} + for _, title := range titles { + profString = strings.ReplaceAll(profString, title, "") + } + + // Split by spaces, hyphens, and other separators + words := strings.FieldsFunc(profString, func(r rune) bool { + return r == ' ' || r == '-' || r == ',' || r == '.' + }) + + // Normalize firstName and lastName + firstNameLower := strings.ToLower(firstName) + lastNameLower := strings.ToLower(lastName) + + lastNameExact := false + lastNameFuzzy := false + lastNameSubstring := false + firstNameExact := false + firstNameFuzzy := false + + for _, word := range words { + word = strings.TrimSpace(word) + if word == "" { + continue + } + + // Check last name + if word == lastNameLower { + lastNameExact = true + } else if levenshteinDistance(word, lastNameLower) <= 1 && len(lastNameLower) > 3 { + lastNameFuzzy = true + } else if strings.Contains(word, lastNameLower) || strings.Contains(lastNameLower, word) { + lastNameSubstring = true + } + + // Check first name + if word == firstNameLower { + firstNameExact = true + } else if levenshteinDistance(word, firstNameLower) <= 1 && len(firstNameLower) > 3 { + firstNameFuzzy = true + } + } + + // Calculate confidence score based on matches + score := 0.0 + + if lastNameExact { + if firstNameExact { + score = 1.0 // Perfect match + } else if firstNameFuzzy { + score = 0.9 // Excellent match + } else { + score = 0.8 // Good match (last name exact, no first name match) + } + } else if lastNameFuzzy { + if firstNameExact || firstNameFuzzy { + score = 0.6 // Decent match (fuzzy last name but first name matches) + } else { + score = 0.5 // Medium match (fuzzy last name, no first name) + } + } else if lastNameSubstring { + score = 0.2 // Weak match (substring only) + } + + return score +} + +// MatchesProfessor checks if the professor string matches the given last name (and optional first name) +// It uses a simplified check suitable for filtering events where we want high recall but reasonable precision. +// It returns true if the confidence score is > 0. +func MatchesProfessor(profString, firstName, lastName string) bool { + return CalculateConfidenceScore(profString, firstName, lastName) > 0 +} + +// levenshteinDistance calculates the Levenshtein distance between two strings +func levenshteinDistance(s1, s2 string) int { + if len(s1) == 0 { + return len(s2) + } + if len(s2) == 0 { + return len(s1) + } + + // Create a 2D array for dynamic programming + d := make([][]int, len(s1)+1) + for i := range d { + d[i] = make([]int, len(s2)+1) + } + + // Initialize first column and row + for i := 0; i <= len(s1); i++ { + d[i][0] = i + } + for j := 0; j <= len(s2); j++ { + d[0][j] = j + } + + // Fill the matrix + for i := 1; i <= len(s1); i++ { + for j := 1; j <= len(s2); j++ { + cost := 0 + if s1[i-1] != s2[j-1] { + cost = 1 + } + + d[i][j] = min( + d[i-1][j]+1, // deletion + d[i][j-1]+1, // insertion + d[i-1][j-1]+cost, // substitution + ) + } + } + + return d[len(s1)][len(s2)] +} + +func min(a, b, c int) int { + if a < b { + if a < c { + return a + } + return c + } + if b < c { + return b + } + return c +} diff --git a/services/data-manager/main.go b/services/data-manager/main.go index a5fbcf6..21652e9 100644 --- a/services/data-manager/main.go +++ b/services/data-manager/main.go @@ -17,27 +17,31 @@ package main import ( - "github.com/pocketbase/pocketbase" - "github.com/pocketbase/pocketbase/plugins/migratecmd" _ "htwkalender/data-manager/migrations" "htwkalender/data-manager/model/serviceModel" "htwkalender/data-manager/service" "htwkalender/data-manager/service/events" "htwkalender/data-manager/service/grpc" + "htwkalender/data-manager/service/professor" "log/slog" "os" "strings" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/plugins/migratecmd" ) func setupApp() *pocketbase.PocketBase { app := pocketbase.New() courseService := events.NewPocketBaseCourseService(app) eventService := events.NewPocketBaseEventService(app) + professorService := professor.NewProfessorService(app) services := serviceModel.Service{ - CourseService: courseService, - EventService: eventService, - App: app, + CourseService: courseService, + EventService: eventService, + ProfessorService: professorService, + App: app, } // loosely check if it was executed using "go run" @@ -53,6 +57,7 @@ func setupApp() *pocketbase.PocketBase { }) service.AddRoutes(services) service.AddSchedules(services) + service.AddHooks(app) return app } diff --git a/services/data-manager/migrations/1745249436_enable_oauth2.go b/services/data-manager/migrations/1745249436_enable_oauth2.go new file mode 100644 index 0000000..98a0a62 --- /dev/null +++ b/services/data-manager/migrations/1745249436_enable_oauth2.go @@ -0,0 +1,32 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("users") + if err != nil { + return err + } + + // Enable OAuth2 + collection.OAuth2.Enabled = true + + // Optional: Map fields if necessary, for now just enabling it + // collection.OAuth2.MappedFields.Name = "name" + + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("users") + if err != nil { + return err + } + + collection.OAuth2.Enabled = false + + return app.Save(collection) + }) +} diff --git a/services/data-manager/migrations/1763841597_collections_snapshot.go b/services/data-manager/migrations/1763841597_collections_snapshot.go new file mode 100644 index 0000000..b2ecc78 --- /dev/null +++ b/services/data-manager/migrations/1763841597_collections_snapshot.go @@ -0,0 +1,791 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + jsonData := `[ + { + "createRule": null, + "deleteRule": null, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1582905952", + "max": 0, + "min": 0, + "name": "method", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "id": "pbc_2279338944", + "indexes": [ + "CREATE INDEX ` + "`" + `idx_mfas_collectionRef_recordRef` + "`" + ` ON ` + "`" + `_mfas` + "`" + ` (collectionRef,recordRef)" + ], + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_mfas", + "system": true, + "type": "base", + "updateRule": null, + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId" + }, + { + "createRule": null, + "deleteRule": null, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 8, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 0, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text3866985172", + "max": 0, + "min": 0, + "name": "sentTo", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "id": "pbc_1638494021", + "indexes": [ + "CREATE INDEX ` + "`" + `idx_otps_collectionRef_recordRef` + "`" + ` ON ` + "`" + `_otps` + "`" + ` (collectionRef, recordRef)" + ], + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_otps", + "system": true, + "type": "base", + "updateRule": null, + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId" + }, + { + "createRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2462348188", + "max": 0, + "min": 0, + "name": "provider", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1044722854", + "max": 0, + "min": 0, + "name": "providerId", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "id": "pbc_2281828961", + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_externalAuths_record_provider` + "`" + ` ON ` + "`" + `_externalAuths` + "`" + ` (collectionRef, recordRef, provider)", + "CREATE UNIQUE INDEX ` + "`" + `idx_externalAuths_collection_provider` + "`" + ` ON ` + "`" + `_externalAuths` + "`" + ` (collectionRef, provider, providerId)" + ], + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_externalAuths", + "system": true, + "type": "base", + "updateRule": null, + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId" + }, + { + "createRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4228609354", + "max": 0, + "min": 0, + "name": "fingerprint", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "id": "pbc_4275539003", + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_authOrigins_unique_pairs` + "`" + ` ON ` + "`" + `_authOrigins` + "`" + ` (collectionRef, recordRef, fingerprint)" + ], + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_authOrigins", + "system": true, + "type": "base", + "updateRule": null, + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId" + }, + { + "authAlert": { + "emailTemplate": { + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Login from a new location" + }, + "enabled": true + }, + "authRule": "", + "authToken": { + "duration": 86400 + }, + "confirmEmailChangeTemplate": { + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Confirm your {APP_NAME} new email address" + }, + "createRule": null, + "deleteRule": null, + "emailChangeToken": { + "duration": 1800 + }, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "fileToken": { + "duration": 180 + }, + "id": "pbc_3142635823", + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey_pbc_3142635823` + "`" + ` ON ` + "`" + `_superusers` + "`" + ` (` + "`" + `tokenKey` + "`" + `)", + "CREATE UNIQUE INDEX ` + "`" + `idx_email_pbc_3142635823` + "`" + ` ON ` + "`" + `_superusers` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''" + ], + "listRule": null, + "manageRule": null, + "mfa": { + "duration": 1800, + "enabled": false, + "rule": "" + }, + "name": "_superusers", + "oauth2": { + "enabled": false, + "mappedFields": { + "avatarURL": "", + "id": "", + "name": "", + "username": "" + } + }, + "otp": { + "duration": 180, + "emailTemplate": { + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "OTP for {APP_NAME}" + }, + "enabled": false, + "length": 8 + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "passwordResetToken": { + "duration": 1800 + }, + "resetPasswordTemplate": { + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Reset your {APP_NAME} password" + }, + "system": true, + "type": "auth", + "updateRule": null, + "verificationTemplate": { + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Verify your {APP_NAME} email" + }, + "verificationToken": { + "duration": 259200 + }, + "viewRule": null + }, + { + "authAlert": { + "emailTemplate": { + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Login from a new location" + }, + "enabled": true + }, + "authRule": "", + "authToken": { + "duration": 604800 + }, + "confirmEmailChangeTemplate": { + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Confirm your {APP_NAME} new email address" + }, + "createRule": "", + "deleteRule": "id = @request.auth.id", + "emailChangeToken": { + "duration": 1800 + }, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 255, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "fileToken": { + "duration": 180 + }, + "id": "_pb_users_auth_", + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey__pb_users_auth_` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `tokenKey` + "`" + `)", + "CREATE UNIQUE INDEX ` + "`" + `idx_email__pb_users_auth_` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''" + ], + "listRule": "id = @request.auth.id", + "manageRule": null, + "mfa": { + "duration": 1800, + "enabled": false, + "rule": "" + }, + "name": "users", + "oauth2": { + "enabled": false, + "mappedFields": { + "avatarURL": "avatar", + "id": "", + "name": "name", + "username": "" + } + }, + "otp": { + "duration": 180, + "emailTemplate": { + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "OTP for {APP_NAME}" + }, + "enabled": false, + "length": 8 + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "passwordResetToken": { + "duration": 1800 + }, + "resetPasswordTemplate": { + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Reset your {APP_NAME} password" + }, + "system": false, + "type": "auth", + "updateRule": "id = @request.auth.id", + "verificationTemplate": { + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

", + "subject": "Verify your {APP_NAME} email" + }, + "verificationToken": { + "duration": 259200 + }, + "viewRule": "id = @request.auth.id" + } + ]` + + return app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false) + }, func(app core.App) error { + return nil + }) +} diff --git a/services/data-manager/model/feedModel.go b/services/data-manager/model/feedModel.go index 88d8979..df21606 100644 --- a/services/data-manager/model/feedModel.go +++ b/services/data-manager/model/feedModel.go @@ -27,6 +27,8 @@ type Feed struct { Created types.DateTime `db:"created" json:"created"` Updated types.DateTime `db:"updated" json:"updated"` Deleted bool `db:"deleted" json:"deleted"` + Type string `db:"type" json:"type"` // "prof" or "student" + User string `db:"user" json:"user"` // Relation to users table core.BaseModel } diff --git a/services/data-manager/model/moduleModel.go b/services/data-manager/model/moduleModel.go index 1a9e3e9..b8e0ad6 100644 --- a/services/data-manager/model/moduleModel.go +++ b/services/data-manager/model/moduleModel.go @@ -30,12 +30,13 @@ func (m *Module) SetName(name string) { } type ModuleDTO struct { - UUID string `json:"uuid" db:"uuid"` - Name string `json:"name" db:"Name"` - Prof string `json:"prof" db:"Prof"` - Course string `json:"course" db:"course"` - Semester string `json:"semester" db:"semester"` - EventType string `db:"EventType" json:"eventType"` + UUID string `json:"uuid" db:"uuid"` + Name string `json:"name" db:"Name"` + Prof string `json:"prof" db:"Prof"` + Course string `json:"course" db:"course"` + Semester string `json:"semester" db:"semester"` + EventType string `db:"EventType" json:"eventType"` + ConfidenceScore float64 `json:"confidenceScore,omitempty"` } func (m *ModuleDTO) GetName() string { diff --git a/services/data-manager/model/serviceModel/serviceModel.go b/services/data-manager/model/serviceModel/serviceModel.go index 8e2fe69..bcc2f75 100644 --- a/services/data-manager/model/serviceModel/serviceModel.go +++ b/services/data-manager/model/serviceModel/serviceModel.go @@ -1,12 +1,15 @@ package serviceModel import ( - "github.com/pocketbase/pocketbase" "htwkalender/data-manager/service/events" + "htwkalender/data-manager/service/professor" + + "github.com/pocketbase/pocketbase" ) type Service struct { - App *pocketbase.PocketBase - EventService events.EventService - CourseService events.CourseService + App *pocketbase.PocketBase + EventService events.EventService + CourseService events.CourseService + ProfessorService *professor.ProfessorService } diff --git a/services/data-manager/service/addRoute.go b/services/data-manager/service/addRoute.go index 898f712..3a1098e 100644 --- a/services/data-manager/service/addRoute.go +++ b/services/data-manager/service/addRoute.go @@ -23,6 +23,7 @@ import ( v1 "htwkalender/data-manager/service/fetch/v1" v2 "htwkalender/data-manager/service/fetch/v2" "htwkalender/data-manager/service/functions/time" + "htwkalender/data-manager/service/professor" "htwkalender/data-manager/service/room" "log/slog" "net/http" @@ -231,6 +232,7 @@ func AddRoutes(services serviceModel.Service) { }).Bind(apis.RequireSuperuserAuth()) addFeedRoutes(se, services.App) + professor.RegisterRoutes(se, services.ProfessorService) return se.Next() }) diff --git a/services/data-manager/service/db/dbFeeds.go b/services/data-manager/service/db/dbFeeds.go index 483833b..4078e4d 100644 --- a/services/data-manager/service/db/dbFeeds.go +++ b/services/data-manager/service/db/dbFeeds.go @@ -18,12 +18,13 @@ package db import ( "errors" + "htwkalender/data-manager/model" + "time" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/types" - "htwkalender/data-manager/model" - "time" ) var _ core.RecordProxy = (*Feed)(nil) @@ -80,6 +81,22 @@ func (f *Feed) GetUpdated() types.DateTime { return f.GetDateTime("updated") } +func (f *Feed) GetType() string { + return f.GetString("type") +} + +func (f *Feed) SetType(feedType string) { + f.Set("type", feedType) +} + +func (f *Feed) GetUser() string { + return f.GetString("user") +} + +func (f *Feed) SetUser(user string) { + f.Set("user", user) +} + func newFeed(record *core.Record) *Feed { return &Feed{ BaseRecordProxy: core.BaseRecordProxy{Record: record}, @@ -99,6 +116,13 @@ func NewFeed(collection *core.Collection, feed model.Feed) (*Feed, error) { dbFeed.SetModules(feed.Modules) dbFeed.SetRetrieved(feed.Retrieved) dbFeed.SetDeleted(feed.Deleted) + // Set type to "student" as default if not specified + if feed.Type == "" { + dbFeed.SetType("student") + } else { + dbFeed.SetType(feed.Type) + } + dbFeed.SetUser(feed.User) return dbFeed, nil } @@ -110,6 +134,8 @@ func (f *Feed) ToModel() model.Feed { Created: f.GetCreated(), Updated: f.GetUpdated(), Deleted: f.GetDeleted(), + Type: f.GetType(), + User: f.GetUser(), BaseModel: core.BaseModel{ Id: f.GetId(), }, @@ -143,6 +169,8 @@ func FindFeedByToken(app *pocketbase.PocketBase, token string) (*model.Feed, err feed.Modules = record.GetString("modules") feed.Retrieved = record.GetDateTime("retrieved") feed.Deleted = record.GetBool("deleted") + feed.Type = record.GetString("type") + feed.User = record.GetString("user") //update retrieved time if the is not marked as deleted if !record.GetBool("deleted") { diff --git a/services/data-manager/service/grpc/feedService.go b/services/data-manager/service/grpc/feedService.go index 67870c8..8b20f97 100644 --- a/services/data-manager/service/grpc/feedService.go +++ b/services/data-manager/service/grpc/feedService.go @@ -2,9 +2,10 @@ package grpc import ( "context" - "github.com/pocketbase/pocketbase" pb "htwkalender/common/genproto/modules" "htwkalender/data-manager/service/db" + + "github.com/pocketbase/pocketbase" ) type FeedServiceHandler struct { @@ -25,9 +26,17 @@ func (s *FeedServiceHandler) GetFeed(ctx context.Context, in *pb.GetFeedRequest) return nil, err } - // Implement your logic here to fetch feed data based on the UUID - // Example response + pbFeed := feedToProto(feed) + + // If feed has a user linked, fetch the user's email + if feed.User != "" { + user, err := s.app.FindRecordById("users", feed.User) + if err == nil && user != nil { + pbFeed.UserEmail = user.Email() + } + } + return &pb.GetFeedResponse{ - Feed: feedToProto(feed), + Feed: pbFeed, }, nil } diff --git a/services/data-manager/service/grpc/mapper.go b/services/data-manager/service/grpc/mapper.go index 4981044..ecfec84 100644 --- a/services/data-manager/service/grpc/mapper.go +++ b/services/data-manager/service/grpc/mapper.go @@ -40,5 +40,7 @@ func feedToProto(feed *model.Feed) *pb.Feed { Retrieved: feed.Retrieved.String(), Modules: feed.Modules, Deleted: feed.Deleted, + Type: feed.Type, + User: feed.User, } } diff --git a/services/data-manager/service/hooks.go b/services/data-manager/service/hooks.go new file mode 100644 index 0000000..421d5d9 --- /dev/null +++ b/services/data-manager/service/hooks.go @@ -0,0 +1,34 @@ +package service + +import ( + "strings" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" +) + +func AddHooks(app *pocketbase.PocketBase) { + app.OnRecordAuthWithOAuth2Request("users").BindFunc(func(e *core.RecordAuthWithOAuth2RequestEvent) error { + email := e.OAuth2User.Email + + // If email is not in the main field, try to extract it from RawUser + if email == "" { + if rawEmail, ok := e.OAuth2User.RawUser["email"].(string); ok { + email = rawEmail + // Explicitly set the email on the OAuth2User so PocketBase uses it + e.OAuth2User.Email = rawEmail + } + } + + if email == "" { + return apis.NewBadRequestError("No email received from OAuth2 provider. Please ensure your account has an email address and the 'email' scope is granted.", nil) + } + // Restrict login to @htwk-leipzig.de employees only (not students) + if !strings.HasSuffix(email, "@htwk-leipzig.de") { + return apis.NewBadRequestError("Login restricted to @htwk-leipzig.de emails. Students (@stud.htwk-leipzig.de) are not allowed.", nil) + } + + return e.Next() + }) +} diff --git a/services/data-manager/service/ical/ical.go b/services/data-manager/service/ical/ical.go index d99e66b..c802e12 100644 --- a/services/data-manager/service/ical/ical.go +++ b/services/data-manager/service/ical/ical.go @@ -18,17 +18,33 @@ package ical import ( "encoding/json" - "github.com/pocketbase/pocketbase" - "github.com/pocketbase/pocketbase/apis" "htwkalender/data-manager/model" "htwkalender/data-manager/service/db" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/apis" ) func CreateIndividualFeed(modules []model.FeedCollection, pb *pocketbase.PocketBase) (string, error) { + return CreateIndividualFeedWithType(modules, "", "", pb) +} + +// CreateIndividualFeedWithType creates a feed with specified type and user +func CreateIndividualFeedWithType(modules []model.FeedCollection, feedType string, userId string, pb *pocketbase.PocketBase) (string, error) { var icalFeed model.Feed jsonModules, _ := json.Marshal(modules) icalFeed.Modules = string(jsonModules) + // Set feed type (defaults to "student" in SaveFeed if not provided) + if feedType != "" { + icalFeed.Type = feedType + } + + // Set user relation for professor feeds + if userId != "" { + icalFeed.User = userId + } + collection, dbError := pb.FindCollectionByNameOrId("feeds") if dbError != nil { return "", apis.NewNotFoundError("Collection could not be found", dbError) diff --git a/services/data-manager/service/professor/professorService.go b/services/data-manager/service/professor/professorService.go new file mode 100644 index 0000000..ae3edca --- /dev/null +++ b/services/data-manager/service/professor/professorService.go @@ -0,0 +1,79 @@ +package professor + +import ( + "htwkalender/data-manager/model" + "log/slog" + + commonProf "htwkalender/common/professor" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase" +) + +type ProfessorService struct { + app *pocketbase.PocketBase +} + +func NewProfessorService(app *pocketbase.PocketBase) *ProfessorService { + return &ProfessorService{app: app} +} + +func (s *ProfessorService) GetModulesForProfessor(email string) ([]model.ModuleDTO, error) { + // Extract name from email + firstName, lastName, err := commonProf.ExtractNameFromEmail(email) + if err != nil { + slog.Warn("Failed to extract name from email", "email", email, "error", err) + return []model.ModuleDTO{}, nil + } + + slog.Info("Searching for modules for professor", "firstName", firstName, "lastName", lastName, "email", email) + + // First, get all distinct modules with their professors + type EventProf struct { + Name string `db:"Name" json:"name"` + EventType string `db:"EventType" json:"eventType"` + Prof string `db:"Prof" json:"prof"` + Course string `db:"course" json:"course"` + Semester string `db:"semester" json:"semester"` + UUID string `db:"uuid" json:"uuid"` + } + + var allEvents []EventProf + err = s.app.DB(). + Select("Name", "EventType", "Prof", "course", "semester", "uuid"). + From("events"). + Where(dbx.NewExp("Prof != ''")). + GroupBy("Name", "course", "Prof"). + Distinct(true). + All(&allEvents) + + if err != nil { + return nil, err + } + + // Filter events by matching professor name and calculate confidence scores + var modules []model.ModuleDTO + seenModules := make(map[string]bool) // key: Name+Course to avoid duplicates + + for _, event := range allEvents { + score := commonProf.CalculateConfidenceScore(event.Prof, firstName, lastName) + if score > 0 { // Include all modules with any match + key := event.Name + "|" + event.Course + if !seenModules[key] { + modules = append(modules, model.ModuleDTO{ + Name: event.Name, + EventType: event.EventType, + Prof: event.Prof, + Course: event.Course, + Semester: event.Semester, + UUID: event.UUID, + ConfidenceScore: score, + }) + seenModules[key] = true + } + } + } + + slog.Info("Found modules for professor", "count", len(modules), "lastName", lastName) + return modules, nil +} diff --git a/services/data-manager/service/professor/routes.go b/services/data-manager/service/professor/routes.go new file mode 100644 index 0000000..fef663e --- /dev/null +++ b/services/data-manager/service/professor/routes.go @@ -0,0 +1,63 @@ +package professor + +import ( + "htwkalender/data-manager/model" + "htwkalender/data-manager/service/ical" + "log/slog" + "net/http" + + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" +) + +func RegisterRoutes(se *core.ServeEvent, service *ProfessorService) { + se.Router.GET("/api/professor/modules", func(e *core.RequestEvent) error { + record := e.Auth + if record == nil { + return apis.NewForbiddenError("Only authenticated users can access this endpoint", nil) + } + + email := record.GetString("email") + if email == "" { + return apis.NewBadRequestError("User has no email", nil) + } + + modules, err := service.GetModulesForProfessor(email) + if err != nil { + return apis.NewBadRequestError("Failed to fetch modules", err) + } + + return e.JSON(http.StatusOK, modules) + }).Bind(apis.RequireAuth()) + + // POST /api/professor/feed - Create a professor-specific feed + se.Router.POST("/api/professor/feed", func(e *core.RequestEvent) error { + record := e.Auth + if record == nil { + return apis.NewForbiddenError("Only authenticated users can access this endpoint", nil) + } + + userId := record.Id + if userId == "" { + return apis.NewBadRequestError("User ID not found", nil) + } + + // Bind the feed collection from request body + var feedCollection []model.FeedCollection + err := e.BindBody(&feedCollection) + if err != nil { + slog.Error("Failed to bind request body", "error", err) + return apis.NewBadRequestError("Invalid request body", err) + } + + // Create feed with type="prof" and link to authenticated user + token, err := ical.CreateIndividualFeedWithType(feedCollection, "prof", userId, service.app) + if err != nil { + slog.Error("Failed to create professor feed", "error", err, "userId", userId) + return apis.NewInternalServerError("Failed to create professor feed", err) + } + + slog.Info("Created professor feed", "userId", userId, "token", token) + return e.JSON(http.StatusOK, token) + }).Bind(apis.RequireAuth()) +} diff --git a/services/ical/model/feedModel.go b/services/ical/model/feedModel.go index 864d629..e55c1d8 100644 --- a/services/ical/model/feedModel.go +++ b/services/ical/model/feedModel.go @@ -27,6 +27,9 @@ type BaseModel struct { type Feed struct { Modules string `db:"modules" json:"modules"` Retrieved JSONTime `db:"retrieved" json:"retrieved"` + Deleted bool `db:"deleted" json:"deleted"` + Type string `db:"type" json:"type"` // "prof" or "student" + User string `db:"user" json:"user"` // Relation to users table BaseModel } diff --git a/services/ical/model/icalModel.go b/services/ical/model/icalModel.go index e5cab49..1d88ac5 100644 --- a/services/ical/model/icalModel.go +++ b/services/ical/model/icalModel.go @@ -62,6 +62,9 @@ type FeedRecord struct { Modules []FeedModule `db:"modules" json:"modules"` Retrieved JSONTime `db:"retrieved" json:"retrieved"` Deleted bool `db:"deleted" json:"deleted"` + Type string `db:"type" json:"type"` // "prof" or "student" + User string `db:"user" json:"user"` // User ID relation + UserEmail string `db:"user_email" json:"userEmail"` // User Email for filtering BaseModel } diff --git a/services/ical/service/connector/grpc/feeds.go b/services/ical/service/connector/grpc/feeds.go index a8a16f6..b1a7039 100644 --- a/services/ical/service/connector/grpc/feeds.go +++ b/services/ical/service/connector/grpc/feeds.go @@ -18,12 +18,13 @@ package grpc import ( "context" - "github.com/goccy/go-json" - "google.golang.org/grpc" pb "htwkalender/common/genproto/modules" "htwkalender/ical/model" "log/slog" "time" + + "github.com/goccy/go-json" + "google.golang.org/grpc" ) func GetFeed(feedId string, conn *grpc.ClientConn) (model.FeedRecord, error) { @@ -65,5 +66,8 @@ func protoToFeed(feed *pb.Feed) (model.FeedRecord, error) { Retrieved: model.ToJSONTime(feed.Retrieved), Modules: modules, Deleted: feed.Deleted, + Type: feed.Type, + User: feed.User, + UserEmail: feed.UserEmail, }, nil } diff --git a/services/ical/service/ical/ical.go b/services/ical/service/ical/ical.go index 17045b2..09dbe35 100644 --- a/services/ical/service/ical/ical.go +++ b/services/ical/service/ical/ical.go @@ -24,6 +24,8 @@ import ( "htwkalender/ical/service/functions" "log/slog" "time" + + commonProf "htwkalender/common/professor" ) const expirationTime = 5 * time.Minute @@ -59,6 +61,11 @@ func Feed(app model.AppType, token string, userAgent string) (string, string, er return "", "", err } + // Filter by professor if type is "prof" + if feed.Type == "prof" && feed.UserEmail != "" { + events = filterEventsByProfessor(events, feed.UserEmail) + } + // Sort events by start date events.Sort() @@ -126,3 +133,22 @@ func FeedRoom(app model.AppType, room string, userAgent string) (string, string, return icalFeed.Content, etag, nil } + +func filterEventsByProfessor(events model.Events, email string) model.Events { + if email == "" { + return events + } + + firstName, lastName, err := commonProf.ExtractNameFromEmail(email) + if err != nil { + return events + } + + var filteredEvents model.Events + for _, event := range events { + if commonProf.MatchesProfessor(event.Prof, firstName, lastName) { + filteredEvents = append(filteredEvents, event) + } + } + return filteredEvents +} diff --git a/services/protobuf/feeds.proto b/services/protobuf/feeds.proto index 243b83a..3eaae78 100644 --- a/services/protobuf/feeds.proto +++ b/services/protobuf/feeds.proto @@ -13,6 +13,9 @@ message Feed { string created = 4; string updated = 5; bool deleted = 6; + string type = 7; // "prof" or "student" + string user = 8; // User ID relation + string user_email = 9; // User Email for filtering } message GetFeedRequest { diff --git a/services/protobuf/htwkalender/common/modules/feeds.pb.go b/services/protobuf/htwkalender/common/modules/feeds.pb.go new file mode 100644 index 0000000..28ba68a --- /dev/null +++ b/services/protobuf/htwkalender/common/modules/feeds.pb.go @@ -0,0 +1,350 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v5.27.1 +// source: feeds.proto + +package modules + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Feed struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Modules string `protobuf:"bytes,2,opt,name=modules,proto3" json:"modules,omitempty"` + Retrieved string `protobuf:"bytes,3,opt,name=retrieved,proto3" json:"retrieved,omitempty"` + Created string `protobuf:"bytes,4,opt,name=created,proto3" json:"created,omitempty"` + Updated string `protobuf:"bytes,5,opt,name=updated,proto3" json:"updated,omitempty"` + Deleted bool `protobuf:"varint,6,opt,name=deleted,proto3" json:"deleted,omitempty"` + Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` // "prof" or "student" + User string `protobuf:"bytes,8,opt,name=user,proto3" json:"user,omitempty"` // User ID relation + UserEmail string `protobuf:"bytes,9,opt,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"` // User Email for filtering +} + +func (x *Feed) Reset() { + *x = Feed{} + if protoimpl.UnsafeEnabled { + mi := &file_feeds_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Feed) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Feed) ProtoMessage() {} + +func (x *Feed) ProtoReflect() protoreflect.Message { + mi := &file_feeds_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Feed.ProtoReflect.Descriptor instead. +func (*Feed) Descriptor() ([]byte, []int) { + return file_feeds_proto_rawDescGZIP(), []int{0} +} + +func (x *Feed) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Feed) GetModules() string { + if x != nil { + return x.Modules + } + return "" +} + +func (x *Feed) GetRetrieved() string { + if x != nil { + return x.Retrieved + } + return "" +} + +func (x *Feed) GetCreated() string { + if x != nil { + return x.Created + } + return "" +} + +func (x *Feed) GetUpdated() string { + if x != nil { + return x.Updated + } + return "" +} + +func (x *Feed) GetDeleted() bool { + if x != nil { + return x.Deleted + } + return false +} + +func (x *Feed) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Feed) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *Feed) GetUserEmail() string { + if x != nil { + return x.UserEmail + } + return "" +} + +type GetFeedRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *GetFeedRequest) Reset() { + *x = GetFeedRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_feeds_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetFeedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFeedRequest) ProtoMessage() {} + +func (x *GetFeedRequest) ProtoReflect() protoreflect.Message { + mi := &file_feeds_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFeedRequest.ProtoReflect.Descriptor instead. +func (*GetFeedRequest) Descriptor() ([]byte, []int) { + return file_feeds_proto_rawDescGZIP(), []int{1} +} + +func (x *GetFeedRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type GetFeedResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Feed *Feed `protobuf:"bytes,1,opt,name=feed,proto3" json:"feed,omitempty"` +} + +func (x *GetFeedResponse) Reset() { + *x = GetFeedResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_feeds_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetFeedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFeedResponse) ProtoMessage() {} + +func (x *GetFeedResponse) ProtoReflect() protoreflect.Message { + mi := &file_feeds_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFeedResponse.ProtoReflect.Descriptor instead. +func (*GetFeedResponse) Descriptor() ([]byte, []int) { + return file_feeds_proto_rawDescGZIP(), []int{2} +} + +func (x *GetFeedResponse) GetFeed() *Feed { + if x != nil { + return x.Feed + } + return nil +} + +var File_feeds_proto protoreflect.FileDescriptor + +var file_feeds_proto_rawDesc = []byte{ + 0x0a, 0x0b, 0x66, 0x65, 0x65, 0x64, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe3, 0x01, + 0x0a, 0x04, 0x46, 0x65, 0x65, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, + 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x64, 0x12, 0x18, + 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x75, 0x73, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x45, 0x6d, + 0x61, 0x69, 0x6c, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x2c, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x66, 0x65, 0x65, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x05, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x52, 0x04, 0x66, + 0x65, 0x65, 0x64, 0x32, 0x3d, 0x0a, 0x0b, 0x46, 0x65, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x12, 0x0f, 0x2e, + 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, + 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x42, 0x1c, 0x5a, 0x1a, 0x68, 0x74, 0x77, 0x6b, 0x61, 0x6c, 0x65, 0x6e, 0x64, 0x65, + 0x72, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_feeds_proto_rawDescOnce sync.Once + file_feeds_proto_rawDescData = file_feeds_proto_rawDesc +) + +func file_feeds_proto_rawDescGZIP() []byte { + file_feeds_proto_rawDescOnce.Do(func() { + file_feeds_proto_rawDescData = protoimpl.X.CompressGZIP(file_feeds_proto_rawDescData) + }) + return file_feeds_proto_rawDescData +} + +var file_feeds_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_feeds_proto_goTypes = []interface{}{ + (*Feed)(nil), // 0: Feed + (*GetFeedRequest)(nil), // 1: GetFeedRequest + (*GetFeedResponse)(nil), // 2: GetFeedResponse +} +var file_feeds_proto_depIdxs = []int32{ + 0, // 0: GetFeedResponse.feed:type_name -> Feed + 1, // 1: FeedService.GetFeed:input_type -> GetFeedRequest + 2, // 2: FeedService.GetFeed:output_type -> GetFeedResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_feeds_proto_init() } +func file_feeds_proto_init() { + if File_feeds_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_feeds_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Feed); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_feeds_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetFeedRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_feeds_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetFeedResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_feeds_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_feeds_proto_goTypes, + DependencyIndexes: file_feeds_proto_depIdxs, + MessageInfos: file_feeds_proto_msgTypes, + }.Build() + File_feeds_proto = out.File + file_feeds_proto_rawDesc = nil + file_feeds_proto_goTypes = nil + file_feeds_proto_depIdxs = nil +} diff --git a/services/protobuf/htwkalender/common/modules/feeds_grpc.pb.go b/services/protobuf/htwkalender/common/modules/feeds_grpc.pb.go new file mode 100644 index 0000000..10d1fc2 --- /dev/null +++ b/services/protobuf/htwkalender/common/modules/feeds_grpc.pb.go @@ -0,0 +1,105 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v5.27.1 +// source: feeds.proto + +package modules + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// FeedServiceClient is the client API for FeedService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type FeedServiceClient interface { + GetFeed(ctx context.Context, in *GetFeedRequest, opts ...grpc.CallOption) (*GetFeedResponse, error) +} + +type feedServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewFeedServiceClient(cc grpc.ClientConnInterface) FeedServiceClient { + return &feedServiceClient{cc} +} + +func (c *feedServiceClient) GetFeed(ctx context.Context, in *GetFeedRequest, opts ...grpc.CallOption) (*GetFeedResponse, error) { + out := new(GetFeedResponse) + err := c.cc.Invoke(ctx, "/FeedService/GetFeed", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// FeedServiceServer is the server API for FeedService service. +// All implementations must embed UnimplementedFeedServiceServer +// for forward compatibility +type FeedServiceServer interface { + GetFeed(context.Context, *GetFeedRequest) (*GetFeedResponse, error) + mustEmbedUnimplementedFeedServiceServer() +} + +// UnimplementedFeedServiceServer must be embedded to have forward compatible implementations. +type UnimplementedFeedServiceServer struct { +} + +func (UnimplementedFeedServiceServer) GetFeed(context.Context, *GetFeedRequest) (*GetFeedResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetFeed not implemented") +} +func (UnimplementedFeedServiceServer) mustEmbedUnimplementedFeedServiceServer() {} + +// UnsafeFeedServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to FeedServiceServer will +// result in compilation errors. +type UnsafeFeedServiceServer interface { + mustEmbedUnimplementedFeedServiceServer() +} + +func RegisterFeedServiceServer(s grpc.ServiceRegistrar, srv FeedServiceServer) { + s.RegisterService(&FeedService_ServiceDesc, srv) +} + +func _FeedService_GetFeed_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetFeedRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(FeedServiceServer).GetFeed(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/FeedService/GetFeed", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FeedServiceServer).GetFeed(ctx, req.(*GetFeedRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// FeedService_ServiceDesc is the grpc.ServiceDesc for FeedService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var FeedService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "FeedService", + HandlerType: (*FeedServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetFeed", + Handler: _FeedService_GetFeed_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "feeds.proto", +}