diff --git a/server/api/tumdev/campus_backend.pb.go b/server/api/tumdev/campus_backend.pb.go index 662fcecc..db845a21 100644 --- a/server/api/tumdev/campus_backend.pb.go +++ b/server/api/tumdev/campus_backend.pb.go @@ -650,7 +650,12 @@ type News struct { Link string `protobuf:"bytes,4,opt,name=link,proto3" json:"link,omitempty"` // where a news thumbnail is stored. empty string means no image is available ImageUrl string `protobuf:"bytes,5,opt,name=image_url,json=imageUrl,proto3" json:"image_url,omitempty"` - Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` + // the id of the news source + SourceId string `protobuf:"bytes,6,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` + // where the icon can be found + SourceIconUrl string `protobuf:"bytes,9,opt,name=source_icon_url,json=sourceIconUrl,proto3" json:"source_icon_url,omitempty"` + // human readable title of the news source + SourceTitle string `protobuf:"bytes,10,opt,name=source_title,json=sourceTitle,proto3" json:"source_title,omitempty"` // when the news item was created in OUR database Created *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created,proto3" json:"created,omitempty"` // the date of the news item @@ -724,9 +729,23 @@ func (x *News) GetImageUrl() string { return "" } -func (x *News) GetSource() string { +func (x *News) GetSourceId() string { if x != nil { - return x.Source + return x.SourceId + } + return "" +} + +func (x *News) GetSourceIconUrl() string { + if x != nil { + return x.SourceIconUrl + } + return "" +} + +func (x *News) GetSourceTitle() string { + if x != nil { + return x.SourceTitle } return "" } @@ -4818,16 +4837,21 @@ var file_tumdev_campus_backend_proto_rawDesc = []byte{ 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x61, 0x6d, 0x70, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x61, 0x6d, 0x70, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xef, 0x01, 0x0a, 0x04, 0x4e, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xbf, 0x02, 0x0a, 0x04, 0x4e, 0x65, 0x77, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x16, - 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x1b, + 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x63, 0x6f, 0x6e, + 0x55, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x04, diff --git a/server/api/tumdev/campus_backend.proto b/server/api/tumdev/campus_backend.proto index f0894032..94cd6985 100644 --- a/server/api/tumdev/campus_backend.proto +++ b/server/api/tumdev/campus_backend.proto @@ -239,7 +239,12 @@ message News { string link = 4; // where a news thumbnail is stored. empty string means no image is available string image_url = 5; - string source = 6; + // the id of the news source + string source_id = 6; + // where the icon can be found + string source_icon_url = 9; + // human readable title of the news source + string source_title = 10; // when the news item was created in OUR database google.protobuf.Timestamp created = 7; // the date of the news item diff --git a/server/api/tumdev/campus_backend.swagger.json b/server/api/tumdev/campus_backend.swagger.json index db409835..188fc4c5 100644 --- a/server/api/tumdev/campus_backend.swagger.json +++ b/server/api/tumdev/campus_backend.swagger.json @@ -1623,8 +1623,17 @@ "type": "string", "title": "where a news thumbnail is stored. empty string means no image is available" }, - "source": { - "type": "string" + "sourceId": { + "type": "string", + "title": "the id of the news source" + }, + "sourceIconUrl": { + "type": "string", + "title": "where the icon can be found" + }, + "sourceTitle": { + "type": "string", + "title": "human readable title of the news source" }, "created": { "type": "string", diff --git a/server/backend/cron/news.go b/server/backend/cron/news.go index de7e4195..8514c0b0 100644 --- a/server/backend/cron/news.go +++ b/server/backend/cron/news.go @@ -116,15 +116,16 @@ func (c *CronService) parseNewsFeed(source model.NewsSource) error { } newsItem := model.News{ - Date: *item.PublishedParsed, - Created: time.Now(), - Title: item.Title, - Description: bluemonday.StrictPolicy().Sanitize(item.Description), - Src: source.Source, - Link: item.Link, - Image: enclosureUrl, - FileID: fileID, - File: file, + Date: *item.PublishedParsed, + Created: time.Now(), + Title: item.Title, + Description: bluemonday.StrictPolicy().Sanitize(item.Description), + NewsSourceID: source.Source, + NewsSource: source, + Link: item.Link, + Image: enclosureUrl, + FileID: fileID, + File: file, } newNews = append(newNews, newsItem) } diff --git a/server/backend/news.go b/server/backend/news.go index 70b11381..f8f29847 100644 --- a/server/backend/news.go +++ b/server/backend/news.go @@ -45,7 +45,7 @@ func (s *CampusServer) ListNews(ctx context.Context, req *pb.ListNewsRequest) (* } var newsEntries []model.News - tx := s.db.WithContext(ctx).Joins("File") + tx := s.db.WithContext(ctx).Joins("File").Joins("NewsSource").Joins("NewsSource.File") if req.NewsSource != 0 { tx = tx.Where("src = ?", req.NewsSource) } @@ -68,14 +68,16 @@ func (s *CampusServer) ListNews(ctx context.Context, req *pb.ListNewsRequest) (* imgUrl = item.File.FullExternalUrl() } resp[i] = &pb.News{ - Id: item.News, - Title: item.Title, - Text: item.Description, - Link: item.Link, - ImageUrl: imgUrl, - Source: fmt.Sprintf("%d", item.Src), - Created: timestamppb.New(item.Created), - Date: timestamppb.New(item.Date), + Id: item.News, + Title: item.Title, + Text: item.Description, + Link: item.Link, + ImageUrl: imgUrl, + SourceId: fmt.Sprintf("%d", item.NewsSource.Source), + SourceTitle: item.NewsSource.Title, + SourceIconUrl: item.NewsSource.File.FullExternalUrl(), + Created: timestamppb.New(item.Created), + Date: timestamppb.New(item.Date), } } return &pb.ListNewsReply{News: resp}, nil diff --git a/server/backend/news_test.go b/server/backend/news_test.go index 7166fee9..62922c7c 100644 --- a/server/backend/news_test.go +++ b/server/backend/news_test.go @@ -141,12 +141,12 @@ func (s *NewsSuite) Test_ListNewsSourcesNone() { require.Equal(s.T(), expectedResp, response) } -const ExpectedListNewsQuery = "SELECT `news`.`news`,`news`.`date`,`news`.`created`,`news`.`title`,`news`.`description`,`news`.`src`,`news`.`link`,`news`.`image`,`news`.`file`,`File`.`file` AS `File__file`,`File`.`name` AS `File__name`,`File`.`path` AS `File__path`,`File`.`downloads` AS `File__downloads`,`File`.`url` AS `File__url`,`File`.`downloaded` AS `File__downloaded` FROM `news` LEFT JOIN `files` `File` ON `news`.`file` = `File`.`file`" +const ExpectedListNewsQuery = "SELECT `news`.`news`,`news`.`date`,`news`.`created`,`news`.`title`,`news`.`description`,`news`.`src`,`news`.`link`,`news`.`image`,`news`.`file`,`File`.`file` AS `File__file`,`File`.`name` AS `File__name`,`File`.`path` AS `File__path`,`File`.`downloads` AS `File__downloads`,`File`.`url` AS `File__url`,`File`.`downloaded` AS `File__downloaded`,`NewsSource`.`source` AS `NewsSource__source`,`NewsSource`.`title` AS `NewsSource__title`,`NewsSource`.`url` AS `NewsSource__url`,`NewsSource`.`icon` AS `NewsSource__icon`,`NewsSource`.`hook` AS `NewsSource__hook`,`NewsSource__File`.`file` AS `NewsSource__File__file`,`NewsSource__File`.`name` AS `NewsSource__File__name`,`NewsSource__File`.`path` AS `NewsSource__File__path`,`NewsSource__File`.`downloads` AS `NewsSource__File__downloads`,`NewsSource__File`.`url` AS `NewsSource__File__url`,`NewsSource__File`.`downloaded` AS `NewsSource__File__downloaded` FROM `news` LEFT JOIN `files` `File` ON `news`.`file` = `File`.`file` LEFT JOIN `newsSource` `NewsSource` ON `news`.`src` = `NewsSource`.`source` LEFT JOIN `files` `NewsSource__File` ON `NewsSource`.`icon` = `NewsSource__File`.`file`" func (s *NewsSuite) Test_ListNewsNone_withFilters() { s.mock.ExpectQuery(regexp.QuoteMeta(ExpectedListNewsQuery+" WHERE src = ? AND news > ?")). WithArgs(1, 2). - WillReturnRows(sqlmock.NewRows([]string{"news", "date", "created", "title", "description", "src", "link", "image", "file", "File__file", "File__name", "File__path", "File__downloads", "File__url", "File__downloaded"})) + WillReturnRows(sqlmock.NewRows([]string{"news", "date", "created", "title", "description", "src", "link", "image", "file", "File__file", "File__name", "File__path", "File__downloads", "File__url", "File__downloaded", "source", "NewsSource__source", "NewsSource__title", "NewsSource__url", "NewsSource__icon", "NewsSource__hook", "NewsSource__File__file", "NewsSource__File__name", "NewsSource__File__path", "NewsSource__File__downloads", "NewsSource__File__url", "NewsSource__File__downloaded"})) meta := metadata.NewIncomingContext(context.Background(), metadata.MD{}) server := CampusServer{db: s.DB, deviceBuf: s.deviceBuf} @@ -159,7 +159,7 @@ func (s *NewsSuite) Test_ListNewsNone_withFilters() { } func (s *NewsSuite) Test_ListNewsNone() { s.mock.ExpectQuery(regexp.QuoteMeta(ExpectedListNewsQuery)). - WillReturnRows(sqlmock.NewRows([]string{"news", "date", "created", "title", "description", "src", "link", "image", "file", "File__file", "File__name", "File__path", "File__downloads", "File__url", "File__downloaded"})) + WillReturnRows(sqlmock.NewRows([]string{"news", "date", "created", "title", "description", "src", "link", "image", "file", "File__file", "File__name", "File__path", "File__downloads", "File__url", "File__downloaded", "source", "NewsSource__source", "NewsSource__title", "NewsSource__url", "NewsSource__icon", "NewsSource__hook", "NewsSource__File__file", "NewsSource__File__name", "NewsSource__File__path", "NewsSource__File__downloads", "NewsSource__File__url", "NewsSource__File__downloaded"})) meta := metadata.NewIncomingContext(context.Background(), metadata.MD{}) server := CampusServer{db: s.DB, deviceBuf: s.deviceBuf} @@ -175,8 +175,8 @@ func (s *NewsSuite) Test_ListNewsMultiple() { n2 := news2() s.mock.ExpectQuery(regexp.QuoteMeta(" ")). WillReturnRows(sqlmock.NewRows([]string{"news", "date", "created", "title", "description", "src", "link", "image", "file", "File__file", "File__name", "File__path", "File__downloads", "File__url", "File__downloaded"}). - AddRow(n1.News, n1.Date, n1.Created, n1.Title, n1.Description, n1.Src, n1.Link, n1.Image, n1.FileID, n1.File.File, n1.File.Name, n1.File.Path, n1.File.Downloads, n1.File.URL, n1.File.Downloaded). - AddRow(n2.News, n2.Date, n2.Created, n2.Title, n2.Description, n2.Src, n2.Link, n2.Image, nil, nil, nil, nil, nil, nil, nil)) + AddRow(n1.News, n1.Date, n1.Created, n1.Title, n1.Description, n1.NewsSourceID, n1.Link, n1.Image, n1.FileID, n1.File.File, n1.File.Name, n1.File.Path, n1.File.Downloads, n1.File.URL, n1.File.Downloaded). + AddRow(n2.News, n2.Date, n2.Created, n2.Title, n2.Description, n2.NewsSourceID, n2.Link, n2.Image, nil, nil, nil, nil, nil, nil, nil)) meta := metadata.NewIncomingContext(context.Background(), metadata.MD{}) server := CampusServer{db: s.DB, deviceBuf: s.deviceBuf} @@ -184,8 +184,8 @@ func (s *NewsSuite) Test_ListNewsMultiple() { require.NoError(s.T(), err) expectedResp := &pb.ListNewsReply{ News: []*pb.News{ - {Id: n1.News, Title: n1.Title, Text: n1.Description, Link: n1.Link, ImageUrl: "https://api.tum.app/files/news/sources/src_1.png", Source: fmt.Sprintf("%d", n1.Src), Created: timestamppb.New(n1.Created), Date: timestamppb.New(n1.Date)}, - {Id: n2.News, Title: n2.Title, Text: n2.Description, Link: n2.Link, ImageUrl: "", Source: fmt.Sprintf("%d", n2.Src), Created: timestamppb.New(n2.Created), Date: timestamppb.New(n2.Date)}, + {Id: n1.News, Title: n1.Title, Text: n1.Description, Link: n1.Link, ImageUrl: "https://api.tum.app/files/news/sources/src_1.png", SourceId: fmt.Sprintf("%d", n1.NewsSourceID), SourceIconUrl: n1.NewsSource.File.FullExternalUrl(), SourceTitle: n1.NewsSource.Title, Created: timestamppb.New(n1.Created), Date: timestamppb.New(n1.Date)}, + {Id: n2.News, Title: n2.Title, Text: n2.Description, Link: n2.Link, ImageUrl: "", SourceId: fmt.Sprintf("%d", n2.NewsSourceID), SourceIconUrl: n2.NewsSource.File.FullExternalUrl(), SourceTitle: n2.NewsSource.Title, Created: timestamppb.New(n2.Created), Date: timestamppb.New(n2.Date)}, }, } require.Equal(s.T(), expectedResp, response) diff --git a/server/main.go b/server/main.go index 75a49da5..01b65ca0 100644 --- a/server/main.go +++ b/server/main.go @@ -124,14 +124,14 @@ func main() { func UnaryRequestLogger(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { start := time.Now() resp, err := handler(ctx, req) - fields := log.Fields{"elapsed": time.Now().Sub(start)} + fields := log.Fields{"elapsed": time.Since(start)} log.WithContext(ctx).WithFields(fields).WithError(err).Info(info.FullMethod) return resp, err } func StreamRequestLogger(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { start := time.Now() err := handler(srv, stream) - log.WithField("elapsed", time.Now().Sub(start)).WithError(err).Info(info.FullMethod) + log.WithField("elapsed", time.Since(start)).WithError(err).Info(info.FullMethod) return err } diff --git a/server/model/news.go b/server/model/news.go index 4fa2158f..5de7ace7 100755 --- a/server/model/news.go +++ b/server/model/news.go @@ -15,16 +15,17 @@ var ( // News struct is a row record of the news table in the tca database type News struct { - News int64 `gorm:"primary_key;AUTO_INCREMENT;column:news;type:int;"` - Date time.Time `gorm:"column:date;type:datetime;"` - Created time.Time `gorm:"column:created;type:timestamp;default:CURRENT_TIMESTAMP;"` - Title string `gorm:"column:title;type:text;size:255;"` - Description string `gorm:"column:description;type:text;size:65535;"` - Src int64 `gorm:"column:src;type:int;"` - Link string `gorm:"column:link;type:varchar(190);"` - Image null.String `gorm:"column:image;type:text;size:65535;"` - FileID null.Int `gorm:"column:file;type:int;"` - File *File `gorm:"foreignKey:FileID;references:file;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` + News int64 `gorm:"primary_key;AUTO_INCREMENT;column:news;type:int;"` + Date time.Time `gorm:"column:date;type:datetime;"` + Created time.Time `gorm:"column:created;type:timestamp;default:CURRENT_TIMESTAMP;"` + Title string `gorm:"column:title;type:text;size:255;"` + Description string `gorm:"column:description;type:text;size:65535;"` + NewsSourceID int64 `gorm:"column:src;type:int;"` + NewsSource NewsSource `gorm:"foreignKey:NewsSourceID;references:source;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` + Link string `gorm:"column:link;type:varchar(190);"` + Image null.String `gorm:"column:image;type:text;size:65535;"` + FileID null.Int `gorm:"column:file;type:int;"` + File *File `gorm:"foreignKey:FileID;references:file;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` } // TableName sets the insert table name for this struct type