~jan0sch/smederee
Showing details for patch 4250d329f0c8eca74b595d9c409f5528ed5c1137.
diff -rN -u old-smederee/modules/hub/src/main/resources/messages.properties new-smederee/modules/hub/src/main/resources/messages.properties --- old-smederee/modules/hub/src/main/resources/messages.properties 2025-01-15 05:30:37.144089689 +0000 +++ new-smederee/modules/hub/src/main/resources/messages.properties 2025-01-15 05:30:37.144089689 +0000 @@ -251,6 +251,7 @@ repository.overview.download.link=Source code (.tar.gz) repository.overview.latest-changes=Latest changes repository.overview.latest-changes.timestamp={0,date,yyyy-MM-dd (E)}, {0,time,short} +repository.overview.rss-feed=RSS feed for this project. # User management / settings user.settings.account.delete.title=Delete your account diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/Rss.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/Rss.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/Rss.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/Rss.scala 2025-01-15 05:30:37.144089689 +0000 @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.hub + +import java.time._ + +/** Data describing a RSS channel. + * + * @see + * https://www.rssboard.org/rss-specification + * + * @param title + * The name of the channel. It's how people refer to your service. If you have an HTML website that contains the same + * information as your RSS file, the title of your channel should be the same as the title of your website. + * @param link + * The URL to the HTML website corresponding to the channel. + * @param description + * Phrase or sentence describing the channel. + * @param copyright + * Copyright notice for content in the channel. + * @param image + * Specifies a GIF, JPEG or PNG image that can be displayed with the channel. + * @param items + * A channel may contain any number of <item>s. An item may represent a "story" -- much like a story in a newspaper + * or magazine; if so its description is a synopsis of the story, and the link points to the full story. + */ +final case class RssChannel( + title: String, + link: String, + description: String, + copyright: Option[String], + image: Option[RssChannelImage], + items: List[RssItem] +) + +/** Description of an image that may be displayed with an RSS channel. + * + * @param url + * The URL of a GIF, JPEG or PNG image that represents the channel. + * @param title + * Describes the image, it's used in the ALT attribute of the HTML <img> tag when the channel is rendered in HTML. + * @param link + * The URL of the site, when the channel is rendered, the image is a link to the site. (Note, in practice the image + * <title> and <link> should have the same value as the channel's <title> and <link>. + * @param description + * Text that is included in the TITLE attribute of the link formed around the image in the HTML rendering. + * @param height + * The height of the image in pixels. + * @param width + * The width of the image in pixels. + */ +final case class RssChannelImage( + url: String, + title: String, + link: String, + description: Option[String], + height: Option[Int], + width: Option[Int] +) + +/** Data describing an item (entry) within a RSS channel. + * + * @param title + * The title of the item. + * @param link + * The url of the item. + * @param description + * The item synopsis. + * @param author + * The author of the item which should per specification contain the email address but we may choose to not include + * it due to privacy reasons. + * @param guid + * A string that uniquely identifies the item. In regard of a repository it might be the full url to a patch hash. + * @param pubDate + * Indicates when the item was published. + */ +final case class RssItem( + title: String, + link: String, + description: String, + author: Option[String], + guid: Option[String], + pubDate: Option[OffsetDateTime] +) diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2025-01-15 05:30:37.144089689 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2025-01-15 05:30:37.148089695 +0000 @@ -709,6 +709,77 @@ } } yield resp + /** Logic for generating the RSS feed for the requested repository. + * + * @param csrf + * An optional CSRF-Token that shall be used. + * @param user + * An optional user account for whom the list of repositories shall be rendered. + * @param repositoryOwnerName + * The name of the user who owns the repository. + * @param repositoryName + * The actual name of the repository. + * @return + * An HTTP response containing the rendered RSS XML. + */ + def doShowRepositoryRssFeed( + csrf: Option[CsrfToken] + )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] = + for { + language <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en"))) + repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName) + repo = repoAndId.map(_._1) + response <- repo match { + case None => NotFound() + case Some(repo) => + for { + directory <- Sync[F].delay( + os.Path( + Paths.get( + darcsConfig.repositoriesDirectory.toPath.toString, + repositoryOwnerName.toString + ) + ) + ) + countChanges <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain("--count")) + numberOfChanges <- Sync[F].delay(countChanges.stdout.toList.mkString.trim.toInt) + maxCount = 15 + from = 1 + to = + if ((from + maxCount > numberOfChanges) && (numberOfChanges > 0)) + numberOfChanges + else + from + maxCount + next = if (to < numberOfChanges) Option(to + 1) else None + vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)( + Chain(s"--index=$from-$to", "--summary", "--xml-output") + ) + xmlLog <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString)) + patches <- Sync[F].delay((xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList) + repositoryBaseUri <- Sync[F].delay( + linkConfig.createFullUri( + Uri(path = + Uri.Path( + Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString)) + ) + ) + ) + ) + items = patches.map(_.toRssItem(repositoryBaseUri.addSegment("patch"))) + channel = RssChannel( + title = s"Smederee: ~$repositoryOwnerName/$repositoryName", + link = repositoryBaseUri.toString, + description = + repo.description.map(_.toString).getOrElse(s"RSS feed for $repositoryOwnerName/$repositoryName"), + copyright = None, + image = None, + items = items + ) + resp <- Ok(views.xml.rss(channel)) + } yield resp + } + } yield response + /** Get a diff for the requested patch and return the rendered response of it. * * @param csrf @@ -1510,6 +1581,16 @@ } yield resp } + private val showRepositoryRssFeed: HttpRoutes[F] = HttpRoutes.of { + case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( + repositoryName + ) / "feed.rss" => + for { + csrf <- Sync[F].delay(req.getCsrfToken) + resp <- doShowRepositoryRssFeed(csrf)(None)(repositoryOwnerName, repositoryName) + } yield resp + } + private val showRepositoryPatchDetails: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( repositoryName @@ -1540,7 +1621,8 @@ val routes = cloneRepository <+> downloadDistributionForGuests <+> showAllRepositoriesForGuests <+> showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryBranchesForGuests <+> - showRepositoryHistoryForGuests <+> showRepositoryPatchDetailsForGuests <+> showRepositoryFilesForGuests + showRepositoryHistoryForGuests <+> showRepositoryPatchDetailsForGuests <+> showRepositoryFilesForGuests <+> + showRepositoryRssFeed } diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala 2025-01-15 05:30:37.144089689 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala 2025-01-15 05:30:37.144089689 +0000 @@ -374,6 +374,8 @@ * an Instant representing the same instant, not null */ def toInstant: Instant = timestamp.toInstant + + def toOffsetDateTime: OffsetDateTime = timestamp } /** Summary construct for a file modification within a patch. @@ -481,6 +483,8 @@ object VcsRepositoryPatchMetadata { private val log = LoggerFactory.getLogger(classOf[VcsRepositoryPatchMetadata]) + private val replaceEmail = """<([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)>""".r + val DarcsCommentFilter: Regex = "^Ignore-this: [0-9a-f]+".r val DarcsDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss") @@ -517,6 +521,28 @@ None case scala.util.Success(patchMetadata) => patchMetadata } + + extension (patch: VcsRepositoryPatchMetadata) { + + /** Convert the patch metadata information into an RSS item that may be included in a RSS feed. + * + * @param baseUri + * The URI needed to access the patch on platform i.e. the address to which only the hash needs to be appended. + * @return + * An RSS item describing the patch. + */ + def toRssItem(baseUri: Uri): RssItem = { + val uri = baseUri.addSegment(patch.hash.toString).toString + RssItem( + title = patch.name.toString, + link = uri, + description = patch.comment.map(_.toString).getOrElse(patch.name.toString).replaceAll("\n", "<br />"), + author = replaceEmail.replaceAllIn(patch.author.toString, "").trim.some, + guid = uri.some, + pubDate = patch.timestamp.toOffsetDateTime.some + ) + } + } } /** Data about a VCS respository. diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/rss.scala.xml new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/rss.scala.xml --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/rss.scala.xml 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/rss.scala.xml 2025-01-15 05:30:37.148089695 +0000 @@ -0,0 +1,23 @@ +@(channel: de.smederee.hub.RssChannel) +<?xml version="1.0" encoding="utf-8"?> +@defining(java.time.format.DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz").withZone(java.time.ZoneOffset.UTC)) { formatter => +<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> + <channel> + <title>@channel.title</title> + <link>@channel.link</link> + <description>@channel.description</description> + @for(copyright <- channel.copyright) {<copyright>@copyright</copyright>} + <atom:link href="@channel.link/feed.rss" rel="self" type="application/rss+xml" /> + @for(item <- channel.items) { + <item> + <title>@item.title</title> + <link>@item.link</link> + <description>@item.description></description> + @for(author <- item.author) {<author>@author</author>} + @for(guid <- item.guid) {<guid>@guid</guid>} + @for(date <- item.pubDate) {<pubDate>@{date.format(formatter)}</pubDate>} + </item> + } + </channel> +</rss> +} diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html 2025-01-15 05:30:37.144089689 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html 2025-01-15 05:30:37.148089695 +0000 @@ -37,7 +37,7 @@ <div class="pure-g"> <div class="pure-u-1 pure-u-lg-3-5"> <div class="l-box"> - <h3>@Messages("repository.overview.latest-changes")</h3> + <h3>@Messages("repository.overview.latest-changes") <a href="@{actionBaseUri.addSegment("feed.rss")}" title="@Messages("repository.overview.rss-feed")">@icon(baseUri)("rss")</a></h3> <div class="overview-latest-changes"> @for(patch <- vcsRepositoryHistory) { @repositoryPatchMetadata(actionBaseUri.some, patch)