| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 50fd7ae commit 75459a1
16 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,5 +18,7 @@ class Constants { | |||
| 18 | 18 | const val SIM2 = "SIM2" | |
| 19 | 19 | ||
| 20 | 20 | const val TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'000000'ZZZZZ" | |
| 21 | + | ||
| 22 | + const val MAX_MMS_ATTACHMENT_SIZE = 1.5 * 1024 * 1024; | ||
| 21 | 23 | } | |
| 22 | 24 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,8 @@ import com.google.android.mms.pdu_alt.PduBody | |||
| 15 | 15 | import com.google.android.mms.pdu_alt.PduComposer | |
| 16 | 16 | import com.google.android.mms.pdu_alt.PduPart | |
| 17 | 17 | import com.google.android.mms.pdu_alt.SendReq | |
| 18 | + import okhttp3.MediaType | ||
| 19 | + import java.io.File | ||
| 18 | 20 | ||
| 19 | 21 | class MyFirebaseMessagingService : FirebaseMessagingService() { | |
| 20 | 22 | // [START receive_message] | |
@@ -177,20 +179,35 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { | |||
| 177 | 179 | return handleMultipartMessage(message, parts) | |
| 178 | 180 | } | |
| 179 | 181 | ||
| 182 | + fun extractFileName(url: String, prefix: String, mimeType: String? = null): String { | ||
| 183 | + val fileName = url.substringAfterLast("/") | ||
| 184 | + .substringBefore("?") | ||
| 185 | + .takeIf { it.isNotBlank() && it.contains(".") } | ||
| 186 | + ?: run { | ||
| 187 | + val extension = mimeType?.let { mime -> | ||
| 188 | + val ext = mime.substringAfterLast("/") | ||
| 189 | + if (ext.isNotBlank()) ".$ext" else "" | ||
| 190 | + } ?: "" | ||
| 191 | + "attachment$extension" | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + return "${prefix}_$fileName" | ||
| 195 | + } | ||
| 196 | + | ||
| 180 | 197 | private fun handleMmsMessage(message: Message): Result { | |
| 181 | 198 | Timber.d("Processing MMS for message ID [${message.id}]") | |
| 182 | 199 | val apiService = HttpSmsApiService.create(applicationContext) | |
| 183 | 200 | ||
| 184 | - val downloadedFiles = mutableListOf<java.io.File>() | ||
| 201 | + val downloadedFiles = mutableListOf<Pair<File, MediaType>>() | ||
| 185 | 202 | ||
| 186 | 203 | try { | |
| 187 | 204 | for ((index, attachment) in message.attachments!!.withIndex()) { | |
| 188 | - val file = apiService.downloadAttachment(applicationContext, attachment.url, message.id, index) | ||
| 189 | - if (file == null) { | ||
| 205 | + val file = apiService.downloadAttachment(applicationContext, attachment, message.id, index) | ||
| 206 | + if (file.first == null || file.second == null) { | ||
| 190 | 207 | handleFailed(applicationContext, message.id, "Failed to download attachment or file size exceeded 1.5MB.") | |
| 191 | 208 | return Result.failure() | |
| 192 | 209 | } | |
| 193 | - downloadedFiles.add(file) | ||
| 210 | + downloadedFiles.add(Pair(file.first!!, file.second!!)) | ||
| 194 | 211 | } | |
| 195 | 212 | ||
| 196 | 213 | val sendReq = SendReq() | |
@@ -207,30 +224,32 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { | |||
| 207 | 224 | textPart.name = "text".toByteArray() | |
| 208 | 225 | textPart.contentId = "text".toByteArray() | |
| 209 | 226 | textPart.contentLocation = "text".toByteArray() | |
| 210 | - | ||
| 227 | + | ||
| 211 | 228 | var messageBody = message.content | |
| 212 | 229 | val encryptionKey = Settings.getEncryptionKey(applicationContext) | |
| 213 | 230 | if (message.encrypted && !encryptionKey.isNullOrEmpty()) { | |
| 214 | 231 | messageBody = Encrypter.decrypt(encryptionKey, messageBody) | |
| 215 | 232 | } | |
| 216 | 233 | textPart.data = messageBody.toByteArray(Charsets.UTF_8) | |
| 217 | - | ||
| 234 | + | ||
| 218 | 235 | pduBody.addPart(textPart) | |
| 219 | 236 | } | |
| 220 | 237 | ||
| 221 | 238 | for ((index, file) in downloadedFiles.withIndex()) { | |
| 222 | - val attachment = message.attachments[index] | ||
| 223 | - val fileBytes = file.readBytes() | ||
| 239 | + val fileBytes = file.first.readBytes() | ||
| 224 | 240 | ||
| 225 | 241 | val mediaPart = PduPart() | |
| 226 | - mediaPart.contentType = attachment.contentType.toByteArray() | ||
| 227 | - | ||
| 228 | - val fileName = "attachment_$index".toByteArray() | ||
| 229 | - mediaPart.name = fileName | ||
| 230 | - mediaPart.contentId = fileName | ||
| 231 | - mediaPart.contentLocation = fileName | ||
| 242 | + mediaPart.contentType = file.second.toString().toByteArray() | ||
| 243 | + | ||
| 244 | + | ||
| 245 | + val fileName = extractFileName(message.attachments[index], index.toString(), file.second.toString()) | ||
| 246 | + mediaPart.name = fileName.toByteArray() | ||
| 247 | + mediaPart.contentId = fileName.toByteArray() | ||
| 248 | + mediaPart.contentLocation = fileName.toByteArray() | ||
| 232 | 249 | mediaPart.data = fileBytes | |
| 233 | - | ||
| 250 | + | ||
| 251 | + Timber.d("Adding MMS attachment with name [$fileName] and size [${fileBytes.size}] and type [${file.second}]") | ||
| 252 | + | ||
| 234 | 253 | pduBody.addPart(mediaPart) | |
| 235 | 254 | } | |
| 236 | 255 | ||
@@ -249,7 +268,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { | |||
| 249 | 268 | if (!mmsDir.exists()) { | |
| 250 | 269 | mmsDir.mkdirs() | |
| 251 | 270 | } | |
| 252 | - | ||
| 271 | + | ||
| 253 | 272 | val pduFile = java.io.File(mmsDir, "pdu_${message.id}.dat") | |
| 254 | 273 | java.io.FileOutputStream(pduFile).use { it.write(pduBytes) } | |
| 255 | 274 | ||
@@ -272,15 +291,15 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { | |||
| 272 | 291 | } finally { | |
| 273 | 292 | // Clean up any downloaded temporary files | |
| 274 | 293 | downloadedFiles.forEach { file -> | |
| 275 | - if (file.exists()) { | ||
| 276 | - file.delete() | ||
| 294 | + if (file.first.exists()) { | ||
| 295 | + file.first.delete() | ||
| 277 | 296 | } | |
| 278 | 297 | } | |
| 279 | 298 | ||
| 280 | 299 | // Also clean up the MMS PDU file to avoid cache buildup in cases where | |
| 281 | 300 | // sendMultimediaMessage fails before the sent broadcast is delivered. | |
| 282 | 301 | try { | |
| 283 | - val pduFile = java.io.File(applicationContext.cacheDir, "pdu_${message.id}.dat") | ||
| 302 | + val pduFile = File(applicationContext.cacheDir, "pdu_${message.id}.dat") | ||
| 284 | 303 | if (pduFile.exists()) { | |
| 285 | 304 | val deleted = pduFile.delete() | |
| 286 | 305 | if (!deleted) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,21 +1,23 @@ | |||
| 1 | 1 | package com.httpsms | |
| 2 | 2 | ||
| 3 | 3 | import android.content.Context | |
| 4 | + import com.httpsms.Constants.Companion.MAX_MMS_ATTACHMENT_SIZE | ||
| 5 | + import okhttp3.MediaType | ||
| 4 | 6 | import okhttp3.MediaType.Companion.toMediaType | |
| 5 | 7 | import okhttp3.OkHttpClient | |
| 6 | 8 | import okhttp3.Request | |
| 7 | 9 | import okhttp3.RequestBody.Companion.toRequestBody | |
| 8 | 10 | import org.apache.commons.text.StringEscapeUtils | |
| 9 | 11 | import timber.log.Timber | |
| 10 | - import java.net.URI | ||
| 11 | - import java.net.URL | ||
| 12 | - import java.util.logging.Level | ||
| 13 | - import java.util.logging.Logger.getLogger | ||
| 14 | 12 | import java.io.File | |
| 15 | 13 | import java.io.FileOutputStream | |
| 16 | 14 | import java.io.IOException | |
| 17 | 15 | import java.io.InputStream | |
| 18 | 16 | import java.io.OutputStream | |
| 17 | + import java.net.URI | ||
| 18 | + import java.net.URL | ||
| 19 | + import java.util.logging.Level | ||
| 20 | + import java.util.logging.Logger.getLogger | ||
| 19 | 21 | ||
| 20 | 22 | ||
| 21 | 23 | class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { | |
@@ -162,49 +164,43 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { | |||
| 162 | 164 | } | |
| 163 | 165 | ||
| 164 | 166 | fun InputStream.copyToWithLimit( | |
| 165 | - out: OutputStream, | ||
| 166 | - limit: Long, | ||
| 167 | + out: OutputStream, | ||
| 168 | + limit: Long, | ||
| 167 | 169 | bufferSize: Int = DEFAULT_BUFFER_SIZE | |
| 168 | 170 | ): Long { | |
| 169 | 171 | var bytesCopied: Long = 0 | |
| 170 | 172 | val buffer = ByteArray(bufferSize) | |
| 171 | 173 | var bytes = read(buffer) | |
| 172 | - | ||
| 174 | + | ||
| 173 | 175 | while (bytes >= 0) { | |
| 174 | 176 | bytesCopied += bytes | |
| 175 | - | ||
| 177 | + | ||
| 176 | 178 | if (bytesCopied > limit) { | |
| 177 | 179 | throw IOException("Download aborted: File exceeded maximum allowed size of $limit bytes.") | |
| 178 | 180 | } | |
| 179 | - | ||
| 181 | + | ||
| 180 | 182 | out.write(buffer, 0, bytes) | |
| 181 | 183 | bytes = read(buffer) | |
| 182 | 184 | } | |
| 183 | 185 | return bytesCopied | |
| 184 | 186 | } | |
| 185 | 187 | ||
| 186 | 188 | // Downloads the attachment URL content locally | |
| 187 | - fun downloadAttachment(context: Context, urlString: String, messageId: String, attachmentIndex: Int): File? { | ||
| 189 | + fun downloadAttachment(context: Context, urlString: String, messageId: String, attachmentIndex: Int): Pair<File?, MediaType?> { | ||
| 188 | 190 | val request = Request.Builder().url(urlString).build() | |
| 189 | 191 | ||
| 190 | 192 | try { | |
| 191 | 193 | client.newCall(request).execute().use { response -> | |
| 192 | 194 | if (!response.isSuccessful) { | |
| 193 | 195 | Timber.e("Failed to download attachment: ${response.code}") | |
| 194 | - return null | ||
| 196 | + return Pair(null, null) | ||
| 195 | 197 | } | |
| 196 | 198 | ||
| 197 | 199 | val body = response.body | |
| 198 | - if (body == null) { | ||
| 199 | - Timber.e("Failed to download attachment: response body is null") | ||
| 200 | - return null | ||
| 201 | - } | ||
| 202 | - | ||
| 203 | - val maxSizeBytes = 1.5 * 1024 * 1024 // most (modern?) carriers have a 2MB limit, so targetting 1.5MB should be safe | ||
| 204 | 200 | val contentLength = body.contentLength() | |
| 205 | - if (contentLength > maxSizeBytes) { | ||
| 201 | + if (contentLength > MAX_MMS_ATTACHMENT_SIZE) { | ||
| 206 | 202 | Timber.e("Attachment is too large ($contentLength bytes).") | |
| 207 | - return null | ||
| 203 | + return Pair(null, null) | ||
| 208 | 204 | } | |
| 209 | 205 | ||
| 210 | 206 | val mmsDir = File(context.cacheDir, "mms_attachments") | |
@@ -216,15 +212,15 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { | |||
| 216 | 212 | val inputStream = body.byteStream() | |
| 217 | 213 | FileOutputStream(tempFile).use { outputStream -> | |
| 218 | 214 | inputStream.use { input -> | |
| 219 | - input.copyToWithLimit(outputStream, maxSizeBytes.toLong()) | ||
| 215 | + input.copyToWithLimit(outputStream, MAX_MMS_ATTACHMENT_SIZE.toLong()) | ||
| 220 | 216 | } | |
| 221 | 217 | } | |
| 222 | 218 | ||
| 223 | - return tempFile | ||
| 219 | + return Pair(tempFile, response.body.contentType()) | ||
| 224 | 220 | } | |
| 225 | 221 | } catch (e: Exception) { | |
| 226 | 222 | Timber.e(e, "Exception while downloading attachment") | |
| 227 | - return null | ||
| 223 | + return Pair(null, null) | ||
| 228 | 224 | } | |
| 229 | 225 | } | |
| 230 | 226 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -29,14 +29,6 @@ data class Phone ( | |||
| 29 | 29 | val userID: String, | |
| 30 | 30 | ) | |
| 31 | 31 | ||
| 32 | - // mms attachment | ||
| 33 | - data class Attachment ( | ||
| 34 | - @Json(name = "content_type") | ||
| 35 | - val contentType: String, | ||
| 36 | - | ||
| 37 | - val url: String | ||
| 38 | - ) | ||
| 39 | - | ||
| 40 | 32 | data class Message ( | |
| 41 | 33 | val contact: String, | |
| 42 | 34 | val content: String, | |
@@ -78,5 +70,5 @@ data class Message ( | |||
| 78 | 70 | @Json(name = "updated_at") | |
| 79 | 71 | val updatedAt: String, | |
| 80 | 72 | ||
| 81 | - val attachments: List<Attachment>? = null | ||
| 73 | + val attachments: List<String>? = null | ||
| 82 | 74 | ) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,10 +1,10 @@ | |||
| 1 | 1 | package entities | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - "strings" | ||
| 5 | 4 | "time" | |
| 6 | 5 | ||
| 7 | 6 | "github.com/google/uuid" | |
| 7 | + "github.com/lib/pq" | ||
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | 10 | // MessageType is the type of message if it is incoming or outgoing | |
@@ -82,23 +82,18 @@ func (s SIM) String() string { | |||
| 82 | 82 | return string(s) | |
| 83 | 83 | } | |
| 84 | 84 | ||
| 85 | - type MessageAttachment struct { | ||
| 86 | - ContentType string `json:"content_type" example:"image/jpeg"` | ||
| 87 | - URL string `json:"url" example:"https://example.com/image.jpg"` | ||
| 88 | - } | ||
| 89 | - | ||
| 90 | 85 | // Message represents a message sent between 2 phone numbers | |
| 91 | 86 | type Message struct { | |
| 92 | - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` | ||
| 93 | - RequestID *string `json:"request_id" example:"153554b5-ae44-44a0-8f4f-7bbac5657ad4" validate:"optional"` | ||
| 94 | - Owner string `json:"owner" example:"+18005550199"` | ||
| 95 | - UserID UserID `json:"user_id" gorm:"index:idx_messages__user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` | ||
| 96 | - Contact string `json:"contact" example:"+18005550100"` | ||
| 97 | - Content string `json:"content" example:"This is a sample text message"` | ||
| 98 | - Attachments []MessageAttachment `json:"attachments,omitempty" gorm:"type:json;serializer:json"` | ||
| 99 | - Encrypted bool `json:"encrypted" example:"false" gorm:"default:false"` | ||
| 100 | - Type MessageType `json:"type" example:"mobile-terminated"` | ||
| 101 | - Status MessageStatus `json:"status" example:"pending"` | ||
| 87 | + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` | ||
| 88 | + RequestID *string `json:"request_id" example:"153554b5-ae44-44a0-8f4f-7bbac5657ad4" validate:"optional"` | ||
| 89 | + Owner string `json:"owner" example:"+18005550199"` | ||
| 90 | + UserID UserID `json:"user_id" gorm:"index:idx_messages__user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` | ||
| 91 | + Contact string `json:"contact" example:"+18005550100"` | ||
| 92 | + Content string `json:"content" example:"This is a sample text message"` | ||
| 93 | + Attachments pq.StringArray `json:"attachments" gorm:"type:text[]" swaggertype:"array,string"` | ||
| 94 | + Encrypted bool `json:"encrypted" example:"false" gorm:"default:false"` | ||
| 95 | + Type MessageType `json:"type" example:"mobile-terminated"` | ||
| 96 | + Status MessageStatus `json:"status" example:"pending"` | ||
| 102 | 97 | // SIM is the SIM card to use to send the message | |
| 103 | 98 | // * SMS1: use the SIM card in slot 1 | |
| 104 | 99 | // * SMS2: use the SIM card in slot 2 | |
@@ -234,19 +229,3 @@ func (message *Message) updateOrderTimestamp(timestamp time.Time) { | |||
| 234 | 229 | message.OrderTimestamp = timestamp | |
| 235 | 230 | } | |
| 236 | 231 | } | |
| 237 | - | ||
| 238 | - func GetAttachmentContentType(url string) string { | ||
| 239 | - // Since there's no easy way to set a type in the CSV, defaulting to octet-stream and then just checking the file extension in the URL | ||
| 240 | - contentType := "application/octet-stream" | ||
| 241 | - lowerURL := strings.ToLower(url) | ||
| 242 | - if strings.HasSuffix(lowerURL, ".jpg") || strings.HasSuffix(lowerURL, ".jpeg") { | ||
| 243 | - contentType = "image/jpeg" | ||
| 244 | - } else if strings.HasSuffix(lowerURL, ".png") { | ||
| 245 | - contentType = "image/png" | ||
| 246 | - } else if strings.HasSuffix(lowerURL, ".gif") { | ||
| 247 | - contentType = "image/gif" | ||
| 248 | - } else if strings.HasSuffix(lowerURL, ".mp4") { | ||
| 249 | - contentType = "video/mp4" | ||
| 250 | - } | ||
| 251 | - return contentType | ||
| 252 | - } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,16 +13,16 @@ const EventTypeMessageAPISent = "message.api.sent" | |||
| 13 | 13 | ||
| 14 | 14 | // MessageAPISentPayload is the payload of the EventTypeMessageSent event | |
| 15 | 15 | type MessageAPISentPayload struct { | |
| 16 | - MessageID uuid.UUID `json:"message_id"` | ||
| 17 | - UserID entities.UserID `json:"user_id"` | ||
| 18 | - Owner string `json:"owner"` | ||
| 19 | - RequestID *string `json:"request_id"` | ||
| 20 | - MaxSendAttempts uint `json:"max_send_attempts"` | ||
| 21 | - Contact string `json:"contact"` | ||
| 22 | - ScheduledSendTime *time.Time `json:"scheduled_send_time"` | ||
| 23 | - RequestReceivedAt time.Time `json:"request_received_at"` | ||
| 24 | - Content string `json:"content"` | ||
| 25 | - Attachments []entities.MessageAttachment `json:"attachments"` | ||
| 26 | - Encrypted bool `json:"encrypted"` | ||
| 27 | - SIM entities.SIM `json:"sim"` | ||
| 16 | + MessageID uuid.UUID `json:"message_id"` | ||
| 17 | + UserID entities.UserID `json:"user_id"` | ||
| 18 | + Owner string `json:"owner"` | ||
| 19 | + RequestID *string `json:"request_id"` | ||
| 20 | + MaxSendAttempts uint `json:"max_send_attempts"` | ||
| 21 | + Contact string `json:"contact"` | ||
| 22 | + ScheduledSendTime *time.Time `json:"scheduled_send_time"` | ||
| 23 | + RequestReceivedAt time.Time `json:"request_received_at"` | ||
| 24 | + Content string `json:"content"` | ||
| 25 | + Attachments []string `json:"attachments"` | ||
| 26 | + Encrypted bool `json:"encrypted"` | ||
| 27 | + SIM entities.SIM `json:"sim"` | ||
| 28 | 28 | } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments