Skip to content

Commit 0da5c3d

Browse files
authored
Added report issue endpoint (#237)
Signed-off-by: Arnau Mora <[email protected]>
1 parent b5c05ed commit 0da5c3d

File tree

6 files changed

+135
-12
lines changed

6 files changed

+135
-12
lines changed

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ dependencies {
5353
// Firebase
5454
implementation(libs.firebase.admin)
5555

56+
// For sending emails
57+
implementation(libs.sendgrid)
58+
5659

5760
testImplementation(libs.kotlin.test)
5861

gradle/libs.versions.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
[versions]
2-
crowdin = "1.25.0"
32
exposed = "0.61.0"
43
firebase-admin = "9.4.3"
54
h2 = "2.3.232"
@@ -10,10 +9,9 @@ kotlin-serialization = "1.8.1"
109
ktor = "3.1.3"
1110
postgresql = "42.7.5"
1211
progressbar = "0.10.1"
13-
sqlite = "3.49.1.0"
12+
sendgrid = "4.10.3"
1413

1514
[libraries]
16-
crowdin = { module = "com.github.crowdin:crowdin-api-client-java", version.ref = "crowdin" }
1715
firebase-admin = { module = "com.google.firebase:firebase-admin", version.ref = "firebase-admin" }
1816
h2 = { module = "com.h2database:h2", version.ref = "h2" }
1917
imageio-webp = { module = "org.sejda.imageio:webp-imageio", version.ref = "imageio-webp" }
@@ -39,7 +37,7 @@ exposed-javaTime = { module = "org.jetbrains.exposed:exposed-java-time", version
3937
exposed-json = { module = "org.jetbrains.exposed:exposed-json", version.ref = "exposed" }
4038
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
4139
progressbar = { module = "me.tongfei:progressbar", version.ref = "progressbar" }
42-
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
40+
sendgrid = { module = "com.sendgrid:sendgrid-java", version.ref = "sendgrid" }
4341

4442
[plugins]
4543
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package server.endpoints.contact
2+
3+
import com.sendgrid.Method
4+
import com.sendgrid.Request
5+
import com.sendgrid.SendGrid
6+
import com.sendgrid.helpers.mail.Mail
7+
import com.sendgrid.helpers.mail.objects.Attachments
8+
import com.sendgrid.helpers.mail.objects.Content
9+
import com.sendgrid.helpers.mail.objects.Email
10+
import io.ktor.http.HttpStatusCode
11+
import io.ktor.server.routing.RoutingContext
12+
import io.ktor.util.encodeBase64
13+
import java.io.File
14+
import server.endpoints.EndpointBase
15+
import server.request.saveFile
16+
import server.response.respondSuccess
17+
import system.EnvironmentVariables
18+
19+
object ReportIssueEndpoint : EndpointBase("/report") {
20+
private val fromEmailAddress = EnvironmentVariables.Services.SendGrid.FromEmail.value!!
21+
private val toEmailAddress = EnvironmentVariables.Services.SendGrid.ToEmail.value!!
22+
23+
override suspend fun RoutingContext.endpoint() {
24+
val sendGridApiKey = EnvironmentVariables.Services.SendGrid.ApiKey.value
25+
if (sendGridApiKey == null) {
26+
return respondSuccess(HttpStatusCode.ServiceUnavailable)
27+
}
28+
29+
var name: String? = null
30+
var email: String? = null
31+
var message: String? = null
32+
val attachments = mutableListOf<File>()
33+
34+
receiveMultipart(
35+
forEachFormItem = { item ->
36+
when (item.name) {
37+
"name" -> name = item.value
38+
"email" -> email = item.value
39+
"message" -> message = item.value
40+
}
41+
},
42+
forEachFileItem = { partData ->
43+
val tempFile = File.createTempFile("report_issue_", null).apply {
44+
deleteOnExit()
45+
}
46+
partData.saveFile(tempFile)
47+
attachments.add(tempFile)
48+
},
49+
)
50+
51+
if (message.isNullOrEmpty()) {
52+
return respondSuccess(HttpStatusCode.NoContent)
53+
}
54+
55+
val sg = SendGrid(sendGridApiKey)
56+
val request = Request().apply {
57+
method = Method.POST
58+
endpoint = "mail/send"
59+
60+
val fromEmail = Email(fromEmailAddress)
61+
val toEmail = Email(toEmailAddress)
62+
body = Mail(
63+
fromEmail,
64+
"New report from ${name ?: "N/A"}",
65+
toEmail,
66+
Content("text/plain", message),
67+
).apply {
68+
val replyTo = email?.let { Email(it) }
69+
name?.let { replyTo?.name = it }
70+
replyTo?.let { setReplyTo(it) }
71+
72+
attachments.forEach { file ->
73+
addAttachments(
74+
Attachments().apply {
75+
content = file.readBytes().encodeBase64()
76+
type = "application/octet-stream"
77+
filename = file.name
78+
disposition = "attachment"
79+
}
80+
)
81+
}
82+
}.build()
83+
}
84+
val response = sg.api(request)
85+
86+
if (response.statusCode in 200..299) {
87+
respondSuccess()
88+
} else {
89+
respondSuccess(HttpStatusCode.InternalServerError)
90+
}
91+
}
92+
}

src/main/kotlin/server/plugins/Routing.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import server.endpoints.blocking.DeleteBlockEndpoint
1313
import server.endpoints.blocking.GetAllBlocksEndpoint
1414
import server.endpoints.blocking.GetBlockEndpoint
1515
import server.endpoints.blocking.PatchBlockEndpoint
16+
import server.endpoints.contact.ReportIssueEndpoint
1617
import server.endpoints.create.NewAreaEndpoint
1718
import server.endpoints.create.NewPathEndpoint
1819
import server.endpoints.create.NewSectorEndpoint
@@ -87,5 +88,7 @@ fun Application.configureEndpoints() {
8788
get(RequestFileEndpoint)
8889
get(RequestFilesEndpoint)
8990
get(DownloadFileEndpoint)
91+
92+
post(ReportIssueEndpoint)
9093
}
9194
}

src/main/kotlin/server/request/MultipartExtensions.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,16 @@ import kotlin.io.path.exists
1010
import kotlin.io.path.outputStream
1111

1212
/**
13-
* Saves the FileItem to a specified root directory. Creates any necessary parent directories.
13+
* Saves the FileItem to a specified file. Creates any necessary parent directories.
1414
*
15-
* @param rootDir The root directory to save the FileItem to.
16-
* @param uuid The UUID for the name of the file. Defaults to a random one.
15+
* @param file The file to save the FileItem to.
1716
*
1817
* @throws IOException If there's a problem while writing to the file system.
1918
*/
20-
suspend fun PartData.FileItem.save(rootDir: File, uuid: UUID? = null): File {
21-
rootDir.mkdirs()
19+
suspend fun PartData.FileItem.saveFile(file: File): File {
20+
file.parentFile.mkdirs()
2221

23-
val fileExtension = originalFileName?.takeLastWhile { it != '.' }
24-
val fileName = "${uuid ?: UUID.randomUUID()}.$fileExtension"
25-
val targetFile = File(rootDir, fileName).toPath()
22+
val targetFile = file.toPath()
2623

2724
if (targetFile.exists()) {
2825
Files.delete(targetFile)
@@ -37,3 +34,21 @@ suspend fun PartData.FileItem.save(rootDir: File, uuid: UUID? = null): File {
3734

3835
return targetFile.toFile()
3936
}
37+
38+
/**
39+
* Saves the FileItem to a specified root directory. Creates any necessary parent directories.
40+
*
41+
* @param rootDir The root directory to save the FileItem to.
42+
* @param uuid The UUID for the name of the file. Defaults to a random one.
43+
*
44+
* @throws IOException If there's a problem while writing to the file system.
45+
*/
46+
suspend fun PartData.FileItem.save(rootDir: File, uuid: UUID? = null): File {
47+
rootDir.mkdirs()
48+
49+
val fileExtension = originalFileName?.takeLastWhile { it != '.' }
50+
val fileName = "${uuid ?: UUID.randomUUID()}.$fileExtension"
51+
val targetFile = File(rootDir, fileName)
52+
53+
return saveFile(targetFile)
54+
}

src/main/kotlin/system/EnvironmentVariables.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ object EnvironmentVariables {
8484
"/var/lib/escalaralcoiaicomtat/google-services.json"
8585
)
8686
}
87+
88+
@KoverIgnore
89+
object SendGrid {
90+
@KoverIgnore
91+
data object ApiKey : EnvironmentVariable("SENDGRID_API_KEY")
92+
93+
@KoverIgnore
94+
data object FromEmail : EnvironmentVariable("FROM_EMAIL", "[email protected]")
95+
96+
@KoverIgnore
97+
data object ToEmail : EnvironmentVariable("TO_EMAIL", "[email protected]")
98+
}
8799
}
88100

89101
@KoverIgnore

0 commit comments

Comments
 (0)