Skip to content

Commit

Permalink
feat: newsletter request (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicopico-dev authored Apr 13, 2024
1 parent c626ba8 commit ece4623
Show file tree
Hide file tree
Showing 23 changed files with 890 additions and 70 deletions.
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-validation")
developmentOnly("org.springframework.boot:spring-boot-devtools")
developmentOnly("org.springframework.boot:spring-boot-docker-compose")

Expand All @@ -66,6 +67,8 @@ dependencies {
implementation("org.jsoup:jsoup:1.17.2")
implementation("com.rometools:rome:2.1.0")

implementation("com.jayway.jsonpath:json-path:2.9.0")

testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.mockito")
}
Expand All @@ -75,6 +78,7 @@ dependencies {
testImplementation("io.mockk:mockk:1.13.10")
testImplementation("com.icegreen:greenmail:2.0.1")
testImplementation("com.icegreen:greenmail-junit5:2.0.1")
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
}

tasks.withType<KotlinCompile> {
Expand Down
6 changes: 3 additions & 3 deletions config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -576,11 +576,11 @@ style:
active: true
comments:
- reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
value: 'FIXME:'
value: 'FIXME'
- reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
value: 'STOPSHIP:'
value: 'STOPSHIP'
- reason: 'Forbidden TODO todo marker in comment, please do the changes.'
value: 'TODO:'
value: 'TODO'
allowedPatterns: ''
ForbiddenImport:
active: false
Expand Down
18 changes: 18 additions & 0 deletions n2rss.http
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,21 @@ X-Secret-Key: abcd

### Content-Type
GET http://localhost:8080/rss/pointer

### Request (Success)
POST http://localhost:8080/send-request
Content-Type: application/x-www-form-urlencoded

newsletterUrl = https://www.some.fo

### Request (FAIL)
POST http://localhost:8080/send-request
Content-Type: application/x-www-form-urlencoded

newsletterUrl = pouet

### Request (almost same)
POST http://localhost:8080/send-request
Content-Type: application/x-www-form-urlencoded

newsletterUrl = https://www.some.fo/
10 changes: 8 additions & 2 deletions src/main/kotlin/fr/nicopico/n2rss/config/N2RssProperties.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ data class N2RssProperties
@ConstructorBinding
constructor(
val maintenance: MaintenanceProperties,
val feeds: FeedsProperties,
val feeds: FeedsProperties = FeedsProperties(),
val recaptcha: ReCaptchaProperties,
) {
data class MaintenanceProperties(
val secretKey: String,
)
data class FeedsProperties(
val forceHttps: Boolean,
val forceHttps: Boolean = true,
)
data class ReCaptchaProperties(
val enabled: Boolean = true,
val siteKey: String,
val secretKey: String,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,38 @@ package fr.nicopico.n2rss.controller.home

import fr.nicopico.n2rss.config.N2RssProperties
import fr.nicopico.n2rss.service.NewsletterService
import fr.nicopico.n2rss.service.ReCaptchaService
import fr.nicopico.n2rss.utils.Url
import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.ConstraintViolationException
import jakarta.validation.constraints.NotEmpty
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import java.net.URL

@Validated
@Controller
class HomeController(
private val newsletterService: NewsletterService,
props: N2RssProperties,
private val reCaptchaService: ReCaptchaService,
private val properties: N2RssProperties,
) {
private val properties = props.feeds

@GetMapping("/")
fun home(request: HttpServletRequest, model: Model): String {
val newslettersInfo = newsletterService.getNewslettersInfo()
.filter { it.publicationCount > 0 }
val requestUrl: String = request.requestURL
.let {
if (properties.forceHttps) {
if (properties.feeds.forceHttps) {
it.replace(Regex("http://"), "https://")
} else {
it.toString()
Expand All @@ -47,7 +60,42 @@ class HomeController(
with(model) {
addAttribute("newsletters", newslettersInfo)
addAttribute("requestUrl", requestUrl)
addAttribute("reCaptchaEnabled", properties.recaptcha.enabled)
addAttribute("reCaptchaSiteKey", properties.recaptcha.siteKey)
}
return "index"
}

@PostMapping("/send-request")
fun requestNewsletter(
@NotEmpty @Url @RequestParam("newsletterUrl") newsletterUrl: String,
@RequestParam("g-recaptcha-response") captchaResponse: String? = null,
): ResponseEntity<String> {
val isCaptchaValid = if (properties.recaptcha.enabled) {
reCaptchaService.isCaptchaValid(
captchaSecretKey = properties.recaptcha.secretKey,
captchaResponse = captchaResponse ?: "",
)
} else true

return if (isCaptchaValid) {
newsletterService.saveRequest(URL(newsletterUrl))
ResponseEntity.ok().build()
} else {
ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("reCaptcha challenge failed")
}
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException::class)
fun handleExceptions(
exception: ConstraintViolationException,
): ResponseEntity<Map<String, String?>> {
val errors = exception.constraintViolations.associate {
it.propertyPath.toString() to it.message
}
return ResponseEntity.badRequest().body(errors)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2024 Nicolas PICON
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions
* of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/

package fr.nicopico.n2rss.data

import fr.nicopico.n2rss.models.NewsletterRequest
import org.springframework.data.mongodb.repository.MongoRepository
import java.net.URL

interface NewsletterRequestRepository : MongoRepository<NewsletterRequest, URL> {
fun getByNewsletterUrl(url: URL): NewsletterRequest?
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ package fr.nicopico.n2rss.mail.newsletter
import fr.nicopico.n2rss.models.Article
import fr.nicopico.n2rss.models.Email
import fr.nicopico.n2rss.models.Newsletter
import fr.nicopico.n2rss.utils.toURL
import fr.nicopico.n2rss.utils.toUrlOrNull
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
Expand Down Expand Up @@ -61,7 +61,7 @@ class AndroidWeeklyNewsletterHandler : NewsletterHandler {
.filter { it -> it.text().isNotBlank() }
.mapNotNull { tag ->
// Ignore entries with invalid link
tag.attr("href").toURL()
tag.attr("href").toUrlOrNull()
?.let { link ->
val title = tag.text().trim()
Article(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import fr.nicopico.n2rss.mail.newsletter.jsoup.process
import fr.nicopico.n2rss.models.Article
import fr.nicopico.n2rss.models.Email
import fr.nicopico.n2rss.models.Newsletter
import fr.nicopico.n2rss.utils.toURL
import fr.nicopico.n2rss.utils.toUrlOrNull
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.safety.Safelist
Expand Down Expand Up @@ -54,7 +54,7 @@ class KotlinWeeklyNewsletterHandler : NewsletterHandler {
sectionDocument.select("a[href]")
.mapNotNull { tag ->
// Ignore entries with invalid link
val link = tag.attr("href").toURL()
val link = tag.attr("href").toUrlOrNull()
?: return@mapNotNull null
val title = markSponsoredTitle(section, tag.text()).trim()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ package fr.nicopico.n2rss.mail.newsletter
import fr.nicopico.n2rss.models.Article
import fr.nicopico.n2rss.models.Email
import fr.nicopico.n2rss.models.Newsletter
import fr.nicopico.n2rss.utils.toURL
import fr.nicopico.n2rss.utils.toUrlOrNull
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
Expand Down Expand Up @@ -66,7 +66,7 @@ class PointerNewsletterHandler : NewsletterHandler {
}

val sponsorSubtitleElement = sponsorSection.selectFirst("a[href]:has(strong:has(span))")
val sponsorLink = sponsorSubtitleElement?.attr("href")?.toURL()
val sponsorLink = sponsorSubtitleElement?.attr("href")?.toUrlOrNull()

return if (sponsorSubtitleElement != null && sponsorLink != null) {
val sponsorName = sponsorSection.select("p")
Expand Down Expand Up @@ -104,7 +104,7 @@ class PointerNewsletterHandler : NewsletterHandler {

val links = articleSectionDocument.select("a[href]:has(strong:has(span))")
val articles = links.mapNotNull { articleTitle ->
val link = articleTitle.attr("href").toURL()
val link = articleTitle.attr("href").toUrlOrNull()
?: return@mapNotNull null
val title = articleTitle.text()
val description = articleTitle.findDescription()
Expand Down
32 changes: 32 additions & 0 deletions src/main/kotlin/fr/nicopico/n2rss/models/NewsletterRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 Nicolas PICON
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions
* of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package fr.nicopico.n2rss.models

import kotlinx.datetime.LocalDateTime
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
import java.net.URL

@Document("newsletter_request")
data class NewsletterRequest(
@Id
val newsletterUrl: URL,
val firstRequestDate: LocalDateTime,
val lastRequestDate: LocalDateTime,
val requestCount: Int,
)
34 changes: 34 additions & 0 deletions src/main/kotlin/fr/nicopico/n2rss/service/NewsletterService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,23 @@

package fr.nicopico.n2rss.service

import fr.nicopico.n2rss.data.NewsletterRequestRepository
import fr.nicopico.n2rss.data.PublicationRepository
import fr.nicopico.n2rss.mail.newsletter.NewsletterHandler
import fr.nicopico.n2rss.models.NewsletterInfo
import fr.nicopico.n2rss.models.NewsletterRequest
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.net.URL

@Service
class NewsletterService(
private val newsletterHandlers: List<NewsletterHandler>,
private val publicationRepository: PublicationRepository,
private val newsletterRequestRepository: NewsletterRequestRepository,
) {
fun getNewslettersInfo(): List<NewsletterInfo> {
return newsletterHandlers
Expand All @@ -41,4 +49,30 @@ class NewsletterService(
)
}
}

@Transactional
fun saveRequest(newsletterUrl: URL) {
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())

// Sanitize URL
val uniqueUrl = URL(
/* protocol = */ "https",
/* host = */ newsletterUrl.host,
/* port = */ newsletterUrl.port,
/* file = */ "",
)
val request = newsletterRequestRepository.getByNewsletterUrl(uniqueUrl)

val updatedRequest = request?.copy(
lastRequestDate = now,
requestCount = request.requestCount + 1
) ?: NewsletterRequest(
newsletterUrl = uniqueUrl,
firstRequestDate = now,
lastRequestDate = now,
requestCount = 1,
)

newsletterRequestRepository.save(updatedRequest)
}
}
Loading

0 comments on commit ece4623

Please sign in to comment.