diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 849f5ec84..f9b756ed6 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -18,10 +18,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: stable @@ -74,6 +74,12 @@ jobs: docker compose wait seed || true sleep 2 + - name: Run Handler Integration Tests + working-directory: ./api + env: + USER_API_KEY: test-user-api-key + run: go test -tags integration -v -timeout 60s ./pkg/handlers + - name: Run Integration Tests working-directory: ./tests run: go test -v -timeout 300s ./... diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 3f893d1f7..50813d153 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -34,10 +34,10 @@ jobs: - uses: pnpm/action-setup@v6 name: Install pnpm with: - version: 11 + version: 11.11.0 - name: Install dependencies 📦 - run: pnpm install + run: pnpm install --trust-lockfile - name: Run linter 👀 run: pnpm lint @@ -58,13 +58,18 @@ jobs: - name: Checkout 🛎 uses: actions/checkout@master + - name: Setup Node 🟢 + uses: actions/setup-node@v7 + with: + node-version: '24.18' + - uses: pnpm/action-setup@v6 name: Install pnpm with: - version: 11 + version: 11.11.0 - name: Install dependencies 📦 - run: pnpm install + run: pnpm install --trust-lockfile - name: Build 🏗️ run: mv .env.production .env && echo "GITHUB_SHA=${GITHUB_SHA}" >> .env && pnpm run generate diff --git a/.gitignore b/.gitignore index e15d590a9..8f4caf4b5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ SECURITY_AUDIT_REPORT.md *.exe -docs/ .output .agents/ skills-lock.json diff --git a/README.md b/README.md index 14690530a..42a6b0d36 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/NdoleStudio/httpsms/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/NdoleStudio/httpsms/?branch=main) [![Better Stack Badge](https://uptime.betterstack.com/status-badges/v1/monitor/ldl5.svg)](https://uptime.betterstack.com/?utm_source=status_badge) +[![Greptile: The War on Bugs](https://www.greptile.com/badge.svg)](https://www.greptile.com/?utm_source=oss_badge&utm_medium=readme&utm_campaign=greptile_for_open_source) [![Sponsors](https://img.shields.io/github/sponsors/ndolestudio?logo=github)](https://github.com/sponsors/ndolestudio) [![Discord](https://img.shields.io/discord/1095778291488653372?label=Discord)](https://discord.gg/kGk8HVqeEZ) @@ -63,7 +64,7 @@ It is hosted as a single page application on firebase. The source code is in the ## API The API https://api.httpsms.com is built using [Fiber](https://gofiber.io/), Go and [CockroachDB](https://www.cockroachlabs.com/) for the database. -It rus as a serverless application on Google Cloud Run. The API documentation can be found here https://api.httpsms.com/index.html +It runs as a serverless application on Google Cloud Run. The API documentation can be found here https://api.httpsms.com/index.html ```go // Sending an SMS Message using Go @@ -95,7 +96,7 @@ works best for you: ### End-to-end Encryption -You can encrypt your messages end-to-end ysubg the military grade [AES-256 encryption](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) +You can encrypt your messages end-to-end using the military grade [AES-256 encryption](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) algorithm. Your encryption key is stored only on our mobile phone so the even the server won't have any way to view the content of your SMS messages which are sent and received on your Android phone. @@ -244,7 +245,7 @@ docker compose up --build ### 7. Create the System User -- The application uses the concept of a system user to process events async. You should manually create this user in `users` table in your database. Make sure you use the same `id` and `api_key` as the `EVENTS_QUEUE_USER_ID`, and `EVENTS_QUEUE_USER_API_KEY` in your `.env` file. +- The application uses the concept of a system user to process events asynchronously. You should manually create this user in `users` table in your database. Make sure you use the same `id` and `api_key` as the `EVENTS_QUEUE_USER_ID`, and `EVENTS_QUEUE_USER_API_KEY` in your `.env` file. ```SQL INSERT INTO users (id, api_key, email ) VALUES ('your-system-user-id', 'your-system-api-key', 'system@domain.com'); diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 15857e708..f670b8bb7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,7 +1,8 @@ plugins { id("com.android.application") id("com.google.gms.google-services") - id("io.sentry.android.gradle") version "6.2.0" + id("io.sentry.android.gradle") version "6.14.0" + id("org.jetbrains.kotlin.plugin.compose") } val gitHash = providers.exec { @@ -9,12 +10,12 @@ val gitHash = providers.exec { }.standardOutput.asText.map { it.trim() } android { - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId = "com.httpsms" minSdk = 28 - targetSdk = 36 + targetSdk = 37 versionCode = 1 versionName = gitHash.getOrElse("unknown") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -38,26 +39,39 @@ android { buildFeatures { buildConfig = true + compose = true } } dependencies { - implementation(platform("com.google.firebase:firebase-bom:34.11.0")) + val composeBom = platform("androidx.compose:compose-bom:2026.06.01") + implementation(composeBom) + androidTestImplementation(composeBom) + + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.activity:activity-compose:1.13.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0") + + implementation(platform("com.google.firebase:firebase-bom:34.16.0")) implementation("com.journeyapps:zxing-android-embedded:4.3.0") implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-messaging") - implementation("com.squareup.okhttp3:okhttp:5.3.2") + implementation("com.squareup.okhttp3:okhttp:5.4.0") implementation("com.jakewharton.timber:timber:5.0.1") implementation("androidx.preference:preference-ktx:1.2.1") - implementation("androidx.work:work-runtime-ktx:2.11.1") - implementation("androidx.core:core-ktx:1.18.0") + implementation("androidx.work:work-runtime-ktx:2.11.2") + implementation("androidx.core:core-ktx:1.19.0") implementation("androidx.cardview:cardview:1.0.0") implementation("com.beust:klaxon:5.6") implementation("androidx.appcompat:appcompat:1.7.1") implementation("org.apache.commons:commons-text:1.15.0") - implementation("com.google.android.material:material:1.13.0") + implementation("com.google.android.material:material:1.14.0") implementation("androidx.constraintlayout:constraintlayout:2.2.1") - implementation("com.googlecode.libphonenumber:libphonenumber:9.0.26") + implementation("com.googlecode.libphonenumber:libphonenumber:9.0.34") implementation("com.klinkerapps:android-smsmms:5.2.6") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.3.0") diff --git a/android/app/src/main/java/com/httpsms/LoginActivity.kt b/android/app/src/main/java/com/httpsms/LoginActivity.kt index babe12df6..17bd6db66 100644 --- a/android/app/src/main/java/com/httpsms/LoginActivity.kt +++ b/android/app/src/main/java/com/httpsms/LoginActivity.kt @@ -8,56 +8,75 @@ import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.telephony.TelephonyManager -import android.view.View -import android.webkit.URLUtil -import android.widget.LinearLayout import android.widget.Toast +import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.core.app.ActivityCompat -import androidx.lifecycle.MutableLiveData import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import com.google.android.material.button.MaterialButton -import com.google.android.material.progressindicator.LinearProgressIndicator -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout -import com.httpsms.validators.PhoneNumberValidator +import com.httpsms.ui.login.LoginScreen +import com.httpsms.ui.login.LoginViewModel +import com.httpsms.ui.theme.HttpSmsTheme import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import timber.log.Timber -import java.net.URI - class LoginActivity : AppCompatActivity() { + private val viewModel: LoginViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) redirectToMain() - setContentView(R.layout.activity_login) - registerListeners() - setPhoneNumber() - disableSim2() - setServerURL() - setupApiKeyInput() - } - - private fun setupApiKeyInput() { - val apiKeyInputLayout = findViewById(R.id.loginApiKeyTextInputLayout) - val apiKeyInput = findViewById(R.id.loginApiKeyTextInput) - - apiKeyInput.setOnClickListener { - startQrCodeScan() - } + + viewModel.initialize(this, getString(R.string.default_server_url)) + + setContent { + HttpSmsTheme { + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(uiState.loginSuccess) { + if (uiState.loginSuccess) { + redirectToMain() + } + } - apiKeyInputLayout.setEndIconOnClickListener { - startQrCodeScan() + LoginScreen( + viewModel = viewModel, + onQrScanClick = { startQrCodeScan() }, + onLoginClick = { + val error = isGooglePlayServicesAvailable() + if (error != null) { + Toast.makeText(this@LoginActivity, error, Toast.LENGTH_SHORT).show() + } else { + viewModel.login( + context = this@LoginActivity, + countryCode = getCountryCode(), + onGooglePlayServicesError = { + Toast.makeText(this@LoginActivity, it, Toast.LENGTH_SHORT).show() + }, + onFcmTokenMissing = { + Toast.makeText( + this@LoginActivity, + "Cannot find FCM token. Make sure you have Google Play Services installed", + Toast.LENGTH_LONG + ).show() + } + ) + } + } + ) + } } } private val barcodeLauncher = registerForActivityResult(ScanContract()) { result -> if (result.contents != null) { - val apiKeyInput = findViewById(R.id.loginApiKeyTextInput) - apiKeyInput.setText(result.contents) + viewModel.onApiKeyChange(result.contents) Toast.makeText(this, "Scanned: ${result.contents}", Toast.LENGTH_LONG).show() } else { Toast.makeText(this, "Scan cancelled", Toast.LENGTH_SHORT).show() @@ -79,46 +98,6 @@ class LoginActivity : AppCompatActivity() { requestPermissions() } - override fun onResume() { - super.onResume() - setPhoneNumber() - disableSim2() - } - - private fun registerListeners() { - loginButton().setOnClickListener { onLoginClick() } - } - - private fun disableSim2() { - if (SmsManagerService.isDualSIM(this)) { - Timber.d("dual sim detected") - val sim2Layout = findViewById(R.id.loginPhoneNumberLayoutSIM2) - sim2Layout.visibility = LinearLayout.VISIBLE - return - } - Timber.d("single sim detected") - val sim2Layout = findViewById(R.id.loginPhoneNumberLayoutSIM2) - sim2Layout.visibility = View.GONE - } - - private fun setPhoneNumber() { - val phoneNumber = getPhoneNumber(this) - if(phoneNumber == null) { - Timber.d("cannot get phone due to no permissions") - return - } - - val phoneInput = findViewById(R.id.loginPhoneNumberInputSIM1) - phoneInput.setText(phoneNumber) - Timber.d("[SIM1] phone number [$phoneNumber] set successfully") - } - - private fun setServerURL() { - val serverUrlInput = findViewById(R.id.loginServerUrlInput) - serverUrlInput.setText(getString(R.string.default_server_url)) - Timber.d("default server url [${serverUrlInput.text.toString()}] set successfully") - } - @SuppressLint("HardwareIds") @Suppress("DEPRECATION") private fun getPhoneNumber(context: Context): String? { @@ -139,13 +118,16 @@ class LoginActivity : AppCompatActivity() { return telephonyManager.line1Number } + private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + permissions.entries.forEach { + Timber.d("${it.key} = ${it.value}") + } + // Try to auto-detect phone numbers after permissions are granted + viewModel.autoDetectPhoneNumbers(this) + } + private fun requestPermissions() { Timber.d("requesting permissions") - val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - permissions.entries.forEach { - Timber.d("${it.key} = ${it.value}") - } - } var permissions = arrayOf( Manifest.permission.SEND_SMS, @@ -175,169 +157,19 @@ class LoginActivity : AppCompatActivity() { return null } - - private fun onLoginClick() { - Timber.d("login button clicked") - - val error = isGooglePlayServicesAvailable() - if (error != null) { - Timber.d("google play services not installed [${error}]") - Toast.makeText(this, error, Toast.LENGTH_SHORT).show() - return - } - - if (Settings.getFcmToken(this) == null) { - Timber.d("The FCM token is not set") - Toast.makeText(this, "Cannot find FCM token. Make sure you have Google Play Services installed", Toast.LENGTH_LONG).show() - return - } - - loginButton().isEnabled = false - val progressBar = findViewById(R.id.loginProgressIndicator) - progressBar.visibility = View.VISIBLE - - val apiKeyLayout = findViewById(R.id.loginApiKeyTextInputLayout) - apiKeyLayout.error = null - - val apiKey = findViewById(R.id.loginApiKeyTextInput) - apiKey.isEnabled = false - - val serverUrlLayout = findViewById(R.id.loginServerUrlLayout) - serverUrlLayout.error = null - - val serverUrl = findViewById(R.id.loginServerUrlInput) - serverUrl.isEnabled = false - - val phoneNumberLayout = findViewById(R.id.loginPhoneNumberLayoutSIM1) - phoneNumberLayout.error = null - - val phoneNumber = findViewById(R.id.loginPhoneNumberInputSIM1) - phoneNumber.isEnabled = false - - val phoneNumberLayoutSIM2 = findViewById(R.id.loginPhoneNumberLayoutSIM2) - phoneNumberLayoutSIM2.error = null - - val phoneNumberSIM2 = findViewById(R.id.loginPhoneNumberInputSIM2) - phoneNumberSIM2.isEnabled = false - - val countryCode = getCountryCode() - - val resetView = fun () { - apiKey.isEnabled = true - serverUrl.isEnabled = true - progressBar.visibility = View.INVISIBLE - phoneNumber.isEnabled = true - phoneNumberSIM2.isEnabled = true - loginButton().isEnabled = true - } - - if (!PhoneNumberValidator.isValidPhoneNumber(phoneNumber.text.toString().trim(), countryCode)) { - Timber.e("[SIM1] phone number [${phoneNumber.text.toString()}] is not valid") - resetView() - phoneNumberLayout.error = "Enter an international phone number in the E.164 format" - return - } - - if (SmsManagerService.isDualSIM(this) && !PhoneNumberValidator.isValidPhoneNumber(phoneNumberSIM2.text.toString().trim(), countryCode)) { - Timber.e("[SIM2] phone number [${phoneNumberSIM2.text.toString()}] is not valid") - resetView() - phoneNumberLayoutSIM2.error = "Enter an international phone number in the E.164 format" - return - } - - if(!URLUtil.isValidUrl(serverUrl.text.toString().trim())) { - Timber.e("url number [${serverUrl.text.toString()}] is not a valid URL") - resetView() - serverUrlLayout.error = "Server URL [${serverUrl.text.toString()}] is invalid" - return - } - - if (!URLUtil.isHttpsUrl(serverUrl.text.toString().trim())) { - Timber.e("url number [${serverUrl.text.toString()}] is not an https URL") - resetView() - serverUrlLayout.error = "Server URL [${serverUrl.text.toString()}] must be HTTPS" - return - } - - val liveData = MutableLiveData>() - liveData.observe(this) { authResult -> - run { - progressBar.visibility = View.INVISIBLE - if (authResult.first != null) { - resetView() - apiKeyLayout.error = authResult.first - return@run - } - - if (authResult.second != null) { - resetView() - serverUrlLayout.error = authResult.second - return@run - } - - Settings.setApiKeyAsync(this, apiKey.text.toString()) - Settings.setServerUrlAsync(this, serverUrl.text.toString().trim()) - - val e164PhoneNumber = PhoneNumberValidator.formatE164(phoneNumber.text.toString().trim(), countryCode) - Settings.setSIM1PhoneNumber(this, e164PhoneNumber) - - if(SmsManagerService.isDualSIM(this)) { - val sim2PhoneNumber = PhoneNumberValidator.formatE164(phoneNumberSIM2.text.toString().trim(), countryCode) - Settings.setSIM2PhoneNumber(this, sim2PhoneNumber) - } - - Timber.d("login successfully redirecting to main view") - redirectToMain() - } - } - - Thread { - val service = HttpSmsApiService(apiKey.text.toString(), URI(serverUrl.text.toString().trim())) - - var e164PhoneNumber = PhoneNumberValidator.formatE164(phoneNumber.text.toString().trim(), countryCode) - var response = service.updateFcmToken(e164PhoneNumber, Constants.SIM1, Settings.getFcmToken(this) ?: "") - if(response.second != null || response.third != null) { - Timber.e("error updating fcm token [${response.second}], third [${response.third}]") - liveData.postValue(Pair(response.second, response.third)) - return@Thread - } - - if (!SmsManagerService.isDualSIM(this)) { - Timber.d("single sim detected, no need to update sim2") - liveData.postValue(Pair(null, null)) - return@Thread - } - - e164PhoneNumber = PhoneNumberValidator.formatE164(phoneNumberSIM2.text.toString().trim(), countryCode) - response = service.updateFcmToken(e164PhoneNumber, Constants.SIM2, Settings.getFcmToken(this) ?: "") - - liveData.postValue(Pair(response.second, response.third)) - Timber.d("finished validating api URL") - }.start() - } - private fun redirectToMain() { if (!Settings.isLoggedIn(this)) { return } - finish() + finish() val switchActivityIntent = Intent(this, MainActivity::class.java) startActivity(switchActivityIntent) } - private fun loginButton(): MaterialButton { - return findViewById(R.id.loginButton) - } - private fun getCountryCode() : String { - // Get the TelephonyManager from the system services val tm = this.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - - // Get the network country ISO code and convert it to uppercase val code = tm.networkCountryIso.uppercase() - - // If the country code is empty, retrieve the country code from the device's locale if (code.isEmpty()) { return this.resources.configuration.locales.get(0).country.uppercase() } diff --git a/android/app/src/main/java/com/httpsms/MainActivity.kt b/android/app/src/main/java/com/httpsms/MainActivity.kt index 363e7c19a..ff7587db2 100644 --- a/android/app/src/main/java/com/httpsms/MainActivity.kt +++ b/android/app/src/main/java/com/httpsms/MainActivity.kt @@ -10,37 +10,29 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.PowerManager -import android.telephony.PhoneNumberUtils -import android.view.View -import android.widget.LinearLayout -import android.widget.TextView +import android.provider.Settings as ProviderSettings import android.widget.Toast +import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.MutableLiveData import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager -import com.google.android.material.button.MaterialButton -import com.google.android.material.card.MaterialCardView -import com.google.android.material.progressindicator.LinearProgressIndicator import com.httpsms.services.StickyNotificationService +import com.httpsms.ui.main.MainScreen +import com.httpsms.ui.main.MainViewModel +import com.httpsms.ui.theme.HttpSmsTheme import com.httpsms.worker.HeartbeatWorker import timber.log.Timber -import java.time.Instant -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.util.* import java.util.concurrent.TimeUnit -import android.provider.Settings as ProviderSettings class MainActivity : AppCompatActivity() { + private val viewModel: MainViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -48,20 +40,42 @@ class MainActivity : AppCompatActivity() { redirectToLogin() - setContentView(R.layout.activity_main) + viewModel.initialize(this, getString(R.string.app_version, BuildConfig.VERSION_NAME)) + + setContent { + HttpSmsTheme { + MainScreen( + viewModel = viewModel, + onSettingsClick = { onSettingsClick() }, + onSmsPermissionClick = { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://httpsms.com/blog/grant-send-and-read-sms-permissions-on-android")) + startActivity(intent) + }, + onBatteryOptimizationClick = { + val intent = Intent() + intent.action = ProviderSettings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + intent.data = Uri.parse("package:$packageName") + startActivity(intent) + }, + onHeartbeatClick = { + viewModel.sendHeartbeat(this) { error -> + if (error != null) { + Timber.w("heartbeat sending failed with [$error]") + Toast.makeText(this, error, Toast.LENGTH_LONG).show() + } else { + Toast.makeText(this, "Heartbeat sent successfully", Toast.LENGTH_SHORT).show() + } + } + } + ) + } + } createChannel() - - setCardContent(this) - registerListeners() refreshToken(this) startStickyNotification(this) scheduleHeartbeatWorker(this) - setVersion() - setHeartbeatListener(this) - setSmsPermissionListener() - setBatteryOptimizationListener() } override fun onStart() { @@ -74,35 +88,7 @@ class MainActivity : AppCompatActivity() { Timber.d( "on activity resume") redirectToLogin() refreshToken(this) - setCardContent(this) - setSmsPermissionListener() - setBatteryOptimizationListener() - } - - private fun setVersion() { - val appVersionView = findViewById(R.id.mainAppVersion) - appVersionView.text = getString(R.string.app_version, BuildConfig.VERSION_NAME) - } - - private fun setCardContent(context: Context) { - val titleText = findViewById(R.id.cardPhoneNumber) - titleText.text = PhoneNumberUtils.formatNumber(Settings.getSIM1PhoneNumber(this), Locale.getDefault().country) - if(!Settings.getActiveStatus(context, Constants.SIM1)) { - titleText.setCompoundDrawables(null, null, null, null) - } - - val titleTextSIM2 = findViewById(R.id.cardPhoneNumberSIM2) - titleTextSIM2.text = PhoneNumberUtils.formatNumber(Settings.getSIM2PhoneNumber(this), Locale.getDefault().country) - if(!Settings.getActiveStatus(context, Constants.SIM2)) { - titleTextSIM2.setCompoundDrawables(null, null, null, null) - } - - setLastHeartbeatTimestamp(context) - - if(!Settings.isDualSIM(context)) { - val sim2Card = findViewById(R.id.mainPhoneCardSIM2) - sim2Card.visibility = MaterialCardView.GONE - } + viewModel.updateState(this, getString(R.string.app_version, BuildConfig.VERSION_NAME)) } private fun requestPermissions(context:Context) { @@ -116,7 +102,7 @@ class MainActivity : AppCompatActivity() { Settings.setIncomingCallEventsEnabled(context, Constants.SIM2, false) } } - setSmsPermissionListener() + viewModel.updateState(context, getString(R.string.app_version, BuildConfig.VERSION_NAME)) } var permissions = arrayOf( @@ -229,10 +215,6 @@ class MainActivity : AppCompatActivity() { } } - private fun registerListeners() { - findViewById(R.id.mainSettingsButton).setOnClickListener { onSettingsClick() } - } - private fun onSettingsClick() { Timber.d("settings button clicked") val switchActivityIntent = Intent(this, SettingsActivity::class.java) @@ -248,28 +230,6 @@ class MainActivity : AppCompatActivity() { return true } - private fun setLastHeartbeatTimestamp(context: Context) { - val refreshTimestampView = findViewById(R.id.cardRefreshTime) - val timestamp = Settings.getHeartbeatTimestamp(context) - - if (timestamp == 0.toLong()) { - Timber.d("no heartbeat timestamp has been set") - refreshTimestampView.text = "--" - return - } - - val timestampZdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC) - val localTime = timestampZdt.withZoneSameInstant(ZoneId.systemDefault()) - Timber.d("heartbeat timestamp in UTC is [${timestampZdt}] and local is [$localTime]") - - refreshTimestampView.text = localTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - - if (Settings.isDualSIM(context)) { - val refreshTimestampViewSIM2 = findViewById(R.id.cardRefreshTimeSIM2) - refreshTimestampViewSIM2.text = localTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - } - } - private fun createChannel() { // Create the NotificationChannel val name = getString(R.string.notification_channel_default) @@ -282,109 +242,4 @@ class MainActivity : AppCompatActivity() { val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(mChannel) } - - @SuppressLint("BatteryLife") - private fun setBatteryOptimizationListener() { - val pm = getSystemService(POWER_SERVICE) as PowerManager - val button = findViewById(R.id.batteryOptimizationButtonButton) - if (!pm.isIgnoringBatteryOptimizations(packageName)) { - button.visibility = View.VISIBLE - button.setOnClickListener { - val intent = Intent() - intent.action = ProviderSettings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - intent.data = Uri.parse("package:$packageName") - startActivity(intent) - } - } else { - button.visibility = View.GONE - } - updatePermissionLayoutVisibility() - } - - private fun setSmsPermissionListener() { - val smsPermissions = arrayOf( - Manifest.permission.SEND_SMS, - Manifest.permission.RECEIVE_SMS, - Manifest.permission.READ_SMS - ) - val allGranted = smsPermissions.all { - checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED - } - - val button = findViewById(R.id.smsPermissionButton) - if (!allGranted) { - button.visibility = View.VISIBLE - button.setOnClickListener { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://httpsms.com/blog/grant-send-and-read-sms-permissions-on-android")) - startActivity(intent) - } - } else { - button.visibility = View.GONE - } - updatePermissionLayoutVisibility() - } - - private fun updatePermissionLayoutVisibility() { - val smsButton = findViewById(R.id.smsPermissionButton) - val batteryButton = findViewById(R.id.batteryOptimizationButtonButton) - val layout = findViewById(R.id.batteryOptimizationLinearLayout) - - if (smsButton.visibility == View.GONE && batteryButton.visibility == View.GONE) { - layout.visibility = View.GONE - } else { - layout.visibility = View.VISIBLE - } - } - - private fun setHeartbeatListener(context: Context) { - findViewById(R.id.mainHeartbeatButton).setOnClickListener{onHeartbeatClick(context)} - } - - private fun onHeartbeatClick(context: Context) { - Timber.d("heartbeat button clicked") - val heartbeatButton = findViewById(R.id.mainHeartbeatButton) - heartbeatButton.isEnabled = false - - val progressBar = findViewById(R.id.mainProgressIndicator) - progressBar.visibility = View.VISIBLE - - val liveData = MutableLiveData() - liveData.observe(this) { exception -> - run { - progressBar.visibility = View.INVISIBLE - heartbeatButton.isEnabled = true - - if (exception != null) { - Timber.w("heartbeat sending failed with [$exception]") - Toast.makeText(context, exception, Toast.LENGTH_LONG).show() - return@run - } - Toast.makeText(context, "Heartbeat sent successfully", Toast.LENGTH_SHORT).show() - - setLastHeartbeatTimestamp(this) - } - } - - Thread { - val charging = Settings.isCharging(applicationContext) - var error: String? = null - try { - val phoneNumbers = mutableListOf() - phoneNumbers.add(Settings.getSIM1PhoneNumber(applicationContext)) - if (Settings.getActiveStatus(applicationContext, Constants.SIM2)) { - phoneNumbers.add(Settings.getSIM2PhoneNumber(applicationContext)) - } - val isStored = HttpSmsApiService.create(context).storeHeartbeat(phoneNumbers.toTypedArray(), charging) - if (!isStored) { - error = "Could not send heartbeat make sure the phone is connected to the internet" - } - Settings.setHeartbeatTimestampAsync(applicationContext, System.currentTimeMillis()) - } catch (exception: Exception) { - Timber.e(exception) - error = exception.javaClass.simpleName - } - liveData.postValue(error) - Timber.d("finished sending pulse") - }.start() - } } diff --git a/android/app/src/main/java/com/httpsms/SettingsActivity.kt b/android/app/src/main/java/com/httpsms/SettingsActivity.kt index cbc2831bc..37a8b3523 100644 --- a/android/app/src/main/java/com/httpsms/SettingsActivity.kt +++ b/android/app/src/main/java/com/httpsms/SettingsActivity.kt @@ -1,121 +1,33 @@ package com.httpsms -import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.widget.doAfterTextChanged -import com.google.android.material.appbar.MaterialToolbar -import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.switchmaterial.SwitchMaterial -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout +import com.httpsms.ui.settings.SettingsScreen +import com.httpsms.ui.settings.SettingsViewModel +import com.httpsms.ui.theme.HttpSmsTheme import timber.log.Timber class SettingsActivity : AppCompatActivity() { + private val viewModel: SettingsViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) - fillSettings(this) - registerListeners() - } - - private fun fillSettings(context: Context) { - val debugLogs = findViewById(R.id.settingEnableDebugLogs) - debugLogs.isChecked = Settings.isDebugLogEnabled(context) - debugLogs.setOnCheckedChangeListener{ _, isChecked -> run { Settings.setDebugLogEnabled(context, isChecked) } } - - - val phoneNumber = findViewById(R.id.settingsSIM1Input) - phoneNumber.setText(Settings.getSIM1PhoneNumber(context)) - phoneNumber.isEnabled = false - - val sim1IncomingMessages = findViewById(R.id.settings_sim1_incoming_messages) - sim1IncomingMessages.isChecked = Settings.isIncomingMessageEnabled(context, Constants.SIM1) - - sim1IncomingMessages.setOnCheckedChangeListener{ _, isChecked -> run { Settings.setIncomingActiveSIM1(context, isChecked) } } - - val sim1OutgoingMessages = findViewById(R.id.settings_sim1_outgoing_messages) - sim1OutgoingMessages.isChecked = Settings.getActiveStatus(context, Constants.SIM1) - sim1OutgoingMessages.setOnCheckedChangeListener{ _, isChecked -> run { Settings.setActiveStatusAsync(context, isChecked, Constants.SIM1) } } - - if (!Settings.isDualSIM(context)) { - val layout = findViewById(R.id.settingsSIM2Layout) - layout.visibility = TextInputLayout.GONE - val sim2Switch = findViewById(R.id.settings_sim2_incoming_messages) - sim2Switch.visibility = SwitchMaterial.GONE - val outgoingSwitch = findViewById(R.id.settings_sim2_outgoing_messages) - outgoingSwitch.visibility = SwitchMaterial.GONE - } else { - val phoneNumberSIM2 = findViewById(R.id.settingsSIM2InputEdit) - phoneNumberSIM2.setText(Settings.getSIM2PhoneNumber(context)) - phoneNumberSIM2.isEnabled = false - - val sim2IncomingMessages = findViewById(R.id.settings_sim2_incoming_messages) - sim2IncomingMessages.isChecked = Settings.isIncomingMessageEnabled(context, Constants.SIM2) - sim2IncomingMessages.setOnCheckedChangeListener{ _, isChecked -> run { Settings.setIncomingActiveSIM2(context, isChecked) } } - - val sim2OutgoingMessages = findViewById(R.id.settings_sim2_outgoing_messages) - sim2OutgoingMessages.isChecked = Settings.getActiveStatus(context, Constants.SIM2) - sim2OutgoingMessages.setOnCheckedChangeListener{ _, isChecked -> run { Settings.setActiveStatusAsync(context, isChecked, Constants.SIM2) } } - } - - handleEncryptionSettings(context) - handleIncomingCallEvents(context) - } - - private fun handleIncomingCallEvents(context: Context) { - val enableIncomingCallEvents = findViewById(R.id.settingsSim1EnableIncomingCallEvents) - enableIncomingCallEvents.isChecked = Settings.isIncomingCallEventsEnabled(context, Constants.SIM1) - enableIncomingCallEvents.setOnCheckedChangeListener{ _, isChecked -> run { - Settings.setIncomingCallEventsEnabled(context, Constants.SIM1, isChecked) - }} - - val sim2IncomingCalls = findViewById(R.id.settingsSim2EnableIncomingCallEvents) - if (!Settings.isDualSIM(context)) { - sim2IncomingCalls.visibility = SwitchMaterial.GONE - return - } - - sim2IncomingCalls.isChecked = Settings.isIncomingCallEventsEnabled(context, Constants.SIM2) - sim2IncomingCalls.setOnCheckedChangeListener{ _, isChecked -> run { - Settings.setIncomingCallEventsEnabled(context, Constants.SIM2, isChecked) - }} - } - - private fun handleEncryptionSettings(context: Context) { - val encryptionKey = findViewById(R.id.settingsEncryptionKeyInputEdit) - val encryptReceivedMessages = findViewById(R.id.settingsEncryptReceivedMessages) - - val key = Settings.getEncryptionKey(context) - if(key.isNullOrEmpty()) { - encryptReceivedMessages.isEnabled = false - } else { - encryptionKey.setText(key.trim()) - } - - encryptionKey.doAfterTextChanged{ - if (it == null || it.toString().isEmpty()) { - Settings.setEncryptionKey(context, null) - Settings.setEncryptReceivedMessages(context, false) - encryptReceivedMessages.isChecked = false - encryptReceivedMessages.isEnabled = false - } else { - encryptReceivedMessages.isEnabled = true - Settings.setEncryptionKey(context, it.toString().trim()) + + viewModel.initialize(this) + + setContent { + HttpSmsTheme { + SettingsScreen( + viewModel = viewModel, + onBackClick = { onBackClicked() }, + onLogoutClick = { onLogoutClick() } + ) } } - - encryptReceivedMessages.isChecked = Settings.encryptReceivedMessages(context) - encryptReceivedMessages.setOnCheckedChangeListener{ _, isChecked -> run { - Settings.setEncryptReceivedMessages(context, isChecked) - }} - } - - private fun registerListeners() { - appToolbar().setOnClickListener { onBackClicked() } - findViewById(R.id.settingsLogoutButton).setOnClickListener { onLogoutClick() } } private fun onBackClicked() { @@ -129,10 +41,6 @@ class SettingsActivity : AppCompatActivity() { startActivity(switchActivityIntent) } - private fun appToolbar(): MaterialToolbar { - return findViewById(R.id.settings_toolbar) - } - private fun onLogoutClick() { Timber.d("logout button clicked") MaterialAlertDialogBuilder(this) @@ -141,20 +49,9 @@ class SettingsActivity : AppCompatActivity() { .setNeutralButton("Cancel"){ _, _ -> Timber.d("logout dialog canceled") } .setPositiveButton("Logout"){_, _ -> Timber.d("logging out user") - Settings.setApiKeyAsync(this, null) - Settings.setSIM1PhoneNumber(this, null) - Settings.setSIM2PhoneNumber(this, null) - Settings.setActiveStatusAsync(this, true, Constants.SIM1) - Settings.setActiveStatusAsync(this, true, Constants.SIM2) - Settings.setIncomingActiveSIM1(this, true) - Settings.setIncomingActiveSIM2(this, true) - Settings.setUserID(this, null) - Settings.setEncryptionKey(this, null) - Settings.setEncryptReceivedMessages(this, false) - Settings.setFcmTokenLastUpdateTimestampAsync(this, 0) - Settings.setIncomingCallEventsEnabled(this, Constants.SIM1, false) - Settings.setIncomingCallEventsEnabled(this, Constants.SIM2, false) - redirectToLogin() + viewModel.logout(this) { + redirectToLogin() + } } .show() } diff --git a/android/app/src/main/java/com/httpsms/ui/login/LoginActivityContent.kt b/android/app/src/main/java/com/httpsms/ui/login/LoginActivityContent.kt new file mode 100644 index 000000000..0fc5e4c9d --- /dev/null +++ b/android/app/src/main/java/com/httpsms/ui/login/LoginActivityContent.kt @@ -0,0 +1,217 @@ +package com.httpsms.ui.login + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.QrCodeScanner +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.httpsms.R +import com.httpsms.ui.theme.Blue500 +import com.httpsms.ui.theme.Pink500 + +@Composable +fun LoginScreen( + viewModel: LoginViewModel, + onQrScanClick: () -> Unit, + onLoginClick: () -> Unit +) { + val uiState by viewModel.uiState.collectAsState() + val uriHandler = LocalUriHandler.current + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(64.dp)) + + Image( + painter = painterResource(id = R.drawable.logo_cropped), + contentDescription = stringResource(id = R.string.img_http_sms_logo), + modifier = Modifier.size(100.dp) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + val annotatedString = buildAnnotatedString { + val text = stringResource(id = R.string.get_your_api_key) + val linkText = "httpsms.com/settings" + val startIndex = text.indexOf(linkText) + + if (startIndex >= 0) { + append(text.substring(0, startIndex)) + + pushStringAnnotation(tag = "URL", annotation = "https://httpsms.com/settings") + withStyle(style = SpanStyle(color = Blue500, fontWeight = FontWeight.Bold)) { + append(linkText) + } + pop() + + append(text.substring(startIndex + linkText.length)) + } else { + append(text) + } + } + + ClickableText( + text = annotatedString, + onClick = { offset -> + annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset) + .firstOrNull()?.let { annotation -> + uriHandler.openUri(annotation.item) + } + }, + style = MaterialTheme.typography.bodyLarge.copy( + textAlign = TextAlign.Center, + fontSize = 20.sp, + lineHeight = 28.sp, + color = MaterialTheme.colorScheme.onBackground + ), + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(24.dp)) + + OutlinedTextField( + value = uiState.apiKey, + onValueChange = { viewModel.onApiKeyChange(it) }, + label = { Text(stringResource(id = R.string.text_area_api_key)) }, + modifier = Modifier.fillMaxWidth(), + isError = uiState.apiKeyError != null, + supportingText = uiState.apiKeyError?.let { { Text(it) } }, + trailingIcon = { + IconButton(onClick = onQrScanClick) { + Icon( + imageVector = Icons.Default.QrCodeScanner, + contentDescription = "Scan QR Code" + ) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + enabled = !uiState.isLoading + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = uiState.phoneNumberSIM1, + onValueChange = { viewModel.onPhoneNumberSIM1Change(it) }, + label = { Text(stringResource(id = R.string.login_phone_number_sim1)) }, + placeholder = { Text(stringResource(id = R.string.login_phone_number_hint)) }, + modifier = Modifier.fillMaxWidth(), + isError = uiState.phoneNumberSIM1Error != null, + supportingText = uiState.phoneNumberSIM1Error?.let { { Text(it) } }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Phone, + imeAction = ImeAction.Done + ), + enabled = !uiState.isLoading + ) + + if (uiState.isDualSim) { + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = uiState.phoneNumberSIM2, + onValueChange = { viewModel.onPhoneNumberSIM2Change(it) }, + label = { Text(stringResource(id = R.string.login_phone_number_sim2)) }, + placeholder = { Text(stringResource(id = R.string.login_phone_number_hint)) }, + modifier = Modifier.fillMaxWidth(), + isError = uiState.phoneNumberSIM2Error != null, + supportingText = uiState.phoneNumberSIM2Error?.let { { Text(it) } }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Phone, + imeAction = ImeAction.Done + ), + enabled = !uiState.isLoading + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = uiState.serverUrl, + onValueChange = { viewModel.onServerUrlChange(it) }, + label = { Text(stringResource(id = R.string.server_url)) }, + placeholder = { Text(stringResource(id = R.string.login_server_url_hint)) }, + modifier = Modifier.fillMaxWidth(), + isError = uiState.serverUrlError != null, + supportingText = uiState.serverUrlError?.let { { Text(it) } }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Done + ), + enabled = !uiState.isLoading + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onLoginClick, + enabled = !uiState.isLoading, + modifier = Modifier.align(Alignment.CenterHorizontally), + colors = ButtonDefaults.buttonColors(containerColor = Blue500), + contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_login), + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + tint = Color.White + ) + Text( + text = stringResource(id = R.string.sign_in_button), + color = Color.White, + fontSize = 18.sp + ) + } + + if (uiState.isLoading) { + Spacer(modifier = Modifier.height(16.dp)) + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Pink500 + ) + } + } +} diff --git a/android/app/src/main/java/com/httpsms/ui/login/LoginViewModel.kt b/android/app/src/main/java/com/httpsms/ui/login/LoginViewModel.kt new file mode 100644 index 000000000..a4803e41e --- /dev/null +++ b/android/app/src/main/java/com/httpsms/ui/login/LoginViewModel.kt @@ -0,0 +1,221 @@ +package com.httpsms.ui.login + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.webkit.URLUtil +import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.httpsms.Constants +import com.httpsms.HttpSmsApiService +import com.httpsms.Settings +import com.httpsms.SmsManagerService +import com.httpsms.validators.PhoneNumberValidator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.net.URI +import java.net.URISyntaxException + +data class LoginUiState( + val apiKey: String = "", + val phoneNumberSIM1: String = "", + val phoneNumberSIM2: String = "", + val serverUrl: String = "", + val isLoading: Boolean = false, + val apiKeyError: String? = null, + val phoneNumberSIM1Error: String? = null, + val phoneNumberSIM2Error: String? = null, + val serverUrlError: String? = null, + val isDualSim: Boolean = false, + val loginSuccess: Boolean = false +) + +class LoginViewModel : ViewModel() { + private val _uiState = MutableStateFlow(LoginUiState()) + val uiState = _uiState.asStateFlow() + + fun initialize(context: Context, defaultServerUrl: String) { + val isDualSim = SmsManagerService.isDualSIM(context) + val phoneNumberSIM1 = Settings.getSIM1PhoneNumber(context) + val phoneNumberSIM2 = Settings.getSIM2PhoneNumber(context) + + _uiState.value = _uiState.value.copy( + isDualSim = isDualSim, + phoneNumberSIM1 = phoneNumberSIM1, + phoneNumberSIM2 = phoneNumberSIM2, + serverUrl = defaultServerUrl + ) + + // Try to auto-detect if fields are empty + if (phoneNumberSIM1.isEmpty() || (isDualSim && phoneNumberSIM2.isEmpty())) { + autoDetectPhoneNumbers(context) + } + } + + fun autoDetectPhoneNumbers(context: Context) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { + Timber.d("READ_PHONE_STATE permission not granted for auto-detecting phone numbers") + return + } + + val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + + var detectedSIM1 = _uiState.value.phoneNumberSIM1 + var detectedSIM2 = _uiState.value.phoneNumberSIM2 + + try { + val subscriptionManager = if (Build.VERSION.SDK_INT >= 31) { + context.getSystemService(SubscriptionManager::class.java) + } else { + SubscriptionManager.from(context) + } + + val activeSubscriptions = try { subscriptionManager.activeSubscriptionInfoList } catch (e: Exception) { null } + + if (detectedSIM1.isEmpty()) { + val line1Number = try { telephonyManager.line1Number } catch (e: Exception) { null } + if (!line1Number.isNullOrEmpty()) { + detectedSIM1 = line1Number + } else if (activeSubscriptions != null && activeSubscriptions.isNotEmpty()) { + detectedSIM1 = activeSubscriptions[0].number ?: "" + } + } + + if (detectedSIM2.isEmpty() && activeSubscriptions != null && activeSubscriptions.size >= 2) { + detectedSIM2 = activeSubscriptions[1].number ?: "" + } + + Timber.d("Auto-detected numbers - SIM1: $detectedSIM1, SIM2: $detectedSIM2") + + } catch (e: SecurityException) { + Timber.e(e, "Security exception while auto-detecting phone numbers") + } + + _uiState.value = _uiState.value.copy( + phoneNumberSIM1 = detectedSIM1, + phoneNumberSIM2 = detectedSIM2, + isDualSim = SmsManagerService.isDualSIM(context) + ) + } + + fun onApiKeyChange(value: String) { + _uiState.value = _uiState.value.copy(apiKey = value, apiKeyError = null) + } + + fun onPhoneNumberSIM1Change(value: String) { + _uiState.value = _uiState.value.copy(phoneNumberSIM1 = value, phoneNumberSIM1Error = null) + } + + fun onPhoneNumberSIM2Change(value: String) { + _uiState.value = _uiState.value.copy(phoneNumberSIM2 = value, phoneNumberSIM2Error = null) + } + + fun onServerUrlChange(value: String) { + _uiState.value = _uiState.value.copy(serverUrl = value, serverUrlError = null) + } + + fun login(context: Context, countryCode: String, onGooglePlayServicesError: (String) -> Unit, onFcmTokenMissing: () -> Unit) { + val currentState = _uiState.value + + // Validation logic from LoginActivity.onLoginClick + if (Settings.getFcmToken(context) == null) { + onFcmTokenMissing() + return + } + + _uiState.value = currentState.copy(isLoading = true) + + viewModelScope.launch { + val apiKey = currentState.apiKey.trim() + val serverUrl = currentState.serverUrl.trim() + val phone1 = currentState.phoneNumberSIM1.trim() + val phone2 = currentState.phoneNumberSIM2.trim() + + if (!PhoneNumberValidator.isValidPhoneNumber(phone1, countryCode)) { + _uiState.value = _uiState.value.copy( + isLoading = false, + phoneNumberSIM1Error = "Enter an international phone number in the E.164 format" + ) + return@launch + } + + if (currentState.isDualSim && !PhoneNumberValidator.isValidPhoneNumber(phone2, countryCode)) { + _uiState.value = _uiState.value.copy( + isLoading = false, + phoneNumberSIM2Error = "Enter an international phone number in the E.164 format" + ) + return@launch + } + + if (!URLUtil.isValidUrl(serverUrl)) { + _uiState.value = _uiState.value.copy( + isLoading = false, + serverUrlError = "Server URL [$serverUrl] is invalid" + ) + return@launch + } + + if (!URLUtil.isHttpsUrl(serverUrl)) { + _uiState.value = _uiState.value.copy( + isLoading = false, + serverUrlError = "Server URL [$serverUrl] must be HTTPS" + ) + return@launch + } + + val authResult = try { + withContext(Dispatchers.IO) { + val service = HttpSmsApiService(apiKey, URI(serverUrl)) + val e164Phone1 = PhoneNumberValidator.formatE164(phone1, countryCode) + val response1 = service.updateFcmToken(e164Phone1, Constants.SIM1, Settings.getFcmToken(context) ?: "") + + if (response1.second != null || response1.third != null) { + return@withContext Pair(response1.second, response1.third) + } + + if (currentState.isDualSim) { + val e164Phone2 = PhoneNumberValidator.formatE164(phone2, countryCode) + val response2 = service.updateFcmToken(e164Phone2, Constants.SIM2, Settings.getFcmToken(context) ?: "") + return@withContext Pair(response2.second, response2.third) + } + + Pair(null, null) + } + } catch (e: URISyntaxException) { + Timber.e(e, "Invalid URI: $serverUrl") + Pair(null, "Server URL [$serverUrl] is invalid") + } catch (e: Exception) { + Timber.e(e, "Login error") + Pair(null, "An unexpected error occurred: ${e.message}") + } + + if (authResult.first != null) { + _uiState.value = _uiState.value.copy(isLoading = false, apiKeyError = authResult.first) + return@launch + } + + if (authResult.second != null) { + _uiState.value = _uiState.value.copy(isLoading = false, serverUrlError = authResult.second) + return@launch + } + + // Save settings + Settings.setApiKeyAsync(context, apiKey) + Settings.setServerUrlAsync(context, serverUrl) + Settings.setSIM1PhoneNumber(context, PhoneNumberValidator.formatE164(phone1, countryCode)) + if (currentState.isDualSim) { + Settings.setSIM2PhoneNumber(context, PhoneNumberValidator.formatE164(phone2, countryCode)) + } + + _uiState.value = _uiState.value.copy(isLoading = false, loginSuccess = true) + } + } +} diff --git a/android/app/src/main/java/com/httpsms/ui/main/MainActivityContent.kt b/android/app/src/main/java/com/httpsms/ui/main/MainActivityContent.kt new file mode 100644 index 000000000..087bf7921 --- /dev/null +++ b/android/app/src/main/java/com/httpsms/ui/main/MainActivityContent.kt @@ -0,0 +1,243 @@ +package com.httpsms.ui.main + +import android.telephony.PhoneNumberUtils +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BatteryAlert +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.httpsms.R +import com.httpsms.ui.theme.Blue500 +import com.httpsms.ui.theme.Pink500 +import java.util.Locale + +@Composable +fun MainScreen( + viewModel: MainViewModel, + onSettingsClick: () -> Unit, + onSmsPermissionClick: () -> Unit, + onBatteryOptimizationClick: () -> Unit, + onHeartbeatClick: () -> Unit +) { + val uiState by viewModel.uiState.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(64.dp)) + + Image( + painter = painterResource(id = R.drawable.logo_cropped), + contentDescription = stringResource(id = R.string.img_http_sms_logo), + modifier = Modifier + .width(147.dp) + .height(92.dp) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + PhoneCard( + phoneNumber = uiState.phoneNumberSIM1, + isActive = uiState.isActiveSIM1, + refreshTime = uiState.lastHeartbeatTime + ) + + if (uiState.isDualSim) { + Spacer(modifier = Modifier.height(24.dp)) + PhoneCard( + phoneNumber = uiState.phoneNumberSIM2, + isActive = uiState.isActiveSIM2, + refreshTime = uiState.lastHeartbeatTime + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + if (!uiState.isSmsPermissionGranted || !uiState.isBatteryOptimizationDisabled) { + Column(modifier = Modifier.fillMaxWidth()) { + if (!uiState.isSmsPermissionGranted) { + Button( + onClick = onSmsPermissionClick, + modifier = Modifier.align(Alignment.CenterHorizontally), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4CAF50)), + contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp) + ) { + Text( + stringResource(id = R.string.enable_sms_permission), + color = Color.White, + fontSize = 18.sp + ) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + painter = painterResource(id = R.drawable.open_in_new_24), + contentDescription = null, + tint = Color.White + ) + } + Spacer(modifier = Modifier.height(8.dp)) + } + + if (!uiState.isBatteryOptimizationDisabled) { + Button( + onClick = onBatteryOptimizationClick, + modifier = Modifier.align(Alignment.CenterHorizontally), + colors = ButtonDefaults.buttonColors(containerColor = Pink500), + contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp) + ) { + Icon( + imageVector = Icons.Default.BatteryAlert, + contentDescription = null, + tint = Color.White + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + stringResource(id = R.string.disable_battery_optimization), + color = Color.White, + fontSize = 18.sp + ) + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier.width(IntrinsicSize.Max), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onHeartbeatClick, + modifier = Modifier.fillMaxWidth(), + enabled = !uiState.isHeartbeatLoading, + colors = ButtonDefaults.buttonColors(containerColor = Blue500), + contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp) + ) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = null, + tint = if (uiState.isHeartbeatLoading) LocalContentColor.current else Pink500 + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + stringResource(id = R.string.send_heartbeat), + color = Color.White, + fontSize = 18.sp + ) + } + + if (uiState.isHeartbeatLoading) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), + color = Pink500 + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = uiState.appVersion, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f) + ) + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onSettingsClick, + colors = ButtonDefaults.buttonColors(containerColor = Color.Black), + contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp) + ) { + Icon(Icons.Default.Settings, contentDescription = null, tint = Color.White) + Spacer(modifier = Modifier.width(8.dp)) + Text( + stringResource(id = R.string.main_app_settings), + color = Color.White, + fontSize = 18.sp + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +fun PhoneCard( + phoneNumber: String, + isActive: Boolean, + refreshTime: String +) { + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = PhoneNumberUtils.formatNumber(phoneNumber, Locale.getDefault().country) ?: phoneNumber, + fontSize = 28.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.onSurface + ) + if (isActive) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = "Active", + tint = Color(0xFF70AB5C), + modifier = Modifier.size(24.dp) + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = refreshTime, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } + } +} diff --git a/android/app/src/main/java/com/httpsms/ui/main/MainViewModel.kt b/android/app/src/main/java/com/httpsms/ui/main/MainViewModel.kt new file mode 100644 index 000000000..273cfe786 --- /dev/null +++ b/android/app/src/main/java/com/httpsms/ui/main/MainViewModel.kt @@ -0,0 +1,116 @@ +package com.httpsms.ui.main + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.PowerManager +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.httpsms.Constants +import com.httpsms.HttpSmsApiService +import com.httpsms.Settings +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.time.Instant +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +data class MainUiState( + val phoneNumberSIM1: String = "", + val isActiveSIM1: Boolean = false, + val phoneNumberSIM2: String = "", + val isActiveSIM2: Boolean = false, + val isDualSim: Boolean = false, + val lastHeartbeatTime: String = "--", + val isSmsPermissionGranted: Boolean = true, + val isBatteryOptimizationDisabled: Boolean = true, + val isHeartbeatLoading: Boolean = false, + val appVersion: String = "" +) + +class MainViewModel : ViewModel() { + private val _uiState = MutableStateFlow(MainUiState()) + val uiState = _uiState.asStateFlow() + + fun initialize(context: Context, appVersion: String) { + updateState(context, appVersion) + } + + fun updateState(context: Context, appVersion: String) { + val isDualSim = Settings.isDualSIM(context) + val phone1 = Settings.getSIM1PhoneNumber(context) ?: "" + val active1 = Settings.getActiveStatus(context, Constants.SIM1) + val phone2 = Settings.getSIM2PhoneNumber(context) ?: "" + val active2 = Settings.getActiveStatus(context, Constants.SIM2) + + val timestamp = Settings.getHeartbeatTimestamp(context) + val lastHeartbeat = if (timestamp == 0L) { + "--" + } else { + val timestampZdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC) + val localTime = timestampZdt.withZoneSameInstant(ZoneId.systemDefault()) + localTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + } + + val smsPermissions = arrayOf( + Manifest.permission.SEND_SMS, + Manifest.permission.RECEIVE_SMS, + Manifest.permission.READ_SMS + ) + val allGranted = smsPermissions.all { + context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED + } + + val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager + val batteryOptimized = pm.isIgnoringBatteryOptimizations(context.packageName) + + _uiState.value = _uiState.value.copy( + phoneNumberSIM1 = phone1, + isActiveSIM1 = active1, + phoneNumberSIM2 = phone2, + isActiveSIM2 = active2, + isDualSim = isDualSim, + lastHeartbeatTime = lastHeartbeat, + isSmsPermissionGranted = allGranted, + isBatteryOptimizationDisabled = batteryOptimized, + appVersion = appVersion + ) + } + + fun sendHeartbeat(context: Context, onComplete: (String?) -> Unit) { + _uiState.value = _uiState.value.copy(isHeartbeatLoading = true) + + viewModelScope.launch { + val result = withContext(Dispatchers.IO) { + val charging = Settings.isCharging(context) + try { + val phoneNumbers = mutableListOf() + phoneNumbers.add(Settings.getSIM1PhoneNumber(context)) + if (Settings.getActiveStatus(context, Constants.SIM2)) { + phoneNumbers.add(Settings.getSIM2PhoneNumber(context)) + } + val isStored = HttpSmsApiService.create(context).storeHeartbeat(phoneNumbers.toTypedArray(), charging) + if (!isStored) { + "Could not send heartbeat make sure the phone is connected to the internet" + } else { + Settings.setHeartbeatTimestampAsync(context, System.currentTimeMillis()) + null + } + } catch (exception: Exception) { + Timber.e(exception) + exception.javaClass.simpleName + } + } + + _uiState.value = _uiState.value.copy(isHeartbeatLoading = false) + updateState(context, _uiState.value.appVersion) + onComplete(result) + } + } +} diff --git a/android/app/src/main/java/com/httpsms/ui/settings/SettingsActivityContent.kt b/android/app/src/main/java/com/httpsms/ui/settings/SettingsActivityContent.kt new file mode 100644 index 000000000..3335eb2c2 --- /dev/null +++ b/android/app/src/main/java/com/httpsms/ui/settings/SettingsActivityContent.kt @@ -0,0 +1,221 @@ +package com.httpsms.ui.settings + +import android.app.Activity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Logout +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.view.WindowCompat +import com.httpsms.R +import com.httpsms.ui.theme.LogoGreen + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + viewModel: SettingsViewModel, + onBackClick: () -> Unit, + onLogoutClick: () -> Unit +) { + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current + val isDarkTheme = isSystemInDarkTheme() + val primaryColor = if (isDarkTheme) Color.Black else LogoGreen + + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = primaryColor.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("App Settings") }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = primaryColor, + titleContentColor = Color.White, + navigationIconContentColor = Color.White + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(16.dp) + ) { + // SIM 1 Settings + OutlinedTextField( + value = uiState.phoneNumberSIM1, + onValueChange = { }, + label = { Text(stringResource(id = R.string.settings_sim1)) }, + modifier = Modifier.fillMaxWidth(), + enabled = false + ) + + SwitchSetting( + text = stringResource(id = R.string.settings_outgoing_messages_sim1), + checked = uiState.isActiveSIM1, + onCheckedChange = { viewModel.setActiveSIM1(context, it) } + ) + + SwitchSetting( + text = stringResource(id = R.string.settings_incoming_messages_sim1), + checked = uiState.isIncomingSIM1Enabled, + onCheckedChange = { viewModel.setIncomingSIM1Enabled(context, it) } + ) + + SwitchSetting( + text = stringResource(id = R.string.enable_incoming_call_events_sim1), + checked = uiState.isIncomingCallEventsSIM1Enabled, + onCheckedChange = { viewModel.setIncomingCallEventsSIM1Enabled(context, it) } + ) + + if (uiState.isDualSim) { + Spacer(modifier = Modifier.height(16.dp)) + // SIM 2 Settings + OutlinedTextField( + value = uiState.phoneNumberSIM2, + onValueChange = { }, + label = { Text(stringResource(id = R.string.settings_sim_2)) }, + modifier = Modifier.fillMaxWidth(), + enabled = false + ) + + SwitchSetting( + text = stringResource(id = R.string.settings_outgoing_messages_sim2), + checked = uiState.isActiveSIM2, + onCheckedChange = { viewModel.setActiveSIM2(context, it) } + ) + + SwitchSetting( + text = stringResource(id = R.string.settings_incoming_messages_sim2), + checked = uiState.isIncomingSIM2Enabled, + onCheckedChange = { viewModel.setIncomingSIM2Enabled(context, it) } + ) + + SwitchSetting( + text = stringResource(id = R.string.enable_sim2_incoming_call_events), + checked = uiState.isIncomingCallEventsSIM2Enabled, + onCheckedChange = { viewModel.setIncomingCallEventsSIM2Enabled(context, it) } + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = uiState.encryptionKey, + onValueChange = { viewModel.setEncryptionKey(context, it) }, + label = { Text(stringResource(id = R.string.encryption_key)) }, + modifier = Modifier.fillMaxWidth() + ) + + SwitchSetting( + text = stringResource(id = R.string.encrypt_received_messages), + checked = uiState.isEncryptReceivedMessagesEnabled, + onCheckedChange = { viewModel.setEncryptReceivedMessagesEnabled(context, it) }, + enabled = uiState.encryptionKey.isNotEmpty() + ) + + SwitchSetting( + text = stringResource(id = R.string.enable_debug_logs), + checked = uiState.isDebugLogEnabled, + onCheckedChange = { viewModel.setDebugLogEnabled(context, it) } + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = onLogoutClick, + modifier = Modifier.align(Alignment.CenterHorizontally), + colors = ButtonDefaults.buttonColors(containerColor = Color.Black), + contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Logout, + contentDescription = null, + tint = Color.White + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + stringResource(id = R.string.main_log_out), + color = Color.White, + fontSize = 18.sp + ) + } + } + } +} + +@Composable +fun SwitchSetting( + text: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + enabled: Boolean = true +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + modifier = Modifier.weight(1f), + fontSize = 18.sp, + color = if (enabled) MaterialTheme.colorScheme.onBackground else MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f) + ) + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled + ) + } +} diff --git a/android/app/src/main/java/com/httpsms/ui/settings/SettingsViewModel.kt b/android/app/src/main/java/com/httpsms/ui/settings/SettingsViewModel.kt new file mode 100644 index 000000000..33a23e574 --- /dev/null +++ b/android/app/src/main/java/com/httpsms/ui/settings/SettingsViewModel.kt @@ -0,0 +1,114 @@ +package com.httpsms.ui.settings + +import android.content.Context +import androidx.lifecycle.ViewModel +import com.httpsms.Constants +import com.httpsms.Settings +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +data class SettingsUiState( + val isDebugLogEnabled: Boolean = false, + val phoneNumberSIM1: String = "", + val isIncomingSIM1Enabled: Boolean = false, + val isActiveSIM1: Boolean = false, + val isIncomingCallEventsSIM1Enabled: Boolean = false, + val isDualSim: Boolean = false, + val phoneNumberSIM2: String = "", + val isIncomingSIM2Enabled: Boolean = false, + val isActiveSIM2: Boolean = false, + val isIncomingCallEventsSIM2Enabled: Boolean = false, + val encryptionKey: String = "", + val isEncryptReceivedMessagesEnabled: Boolean = false +) + +class SettingsViewModel : ViewModel() { + private val _uiState = MutableStateFlow(SettingsUiState()) + val uiState = _uiState.asStateFlow() + + fun initialize(context: Context) { + _uiState.value = SettingsUiState( + isDebugLogEnabled = Settings.isDebugLogEnabled(context), + phoneNumberSIM1 = Settings.getSIM1PhoneNumber(context) ?: "", + isIncomingSIM1Enabled = Settings.isIncomingMessageEnabled(context, Constants.SIM1), + isActiveSIM1 = Settings.getActiveStatus(context, Constants.SIM1), + isIncomingCallEventsSIM1Enabled = Settings.isIncomingCallEventsEnabled(context, Constants.SIM1), + isDualSim = Settings.isDualSIM(context), + phoneNumberSIM2 = Settings.getSIM2PhoneNumber(context) ?: "", + isIncomingSIM2Enabled = Settings.isIncomingMessageEnabled(context, Constants.SIM2), + isActiveSIM2 = Settings.getActiveStatus(context, Constants.SIM2), + isIncomingCallEventsSIM2Enabled = Settings.isIncomingCallEventsEnabled(context, Constants.SIM2), + encryptionKey = Settings.getEncryptionKey(context) ?: "", + isEncryptReceivedMessagesEnabled = Settings.encryptReceivedMessages(context) + ) + } + + fun setDebugLogEnabled(context: Context, enabled: Boolean) { + Settings.setDebugLogEnabled(context, enabled) + _uiState.value = _uiState.value.copy(isDebugLogEnabled = enabled) + } + + fun setIncomingSIM1Enabled(context: Context, enabled: Boolean) { + Settings.setIncomingActiveSIM1(context, enabled) + _uiState.value = _uiState.value.copy(isIncomingSIM1Enabled = enabled) + } + + fun setActiveSIM1(context: Context, enabled: Boolean) { + Settings.setActiveStatusAsync(context, enabled, Constants.SIM1) + _uiState.value = _uiState.value.copy(isActiveSIM1 = enabled) + } + + fun setIncomingCallEventsSIM1Enabled(context: Context, enabled: Boolean) { + Settings.setIncomingCallEventsEnabled(context, Constants.SIM1, enabled) + _uiState.value = _uiState.value.copy(isIncomingCallEventsSIM1Enabled = enabled) + } + + fun setIncomingSIM2Enabled(context: Context, enabled: Boolean) { + Settings.setIncomingActiveSIM2(context, enabled) + _uiState.value = _uiState.value.copy(isIncomingSIM2Enabled = enabled) + } + + fun setActiveSIM2(context: Context, enabled: Boolean) { + Settings.setActiveStatusAsync(context, enabled, Constants.SIM2) + _uiState.value = _uiState.value.copy(isActiveSIM2 = enabled) + } + + fun setIncomingCallEventsSIM2Enabled(context: Context, enabled: Boolean) { + Settings.setIncomingCallEventsEnabled(context, Constants.SIM2, enabled) + _uiState.value = _uiState.value.copy(isIncomingCallEventsSIM2Enabled = enabled) + } + + fun setEncryptionKey(context: Context, key: String) { + val trimmedKey = key.trim() + if (trimmedKey.isEmpty()) { + Settings.setEncryptionKey(context, null) + Settings.setEncryptReceivedMessages(context, false) + _uiState.value = _uiState.value.copy(encryptionKey = "", isEncryptReceivedMessagesEnabled = false) + } else { + Settings.setEncryptionKey(context, trimmedKey) + _uiState.value = _uiState.value.copy(encryptionKey = trimmedKey) + } + } + + fun setEncryptReceivedMessagesEnabled(context: Context, enabled: Boolean) { + Settings.setEncryptReceivedMessages(context, enabled) + _uiState.value = _uiState.value.copy(isEncryptReceivedMessagesEnabled = enabled) + } + + fun logout(context: Context, onLogoutComplete: () -> Unit) { + Settings.setApiKeyAsync(context, null) + Settings.setSIM1PhoneNumber(context, null) + Settings.setSIM2PhoneNumber(context, null) + Settings.setActiveStatusAsync(context, true, Constants.SIM1) + Settings.setActiveStatusAsync(context, true, Constants.SIM2) + Settings.setIncomingActiveSIM1(context, true) + Settings.setIncomingActiveSIM2(context, true) + Settings.setUserID(context, null) + Settings.setEncryptionKey(context, null) + Settings.setEncryptReceivedMessages(context, false) + Settings.setFcmTokenLastUpdateTimestampAsync(context, 0) + Settings.setIncomingCallEventsEnabled(context, Constants.SIM1, false) + Settings.setIncomingCallEventsEnabled(context, Constants.SIM2, false) + onLogoutComplete() + } +} diff --git a/android/app/src/main/java/com/httpsms/ui/theme/Theme.kt b/android/app/src/main/java/com/httpsms/ui/theme/Theme.kt new file mode 100644 index 000000000..88c032fa6 --- /dev/null +++ b/android/app/src/main/java/com/httpsms/ui/theme/Theme.kt @@ -0,0 +1,75 @@ +package com.httpsms.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +val Blue500 = Color(0xFF2196F3) +val Pink500 = Color(0xFFE91E63) +val LogoGreen = Color(0xFF70AB5C) + +private val DarkColorScheme = darkColorScheme( + primary = Blue500, + secondary = Pink500, + tertiary = Color.White +) + +private val LightColorScheme = lightColorScheme( + primary = Blue500, + secondary = Pink500, + tertiary = Color.Black + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun HttpSmsTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.background.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/android/app/src/main/java/com/httpsms/ui/theme/Typography.kt b/android/app/src/main/java/com/httpsms/ui/theme/Typography.kt new file mode 100644 index 000000000..d8030fef1 --- /dev/null +++ b/android/app/src/main/java/com/httpsms/ui/theme/Typography.kt @@ -0,0 +1,17 @@ +package com.httpsms.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) diff --git a/android/app/src/main/res/layout/activity_login.xml b/android/app/src/main/res/layout/activity_login.xml deleted file mode 100644 index 03552be57..000000000 --- a/android/app/src/main/res/layout/activity_login.xml +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 758494754..000000000 --- a/android/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,232 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/layout/activity_settings.xml b/android/app/src/main/res/layout/activity_settings.xml deleted file mode 100644 index bfc078cae..000000000 --- a/android/app/src/main/res/layout/activity_settings.xml +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/build.gradle.kts b/android/build.gradle.kts index ee8d1b8d2..3be829b64 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -5,13 +5,15 @@ buildscript { mavenCentral() } dependencies { - classpath("com.google.gms:google-services:4.4.2") + classpath("com.google.gms:google-services:4.5.0") } } plugins { id("com.android.application") version "9.2.1" apply false id("com.android.library") version "9.2.1" apply false + id("org.jetbrains.kotlin.android") version "2.4.0" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.4.0" apply false } tasks.register("clean") { diff --git a/android/gradle/gradle-daemon-jvm.properties b/android/gradle/gradle-daemon-jvm.properties new file mode 100644 index 000000000..6c1139ec0 --- /dev/null +++ b/android/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,12 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect +toolchainVersion=21 diff --git a/api/docs/docs.go b/api/docs/docs.go index c57458255..8cd408cb1 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -871,7 +871,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.PhoneResponse" + "$ref": "#/definitions/responses.MessageThreadResponse" } }, "400": { @@ -886,6 +886,12 @@ const docTemplate = `{ "$ref": "#/definitions/responses.Unauthorized" } }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, "422": { "description": "Unprocessable Entity", "schema": { @@ -3379,7 +3385,7 @@ const docTemplate = `{ }, "request_id": { "type": "string", - "example": "bulk-csv-a1B2c3D4e5" + "example": "bulk-httpsms-file.csv" }, "scheduled_count": { "type": "integer", @@ -3691,6 +3697,7 @@ const docTemplate = `{ "created_at", "id", "is_archived", + "is_read", "last_message_content", "last_message_id", "order_timestamp", @@ -3720,6 +3727,10 @@ const docTemplate = `{ "type": "boolean", "example": false }, + "is_read": { + "type": "boolean", + "example": true + }, "last_message_content": { "type": "string", "example": "This is a sample message content" @@ -3760,6 +3771,7 @@ const docTemplate = `{ "messages_per_minute", "phone_number", "sim", + "unarchive_thread", "updated_at", "user_id" ], @@ -3804,6 +3816,11 @@ const docTemplate = `{ "sim": { "$ref": "#/definitions/entities.SIM" }, + "unarchive_thread": { + "description": "UnarchiveThread moves an archived message thread back to the inbox when a new message is received on this phone.", + "type": "boolean", + "example": false + }, "updated_at": { "type": "string", "example": "2022-06-05T14:26:10.303278+03:00" @@ -4382,13 +4399,14 @@ const docTemplate = `{ }, "requests.MessageThreadUpdate": { "type": "object", - "required": [ - "is_archived" - ], "properties": { "is_archived": { "type": "boolean", "example": true + }, + "is_read": { + "type": "boolean", + "example": true } } }, @@ -4436,7 +4454,8 @@ const docTemplate = `{ "messages_per_minute", "missed_call_auto_reply", "phone_number", - "sim" + "sim", + "unarchive_thread" ], "properties": { "fcm_token": { @@ -4473,6 +4492,11 @@ const docTemplate = `{ "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", "type": "string", "example": "SIM1" + }, + "unarchive_thread": { + "description": "UnarchiveThread moves an archived thread back to the inbox when a new message is received on this phone.", + "type": "boolean", + "example": false } } }, @@ -4892,6 +4916,27 @@ const docTemplate = `{ } } }, + "responses.MessageThreadResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.MessageThread" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } + }, "responses.MessageThreadsResponse": { "type": "object", "required": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 009797ca9..ac9c12c98 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -868,7 +868,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.PhoneResponse" + "$ref": "#/definitions/responses.MessageThreadResponse" } }, "400": { @@ -883,6 +883,12 @@ "$ref": "#/definitions/responses.Unauthorized" } }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, "422": { "description": "Unprocessable Entity", "schema": { @@ -3376,7 +3382,7 @@ }, "request_id": { "type": "string", - "example": "bulk-csv-a1B2c3D4e5" + "example": "bulk-httpsms-file.csv" }, "scheduled_count": { "type": "integer", @@ -3688,6 +3694,7 @@ "created_at", "id", "is_archived", + "is_read", "last_message_content", "last_message_id", "order_timestamp", @@ -3717,6 +3724,10 @@ "type": "boolean", "example": false }, + "is_read": { + "type": "boolean", + "example": true + }, "last_message_content": { "type": "string", "example": "This is a sample message content" @@ -3757,6 +3768,7 @@ "messages_per_minute", "phone_number", "sim", + "unarchive_thread", "updated_at", "user_id" ], @@ -3801,6 +3813,11 @@ "sim": { "$ref": "#/definitions/entities.SIM" }, + "unarchive_thread": { + "description": "UnarchiveThread moves an archived message thread back to the inbox when a new message is received on this phone.", + "type": "boolean", + "example": false + }, "updated_at": { "type": "string", "example": "2022-06-05T14:26:10.303278+03:00" @@ -4379,13 +4396,14 @@ }, "requests.MessageThreadUpdate": { "type": "object", - "required": [ - "is_archived" - ], "properties": { "is_archived": { "type": "boolean", "example": true + }, + "is_read": { + "type": "boolean", + "example": true } } }, @@ -4433,7 +4451,8 @@ "messages_per_minute", "missed_call_auto_reply", "phone_number", - "sim" + "sim", + "unarchive_thread" ], "properties": { "fcm_token": { @@ -4470,6 +4489,11 @@ "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", "type": "string", "example": "SIM1" + }, + "unarchive_thread": { + "description": "UnarchiveThread moves an archived thread back to the inbox when a new message is received on this phone.", + "type": "boolean", + "example": false } } }, @@ -4889,6 +4913,27 @@ } } }, + "responses.MessageThreadResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.MessageThread" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } + }, "responses.MessageThreadsResponse": { "type": "object", "required": [ diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index bb0041717..ddc6ae700 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -58,7 +58,7 @@ definitions: example: 30 type: integer request_id: - example: bulk-csv-a1B2c3D4e5 + example: bulk-httpsms-file.csv type: string scheduled_count: example: 50 @@ -319,6 +319,9 @@ definitions: is_archived: example: false type: boolean + is_read: + example: true + type: boolean last_message_content: example: This is a sample message content type: string @@ -346,6 +349,7 @@ definitions: - created_at - id - is_archived + - is_read - last_message_content - last_message_id - order_timestamp @@ -388,6 +392,11 @@ definitions: type: string sim: $ref: '#/definitions/entities.SIM' + unarchive_thread: + description: UnarchiveThread moves an archived message thread back to the + inbox when a new message is received on this phone. + example: false + type: boolean updated_at: example: "2022-06-05T14:26:10.303278+03:00" type: string @@ -402,6 +411,7 @@ definitions: - messages_per_minute - phone_number - sim + - unarchive_thread - updated_at - user_id type: object @@ -847,8 +857,9 @@ definitions: is_archived: example: true type: boolean - required: - - is_archived + is_read: + example: true + type: boolean type: object requests.PhoneAPIKeyStoreRequest: properties: @@ -908,6 +919,11 @@ definitions: 1 SIM slot example: SIM1 type: string + unarchive_thread: + description: UnarchiveThread moves an archived thread back to the inbox when + a new message is received on this phone. + example: false + type: boolean required: - fcm_token - max_send_attempts @@ -916,6 +932,7 @@ definitions: - missed_call_auto_reply - phone_number - sim + - unarchive_thread type: object requests.UserNotificationUpdate: properties: @@ -1215,6 +1232,21 @@ definitions: - message - status type: object + responses.MessageThreadResponse: + properties: + data: + $ref: '#/definitions/entities.MessageThread' + message: + example: Request handled successfully + type: string + status: + example: success + type: string + required: + - data + - message + - status + type: object responses.MessageThreadsResponse: properties: data: @@ -2170,7 +2202,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/responses.PhoneResponse' + $ref: '#/definitions/responses.MessageThreadResponse' "400": description: Bad Request schema: @@ -2179,6 +2211,10 @@ paths: description: Unauthorized schema: $ref: '#/definitions/responses.Unauthorized' + "404": + description: Not Found + schema: + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: diff --git a/api/go.mod b/api/go.mod index cff06f1fc..6e3d5bdb0 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,28 +3,29 @@ module github.com/NdoleStudio/httpsms go 1.25.8 require ( - cloud.google.com/go/cloudtasks v1.18.0 + cloud.google.com/go/cloudtasks v1.19.0 cloud.google.com/go/compute/metadata v0.9.0 - cloud.google.com/go/storage v1.62.3 + cloud.google.com/go/storage v1.64.0 firebase.google.com/go v3.13.0+incompatible - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.33.0 + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0 + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.35.0 github.com/NdoleStudio/go-otelroundtripper v0.0.15 - github.com/NdoleStudio/lemonsqueezy-go v1.3.1 + github.com/NdoleStudio/lemonsqueezy-go v1.3.2 github.com/NdoleStudio/plunk-go v0.0.2 + github.com/NdoleStudio/stacktrace v1.1.0 github.com/avast/retry-go/v5 v5.0.0 github.com/axiomhq/axiom-go v0.32.0 github.com/carlmjohnson/requests v0.25.1 github.com/cloudevents/sdk-go/v2 v2.16.2 github.com/cockroachdb/cockroach-go/v2 v2.4.3 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/dgraph-io/ristretto/v2 v2.4.0 + github.com/dgraph-io/ristretto/v2 v2.4.2 github.com/dustin/go-humanize v1.0.1 github.com/gertd/go-pluralize v0.2.1 github.com/go-hermes/hermes/v2 v2.6.2 - github.com/gofiber/contrib/v3/otel v1.2.1 - github.com/gofiber/contrib/v3/swaggo v1.0.7 - github.com/gofiber/fiber/v3 v3.3.0 + github.com/gofiber/contrib/v3/otel v1.2.2 + github.com/gofiber/contrib/v3/swaggo v1.0.8 + github.com/gofiber/fiber/v3 v3.4.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 github.com/hashicorp/go-retryablehttp v0.7.8 @@ -34,8 +35,7 @@ require ( github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/jszwec/csvutil v1.10.0 github.com/lib/pq v1.12.3 - github.com/nyaruka/phonenumbers v1.8.0 - github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177 + github.com/nyaruka/phonenumbers v1.8.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/pusher/pusher-http-go/v5 v5.1.1 @@ -46,8 +46,8 @@ require ( github.com/swaggo/swag v1.16.6 github.com/thedevsaddam/govalidator v1.9.10 github.com/uptrace/uptrace-go v1.43.0 - github.com/xuri/excelize/v2 v2.10.1 - go.mongodb.org/mongo-driver/v2 v2.7.0 + github.com/xuri/excelize/v2 v2.11.0 + go.mongodb.org/mongo-driver/v2 v2.8.0 go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260624193928-df9c7a836708 go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 @@ -56,11 +56,11 @@ require ( go.opentelemetry.io/otel/sdk v1.44.0 go.opentelemetry.io/otel/sdk/metric v1.44.0 go.opentelemetry.io/otel/trace v1.44.0 - golang.org/x/sync v0.21.0 - google.golang.org/api v0.286.0 + golang.org/x/sync v0.22.0 + google.golang.org/api v0.287.1 google.golang.org/protobuf v1.36.11 gorm.io/driver/postgres v1.6.0 - gorm.io/gorm v1.31.1 + gorm.io/gorm v1.31.2 gorm.io/plugin/opentelemetry v0.1.16 ) @@ -83,7 +83,7 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/firestore v1.22.0 // indirect cloud.google.com/go/iam v1.11.0 // indirect - cloud.google.com/go/longrunning v1.0.0 // indirect + cloud.google.com/go/longrunning v1.2.0 // indirect cloud.google.com/go/monitoring v1.29.0 // indirect cloud.google.com/go/trace v1.16.0 // indirect dario.cat/mergo v1.0.2 // indirect @@ -91,11 +91,11 @@ require ( github.com/ClickHouse/ch-go v0.72.0 // indirect github.com/ClickHouse/clickhouse-go/v2 v2.46.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/PuerkitoBio/goquery v1.12.0 // indirect - github.com/andybalholm/brotli v1.2.1 // indirect + github.com/andybalholm/brotli v1.2.2 // indirect github.com/andybalholm/cascadia v1.3.4 // indirect github.com/buger/jsonparser v1.2.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -113,25 +113,25 @@ require ( github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.23.1 // indirect + github.com/go-openapi/jsonpointer v0.24.0 // indirect github.com/go-openapi/jsonreference v0.21.6 // indirect github.com/go-openapi/spec v0.22.6 // indirect - github.com/go-openapi/swag/conv v0.26.1 // indirect - github.com/go-openapi/swag/jsonname v0.26.1 // indirect - github.com/go-openapi/swag/jsonutils v0.26.1 // indirect - github.com/go-openapi/swag/loading v0.26.1 // indirect - github.com/go-openapi/swag/stringutils v0.26.1 // indirect - github.com/go-openapi/swag/typeutils v0.26.1 // indirect - github.com/go-openapi/swag/yamlutils v0.26.1 // indirect + github.com/go-openapi/swag/conv v0.27.0 // indirect + github.com/go-openapi/swag/jsonname v0.27.0 // indirect + github.com/go-openapi/swag/jsonutils v0.27.0 // indirect + github.com/go-openapi/swag/loading v0.27.0 // indirect + github.com/go-openapi/swag/stringutils v0.27.0 // indirect + github.com/go-openapi/swag/typeutils v0.27.0 // indirect + github.com/go-openapi/swag/yamlutils v0.27.0 // indirect github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/goccy/go-json v0.10.6 // indirect - github.com/gofiber/schema v1.8.0 // indirect - github.com/gofiber/utils/v2 v2.1.0 // indirect + github.com/gofiber/schema v1.8.2 // indirect + github.com/gofiber/utils/v2 v2.2.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.17 // indirect - github.com/googleapis/gax-go/v2 v2.22.0 // indirect + github.com/googleapis/gax-go/v2 v2.23.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -144,7 +144,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/compress v1.19.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect @@ -170,7 +170,7 @@ require ( github.com/tiendc/go-deepcopy v1.7.2 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.71.0 // indirect + github.com/valyala/fasthttp v1.72.0 // indirect github.com/vanng822/css v1.0.1 // indirect github.com/vanng822/go-premailer v1.34.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -196,19 +196,19 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.53.0 // indirect + golang.org/x/crypto v0.54.0 // indirect golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/net v0.57.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.46.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20260622175928-b703f567277d // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect - google.golang.org/grpc v1.81.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect + google.golang.org/grpc v1.82.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/mysql v1.6.0 // indirect diff --git a/api/go.sum b/api/go.sum index f818891d8..bea96100b 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,7 +1,5 @@ bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= @@ -10,35 +8,22 @@ cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/cloudtasks v1.18.0 h1:KzT7hfix/9/xAf20tNPIxwX59XGpRF0Lun2t8LHOj9E= -cloud.google.com/go/cloudtasks v1.18.0/go.mod h1:3KeCxwtGEyaySL7CR3lMmEa2I4mq1ynXdgmfNiO4RYE= -cloud.google.com/go/compute v1.64.0 h1:7MmuzeAxlG5MOG5PQD2NLtyYR6bWjkvGljRu7pByoRU= -cloud.google.com/go/compute v1.64.0/go.mod h1:eHhcRZ6vf70fQCS3VEsiWSh+nQ+tLvSMb7mwLQskgN0= +cloud.google.com/go/cloudtasks v1.19.0 h1:+RK0lPIB6TlcBP7JyqmmhCNihp1Iw4QQ8uxcvlKhBVQ= +cloud.google.com/go/cloudtasks v1.19.0/go.mod h1:8q8wNubq0jFvXW5Pz8P3O7QWJBXOmfrY918FqTgIqHA= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/firestore v1.22.0 h1:avooeboIq37vKXobrbPUFhFBxS/c3FqmWoX0xs8dO6E= cloud.google.com/go/firestore v1.22.0/go.mod h1:PaM4i7i7ruALSKmlpHXXZaPObcZw0W7ie5UOPr72iTU= -cloud.google.com/go/iam v1.10.0 h1:cWWt8u8jXv3MzpvBmQgNClvvbVCRukruCJAnoK3fIJY= -cloud.google.com/go/iam v1.10.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM= cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= -cloud.google.com/go/logging v1.17.0 h1:rUFekZYwHiKElXCyz3zYBGz4BOeIqzgCKxVLdgrZ5mY= -cloud.google.com/go/logging v1.17.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k= -cloud.google.com/go/longrunning v0.12.0 h1:wLv2hXvID9zHejLtcPo1B0JBjErnwZCYAPKSTa65xpY= -cloud.google.com/go/longrunning v0.12.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= -cloud.google.com/go/longrunning v1.0.0 h1:lwzWEYD8+NkYV7dhexOz6kmlvajZA70+bW/xMhRVVdY= -cloud.google.com/go/longrunning v1.0.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= -cloud.google.com/go/monitoring v1.28.0 h1:jOe0Wkm+a56ptZnEeyHevXo7+KPWAPPP5wUTEJdP7GY= -cloud.google.com/go/monitoring v1.28.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM= +cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= +cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= +cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0= cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY= cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM= -cloud.google.com/go/storage v1.62.2 h1:WgR4U9n7bIzXkkVnwPKKE8bkaKUNsHG+0MAAlh9DGU4= -cloud.google.com/go/storage v1.62.2/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= -cloud.google.com/go/storage v1.62.3 h1:SZq1t23NCI+e96dH77Dg3PEfsNNEjqO8zE5AnD8gVD0= -cloud.google.com/go/storage v1.62.3/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= -cloud.google.com/go/trace v1.15.0 h1:kAYkTwKyYHkGtAGFuu6qaUFRBkOVr+d1Yo44yZtGtgg= -cloud.google.com/go/trace v1.15.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= +cloud.google.com/go/storage v1.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSPZg= +cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ= cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E= cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -47,31 +32,20 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4= firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= -github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= -github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= github.com/ClickHouse/ch-go v0.72.0 h1:DSyUd4kuxisOVXlZSXyIQYBAajSErZWC379651DpAMU= github.com/ClickHouse/ch-go v0.72.0/go.mod h1:eeWlJavWDsMf5fZzLNCYaBiMxVoREJYK00aiZ9FJ3E0= github.com/ClickHouse/clickhouse-go/v2 v2.46.0 h1:s3eRy+hYmu5uzotB6ZhDofgHu8kDgGN/fpmjxRkqSpk= github.com/ClickHouse/clickhouse-go/v2 v2.46.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 h1:O2sXMyJh8b7devAGdE+163xtRurt0RVpB6DIzX5vGfg= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0/go.mod h1:hEpiGU18xf70qb3jbTcIggWAiEfX/cOIVc2OTe4OegA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.32.0 h1:ftVmySBwuOJafpEJnnZvco+iV3p6Lokgu2sd89/qY7M= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.32.0/go.mod h1:nikqFGPI5OGwEsdxXzd3f58sB3tzkjqpqwYOV/S1rmo= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.33.0 h1:92kCbSANHUnbS7tzc9uhUx4H+MF1Lr/QXInoPkjz6JU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.33.0/go.mod h1:n2lr2dT7IuNeGhW6iB/+nJB+Fo9vZxCGwEQOy2Hc15Y= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0 h1:ZIT85vKP7LBS84XJ0WdJ3dPOX3iz4j3c0+lpajGQMyo= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0/go.mod h1:rqP9UEhOXv9WhQ7Gjz+G5y/pf8+BJZW5/Ts0AhE0PwE= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 h1:0YP0+/ixwu+Uqeu/FGiBZNQ19huiUxxiPXIc9WsLKuQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0/go.mod h1:6ZZMQhZKDvUvkJw2rc+oDP90tMMzuU/J+5HG1ZmPOmE= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0 h1:c/Ivw7FuawPLfrr+zB0LZKeCchO2cAHQpF2qZ6OV7rQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0/go.mod h1:Zba7lknY/d78oxbKqFTmCsaGwfpzeJ3ktrrLXtnTV6g= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.35.0 h1:PaC2zW6tfeh9rQydOQq6Pff4xpWwUDMCMolQXiypq0o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.35.0/go.mod h1:3zRs1e/YFBfj7yv/2HFUlH1EKaP0priee5IqmYUHMP4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.59.0 h1:xTXsqDOj5k9mK3VVWHYUryryJCIdYfXxdjKFwpzINUw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.59.0/go.mod h1:V9g30lTKzfUsEW+gpWssck6u9IhARajmipodImLLcwI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0 h1:18FRm6ZcN/x9+ZmhMr96hLcTtlLn2/gHPuDLVeg7XcY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -82,16 +56,16 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/NdoleStudio/go-otelroundtripper v0.0.15 h1:lClvnSNRKfdPejUcMaFa3vv9nkIdONJOzO/WSNBVY2w= github.com/NdoleStudio/go-otelroundtripper v0.0.15/go.mod h1:YkRryIMC2i4a/6S86isXH+bht3Qp4RRB0rOnnnHBPjU= -github.com/NdoleStudio/lemonsqueezy-go v1.3.1 h1:lMUVgdAx2onbOUJIVPR05xAANYuCMXBRaGWpAdA4LiM= -github.com/NdoleStudio/lemonsqueezy-go v1.3.1/go.mod h1:xKRsRX1jSI6mLrVXyWh2sF/1isxTioZrSjWy6HpA3xQ= +github.com/NdoleStudio/lemonsqueezy-go v1.3.2 h1:XUNOzpMriXErxRQRW0dQT0OvkNBP6VAyZKlFWypyGrc= +github.com/NdoleStudio/lemonsqueezy-go v1.3.2/go.mod h1:xKRsRX1jSI6mLrVXyWh2sF/1isxTioZrSjWy6HpA3xQ= github.com/NdoleStudio/plunk-go v0.0.2 h1:afPW7MHK4Z3rsybpJBnmTmxKCLKF1M7sPI+BNGPf35A= github.com/NdoleStudio/plunk-go v0.0.2/go.mod h1:pqG3zKhpn/A2bL1K+WsWzvfTpOeSkYgXhNk5H65uEc8= +github.com/NdoleStudio/stacktrace v1.1.0 h1:Ai0j//4tDo/So3nR8bZgS3pQ4NYmViHlarpIReiyb5k= +github.com/NdoleStudio/stacktrace v1.1.0/go.mod h1:WOBcbXShYu+v8JtjFEM+rRV9GJW50m+dwIvp4sLuVTg= github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= -github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= -github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= -github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= +github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg= github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM= github.com/avast/retry-go/v5 v5.0.0 h1:kf1Qc2UsTZ4qq8elDymqfbISvkyMuhgRxuJqX2NHP7k= @@ -102,8 +76,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= -github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/carlmjohnson/requests v0.25.1 h1:17zNRLecxtAjhtdEIV+F+wrYfe+AGZUjWJtpndcOUYA= @@ -128,8 +100,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU= -github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= +github.com/dgraph-io/ristretto/v2 v2.4.2 h1:x0cvjmUKxt764Yxdk2nr94we1AvPPAMh1rh5TQ+Jo80= +github.com/dgraph-io/ristretto/v2 v2.4.2/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -144,8 +116,6 @@ github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMD github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -167,64 +137,47 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= -github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/jsonpointer v0.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M= +github.com/go-openapi/jsonpointer v0.24.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= -github.com/go-openapi/spec v0.22.5 h1:KhO7RBlKQfonUWX2WzQCoLIXVA6AcNqDGZ3a1Dutdlo= -github.com/go-openapi/spec v0.22.5/go.mod h1:vxpOtMya5TXtENXKE5bKqv5NjocVhyhxHrlZfvKnZ74= github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic= github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= -github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= -github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg= -github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A= -github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= -github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= -github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= -github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= -github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= -github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= -github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY= -github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ= -github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= -github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= -github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w= -github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E= -github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= -github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= -github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU= -github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk= -github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= -github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= -github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU= -github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o= -github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= -github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= -github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk= -github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M= -github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8= -github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk= -github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo= -github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8= +github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8= +github.com/go-openapi/swag/jsonname v0.27.0 h1:4QVB//CKOdE8IOiBg19JNY2wfDS48MhesIquYBy2rUE= +github.com/go-openapi/swag/jsonname v0.27.0/go.mod h1:I1YsyvvhBuZsFXSW6I7ODfdyq13p7hDil//1T9/pFFk= +github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE= +github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0 h1:+d7C7Ur/SsGg/UZ9G0JEovnfRqtMNZCJQGKc2h/ojoE= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= +github.com/go-openapi/swag/loading v0.27.0 h1:s8DA9aPEdFH6OluHUYUn3DnIuoTdyWs9RwffXBUfyeI= +github.com/go-openapi/swag/loading v0.27.0/go.mod h1:VOz+Jg6UGGywcmRvYsI4fvtp+bd7NfioseGEPleYdA4= +github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio= +github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= +github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o= +github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= +github.com/go-openapi/swag/yamlutils v0.27.0 h1:bQ6eAMil5X9tdcf7dMn4t15alzG6jddnrKPuKa/zxKM= +github.com/go-openapi/swag/yamlutils v0.27.0/go.mod h1:yRfIo7qqVkmJRQjX8exjA3AfcI8rH1KDNPsTparoCv4= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= +github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= +github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gofiber/contrib/v3/otel v1.2.1 h1:S+ViO/LvMA8xLAitIQ6uMW7amlXlT88buD7az0PFtDk= -github.com/gofiber/contrib/v3/otel v1.2.1/go.mod h1:cvZ+7Lc3zk72ucH6hDiW46LzEFHFvBMvqJ8HakfITmo= -github.com/gofiber/contrib/v3/swaggo v1.0.7 h1:NwjBOldwhl4JaUVpo3XQ+L9d5vl7bjc24GJx4e5rqfs= -github.com/gofiber/contrib/v3/swaggo v1.0.7/go.mod h1:UouDgg79E4BRhRN7KAJlMMYnbW3VVXW2Sc2q6O5hfnU= -github.com/gofiber/fiber/v3 v3.3.0 h1:QBd3sYCqdy6Qs5gJYzSw4I4SbqL204jPqpdub/ueiw8= -github.com/gofiber/fiber/v3 v3.3.0/go.mod h1:YH7/TAoRaU4kF8slDCtQuFJ1NzC+3MtxUI4KfvQtaIA= -github.com/gofiber/schema v1.8.0 h1:NGsC9toPHmj8Xg4KpznuXBzNmHG6V5YV0tXKpKMcmis= -github.com/gofiber/schema v1.8.0/go.mod h1:lmbXPQ8hvzXSLkdS2DS7pb4kpunC2Roh7Sj3HMjGfzA= -github.com/gofiber/utils/v2 v2.1.0 h1:WSu4COJhJw9moNfJu2nQvaM9AFvAQ/nZbigjhHqKgOQ= -github.com/gofiber/utils/v2 v2.1.0/go.mod h1:DdOgEVwQTi8cou/AKWPqhXOR4fHGRVhA/rEWL3IXG7Q= +github.com/gofiber/contrib/v3/otel v1.2.2 h1:DQ0S5dIDb5UsnUT2HI5rXAwzy/pIiOMZkO6izQr8Fok= +github.com/gofiber/contrib/v3/otel v1.2.2/go.mod h1:icVCf1XJoqCuY7Fq/EAbaTk30ngA1Fc9r4zq/P2FBvk= +github.com/gofiber/contrib/v3/swaggo v1.0.8 h1:11YmMVbkMO7zv6YR0gdNfRWcs5ZVUkDpR21AhjhbK00= +github.com/gofiber/contrib/v3/swaggo v1.0.8/go.mod h1:7jyBZxqQ4MjOxIidOvLQ9jNAxNk05yKpid40OYb3pLE= +github.com/gofiber/fiber/v3 v3.4.0 h1:F0aND4vwZF7dR7cbvSwFQQEpBU902XHKWxrLsFBkVqw= +github.com/gofiber/fiber/v3 v3.4.0/go.mod h1:nAhJfdxUIJJph2tPWPmqWf8QDIN2iiqQiQf3lENZpdk= +github.com/gofiber/schema v1.8.2 h1:wq+LO2xEGlsqma/8Akp9PUebQ6vcsYmF0xYQ4F2ijvU= +github.com/gofiber/schema v1.8.2/go.mod h1:iyAMJztdyky7Pk2U7bwUP3EHjzMqUNjZ/hglE0nYO/g= +github.com/gofiber/utils/v2 v2.2.0 h1:YSSmCzQponq/f9uSOg2HtXC5qK1Dmor0o6DqaQVz8GE= +github.com/gofiber/utils/v2 v2.2.0/go.mod h1:Ieopk6sQh7rbhQ12aBNCJtJuG0gxAg0nz63sFCrrOmE= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= @@ -246,12 +199,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw= -github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE= github.com/googleapis/enterprise-certificate-proxy v0.3.17 h1:73NfMHdiqo9JFU9+7a5ExpVa10/R29pXfZIaW559nrg= github.com/googleapis/enterprise-certificate-proxy v0.3.17/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= -github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= -github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= +github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= +github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= @@ -274,11 +225,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= -github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= -github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jaswdr/faker/v2 v2.9.1 h1:J0Rjqb2/FquZnoZplzkGVL5LmhNkeIpvsSMoJKzn+8E= @@ -295,8 +243,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jszwec/csvutil v1.10.0 h1:upMDUxhQKqZ5ZDCs/wy+8Kib8rZR8I8lOR34yJkdqhI= github.com/jszwec/csvutil v1.10.0/go.mod h1:/E4ONrmGkwmWsk9ae9jpXnv9QT8pLHEPcCirMFhxG9I= -github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -309,8 +257,6 @@ github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= @@ -324,10 +270,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nyaruka/phonenumbers v1.7.5 h1:xXnwzxyuoRtZOk0VKboO0XqFEpcG7KvrTejcUJOSJBU= -github.com/nyaruka/phonenumbers v1.7.5/go.mod h1:fsKPJ70O9JetEA4ggnJadYTFWwtGPvu/lETTXNXq6Cs= -github.com/nyaruka/phonenumbers v1.8.0 h1:TrXNJmbwcAHajzDqin3mLWw57vqLUA6ZjVdeNds0heQ= -github.com/nyaruka/phonenumbers v1.8.0/go.mod h1:fsKPJ70O9JetEA4ggnJadYTFWwtGPvu/lETTXNXq6Cs= +github.com/nyaruka/phonenumbers v1.8.1 h1:2K9YMQuv1dCGqjjzB1DwmdCe89khT4KPBQb2CxAMMlU= +github.com/nyaruka/phonenumbers v1.8.1/go.mod h1:fsKPJ70O9JetEA4ggnJadYTFWwtGPvu/lETTXNXq6Cs= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U= @@ -336,16 +280,12 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= -github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177 h1:nRlQD0u1871kaznCnn1EvYiMbum36v7hw1DLPEjds4o= -github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177/go.mod h1:ao5zGxj8Z4x60IOVYZUbDSmt3R8Ddo080vEgPosHpak= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/paulmach/orb v0.13.0 h1:r7n7mQGGF+cj/CbcivEj9J3HGK+XR+yXnvzRdq9saIw= github.com/paulmach/orb v0.13.0/go.mod h1:6scRWINywA2Jf05dcjOfLfxrUIMECvTSG2MVbRLxu/k= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= -github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -357,20 +297,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pusher/pusher-http-go/v5 v5.1.1 h1:ZLUGdLA8yXMvByafIkS47nvuXOHrYmlh4bsQvuZnYVQ= github.com/pusher/pusher-http-go/v5 v5.1.1/go.mod h1:Ibji4SGoUDtOy7CVRhCiEpgy+n5Xv6hSL/QqYOhmWW8= -github.com/redis/go-redis/extra/rediscmd/v9 v9.20.0 h1:hx/fwTZnY+7hJ3P+zcdFthE6tw/uAEN9TNIZ1CQ7z8Y= -github.com/redis/go-redis/extra/rediscmd/v9 v9.20.0/go.mod h1:IL+oJIDTKpwgW/ld8CA7ldqO2ACgmbOd2WK9WgPqywY= github.com/redis/go-redis/extra/rediscmd/v9 v9.21.0 h1:jsV3tyMeJrEoc2f3EhNf7qoBW3NEZW7l/4ziT3M+OJI= github.com/redis/go-redis/extra/rediscmd/v9 v9.21.0/go.mod h1:e5t17bY9cEpVV+xw2U7jsPOKkXBtL5IQmNVABShnHUk= -github.com/redis/go-redis/extra/redisotel/v9 v9.20.0 h1:+Bf7EiyYpSyYf5/JBnsHPDR+tBQ4CgRlBt4WV+QPS0k= -github.com/redis/go-redis/extra/redisotel/v9 v9.20.0/go.mod h1:dRCuOymGRykV8SJVDjvOID00wi2gOthNGg3iLEyrq0A= github.com/redis/go-redis/extra/redisotel/v9 v9.21.0 h1:36qq3rbF2If2CP0zGHHF8o/4XDluErn6DD0c9/L2iNI= github.com/redis/go-redis/extra/redisotel/v9 v9.21.0/go.mod h1:7y2cVB/LXXLHqHOO2jCVzBqimIQk1w7Rp9WSpyVY/o8= -github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0= -github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E= github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= -github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= -github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0= github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= @@ -381,16 +313,14 @@ github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/shamaton/msgpack/v3 v3.1.2 h1:d5gWAIyMU4M0WgDjz6IFSCuXJUA2dFwRHBpDclE8CLw= -github.com/shamaton/msgpack/v3 v3.1.2/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= +github.com/shamaton/msgpack/v3 v3.2.0 h1:1q2Ms+MWmuRju+PuDMSFDB7p7621npeX4zprJN5Zck8= +github.com/shamaton/msgpack/v3 v3.2.0/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= -github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E= github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= @@ -422,12 +352,10 @@ github.com/uptrace/uptrace-go v1.43.0 h1:5QuCdyFJdWUEXx6Fr6sYfezdgO6n6lnkOvUTLly github.com/uptrace/uptrace-go v1.43.0/go.mod h1:ehDTIdtBSolg4Z0CCvg1C8yR6VX1YFDqBcg2KmsXWn0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k= -github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA= +github.com/valyala/fasthttp v1.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M= +github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk= github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8= github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= -github.com/vanng822/go-premailer v1.33.0 h1:nglIpKn/7e3kIAwYByiH5xpauFur7RwAucqyZ59hcic= -github.com/vanng822/go-premailer v1.33.0/go.mod h1:LGYI7ym6FQ7KcHN16LiQRF+tlan7qwhP1KEhpTINFpo= github.com/vanng822/go-premailer v1.34.0 h1:CW7RUnjCfXrkuCbgC2wi/Cub7IwKslJWD/OkIBlcQUk= github.com/vanng822/go-premailer v1.34.0/go.mod h1:LGYI7ym6FQ7KcHN16LiQRF+tlan7qwhP1KEhpTINFpo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -440,8 +368,8 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= -github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= +github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88= +github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= @@ -453,42 +381,26 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8= -go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= -go.mongodb.org/mongo-driver/v2 v2.7.0 h1:RO+zqavD2/GCL3cxOMyZhx6R9Irzr8/6gsoqx5tcY/c= -go.mongodb.org/mongo-driver/v2 v2.7.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8= +go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib v1.44.0 h1:cVL0yu3uyrXkAmxonxvzYysIo5EZa8jKh3740MzxBzI= go.opentelemetry.io/contrib v1.44.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw= go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s= -go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260513205827-ba143fc95a5e h1:OX282aWfZNOrSVUPF59HlRhyA+MDcyi4kI8WWXt6A8I= -go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260513205827-ba143fc95a5e/go.mod h1:lw7VQzmNsmkZBRQqOQiREGxO3GtzG/pOVEmKufablmA= go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260624193928-df9c7a836708 h1:s5k/FTgmE4Mj2jFG38Dj5TQgxDRMG92fRK3puTdMHew= go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260624193928-df9c7a836708/go.mod h1:VgtIsrVXKo8KmG2sFvQazHcQ98P2FHQ+ZePXtpTPjqs= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= -go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0 h1:jhVIQEprwUTV+KfzzliLidclhoTOoHTgdz96kAyR8mU= -go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0/go.mod h1:4HsdbLUbernaTnA8CNaNE+1g026SciXb3juRYe3l8EY= go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0 h1:MtkMsuRo3zEXTTMALfyrszwCDZTkB6wolyPjbwFAdq0= go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0/go.mod h1:FYTxnpsm+UPD0erZNq20GvnM8T2YQHiHtT2vokdpoac= -go.opentelemetry.io/contrib/processors/minsev v0.16.0 h1:bjTZkvAKnG1mqWgCjU7RkOkHRTMsGlJO/UlqjRCweeU= -go.opentelemetry.io/contrib/processors/minsev v0.16.0/go.mod h1:R2mmaDsqsWb+Y0mQkPifiCwifdotrG4fFoD4z0tim+g= go.opentelemetry.io/contrib/processors/minsev v0.16.1 h1:DYL02u57VGQjrG8c09i6Bx5R74h4XxLj75wEk5h8/DA= go.opentelemetry.io/contrib/processors/minsev v0.16.1/go.mod h1:VnF+kZnkagrTfTb2mV+6PTMAtPulgjpcUajm8sRk3tQ= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0/go.mod h1:MP4eemTiI9zC8fgg+DYynhYDYf3ba72S376TvP+Ye0Q= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA= @@ -497,14 +409,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUY go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D/gAmznQTd4dxxMA93aKbcpKqiTCs= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0= -go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= -go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs= go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= @@ -513,13 +421,10 @@ go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/x go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= -go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY= go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg= +go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= @@ -539,117 +444,64 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= -golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= +golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I= -google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= -google.golang.org/api v0.286.0 h1:TdTXMvzYKnWV1/lPbCdbXRqBrkDqjPto22H2xeZZ8LI= -google.golang.org/api v0.286.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= +google.golang.org/api v0.287.1 h1:LiyJx32VU3cwQfLchn/513qKhc25hq0pEANYJoWNnnI= +google.golang.org/api v0.287.1/go.mod h1:lM2kYRzYUCBY91P9h6VF1PYmvhxii3O5hji37qRvIcY= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 h1:JjVGDZYWkJWZcxveJGzfkXC5myDVWAd4dZdgbzrDUv8= -google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348/go.mod h1:95PqD4xM+AdOcBGsmgfaofXsiA37uXDtDufVbntT3TU= google.golang.org/genproto v0.0.0-20260622175928-b703f567277d h1:CP5omUq8AJTiWMrPKM1WRLJ7zZeXd9OPcQD3TbBNAyY= google.golang.org/genproto v0.0.0-20260622175928-b703f567277d/go.mod h1:DrwuGJgFSEVNpv3S5Q5VxhRTvdnjauw9GtvwVOEARfA= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d h1:xr2lwHI91bn3UiXcnyzRMQjp2LRiM8wEHzwUaE0YhTs= -google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d/go.mod h1:O0ZOWSrfWfJ+Z5HbwZ+wNtHsg/vk1k2C/w67eww8PfQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d h1:mpAgMyM9vQHxycBlDq50y1VHpfSfVwzXvrQKtYbXuUY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU= +google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= @@ -670,7 +522,7 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= -gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= -gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= +gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/plugin/opentelemetry v0.1.16 h1:Kypj2YYAliJqkIczDZDde6P6sFMhKSlG5IpngMFQGpc= gorm.io/plugin/opentelemetry v0.1.16/go.mod h1:P3RmTeZXT+9n0F1ccUqR5uuTvEXDxF8k2UpO7mTIB2Y= diff --git a/api/pkg/cache/memory_cache.go b/api/pkg/cache/memory_cache.go index 2e32bcffb..a5b0487d7 100644 --- a/api/pkg/cache/memory_cache.go +++ b/api/pkg/cache/memory_cache.go @@ -2,11 +2,10 @@ package cache import ( "context" - "fmt" "time" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ttlCache "github.com/patrickmn/go-cache" ) @@ -31,7 +30,7 @@ func (cache *memoryCache) Get(ctx context.Context, key string) (value string, er response, ok := cache.store.Get(key) if !ok { - return "", stacktrace.NewError(fmt.Sprintf("no item found in cache with key [%s]", key)) + return "", stacktrace.NewErrorf("no item found in cache with key [%s]", key) } return response.(string), nil diff --git a/api/pkg/cache/redis_cache.go b/api/pkg/cache/redis_cache.go index 18a6a887b..46d9044bd 100644 --- a/api/pkg/cache/redis_cache.go +++ b/api/pkg/cache/redis_cache.go @@ -3,11 +3,10 @@ package cache import ( "context" "errors" - "fmt" "time" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "github.com/redis/go-redis/v9" ) @@ -32,10 +31,10 @@ func (cache *redisCache) Get(ctx context.Context, key string) (value string, err response, err := cache.client.Get(ctx, key).Result() if errors.Is(err, redis.Nil) { - return "", stacktrace.Propagate(err, fmt.Sprintf("no item found in redis with key [%s]", key)) + return "", stacktrace.Propagatef(err, "no item found in redis with key [%s]", key) } if err != nil { - return "", stacktrace.Propagate(err, fmt.Sprintf("cannot get item in redis with key [%s]", key)) + return "", stacktrace.Propagatef(err, "cannot get item in redis with key [%s]", key) } return response, nil } @@ -47,7 +46,7 @@ func (cache *redisCache) Set(ctx context.Context, key string, value string, ttl err := cache.client.Set(ctx, key, value, ttl).Err() if err != nil { - return cache.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot set item in redis")) + return cache.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot set item in redis")) } return nil } diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index f84f5b301..ad9a18858 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -67,10 +67,10 @@ import ( "github.com/NdoleStudio/httpsms/pkg/listeners" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/services" + "github.com/NdoleStudio/stacktrace" swagger "github.com/gofiber/contrib/v3/swaggo" "github.com/gofiber/fiber/v3" fiberLogger "github.com/gofiber/fiber/v3/middleware/logger" - "github.com/palantir/stacktrace" ttlCache "github.com/patrickmn/go-cache" "gorm.io/gorm" @@ -268,7 +268,7 @@ func (container *Container) DedicatedDB() (db *gorm.DB) { } if err = db.Use(tracing.NewPlugin()); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot use GORM tracing plugin")) } container.dedicatedDB = db @@ -279,11 +279,11 @@ func (container *Container) DedicatedDB() (db *gorm.DB) { container.logger.Debug(fmt.Sprintf("Running migrations for dedicated [%T]", db)) if err = db.AutoMigrate(&entities.Heartbeat{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Heartbeat{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Heartbeat{})) } if err = db.AutoMigrate(&entities.HeartbeatMonitor{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.HeartbeatMonitor{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.HeartbeatMonitor{})) } return container.dedicatedDB @@ -326,7 +326,7 @@ func (container *Container) DBWithoutMigration() (db *gorm.DB) { container.db = db if err = db.Use(tracing.NewPlugin()); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot use GORM tracing plugin")) } return container.db } @@ -351,7 +351,7 @@ func (container *Container) DB() (db *gorm.DB) { container.db = db if err = db.Use(tracing.NewPlugin()); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot use GORM tracing plugin")) } if os.Getenv("DATABASE_MIGRATION_SKIP") != "" { @@ -371,47 +371,47 @@ ALTER TABLE discords ADD CONSTRAINT IF NOT EXISTS uni_discords_server_id CHECK ( } if err = db.AutoMigrate(&entities.Message{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Message{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Message{})) } if err = db.AutoMigrate(&entities.MessageThread{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.MessageThread{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.MessageThread{})) } if err = db.AutoMigrate(&entities.User{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.User{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.User{})) } if err = db.AutoMigrate(&entities.MessageSendSchedule{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.MessageSendSchedule{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.MessageSendSchedule{})) } if err = db.AutoMigrate(&entities.Phone{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Phone{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Phone{})) } if err = db.AutoMigrate(&entities.PhoneNotification{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.PhoneNotification{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.PhoneNotification{})) } if err = db.AutoMigrate(&entities.BillingUsage{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.BillingUsage{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.BillingUsage{})) } if err = db.AutoMigrate(&entities.Webhook{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Webhook{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Webhook{})) } if err = db.AutoMigrate(&entities.Discord{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Discord{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Discord{})) } if err = db.AutoMigrate(&entities.Integration3CX{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Integration3CX{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Integration3CX{})) } if err = db.AutoMigrate(&entities.PhoneAPIKey{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.PhoneAPIKey{}))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.PhoneAPIKey{})) } return container.db @@ -423,8 +423,7 @@ func (container *Container) FirebaseApp() (app *firebase.App) { app, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsJSON(container.FirebaseCredentials())) if err != nil { - msg := "cannot initialize firebase application" - container.logger.Fatal(stacktrace.Propagate(err, msg)) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize firebase application")) } return app } @@ -445,7 +444,7 @@ func (container *Container) Cache() cache.Cache { container.logger.Debug("creating cache.Cache") opt, err := redis.ParseURL(os.Getenv("REDIS_URL")) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot parse redis url [%s]", os.Getenv("REDIS_URL")))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot parse redis url [%s]", os.Getenv("REDIS_URL"))) } if strings.HasPrefix(os.Getenv("REDIS_URL"), "rediss://") { opt.TLSConfig = &tls.Config{ @@ -457,12 +456,12 @@ func (container *Container) Cache() cache.Cache { // Enable tracing instrumentation. if err = redisotel.InstrumentTracing(redisClient); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot instrument redis tracing")) + container.logger.Error(stacktrace.Propagatef(err, "cannot instrument redis tracing")) } // Enable metrics instrumentation. if err = redisotel.InstrumentMetrics(redisClient); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot instrument redis metrics")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot instrument redis metrics")) } return cache.NewRedisCache(container.Tracer(), redisClient) @@ -473,8 +472,7 @@ func (container *Container) FirebaseAuthClient() (client *auth.Client) { container.logger.Debug(fmt.Sprintf("creating %T", client)) authClient, err := container.FirebaseApp().Auth(context.Background()) if err != nil { - msg := "cannot initialize firebase auth client" - container.logger.Fatal(stacktrace.Propagate(err, msg)) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize firebase auth client")) } return authClient } @@ -485,7 +483,7 @@ func (container *Container) CloudTasksClient() (client *cloudtasks.Client) { client, err := cloudtasks.NewClient(context.Background(), option.WithCredentialsJSON(container.FirebaseCredentials())) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize cloud tasks client")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize cloud tasks client")) } return client @@ -553,8 +551,7 @@ func (container *Container) FCMClient() services.FCMClient { container.logger.Debug("creating FirebaseFCMClient") messagingClient, err := container.FirebaseApp().Messaging(context.Background()) if err != nil { - msg := "cannot initialize firebase messaging client" - container.logger.Fatal(stacktrace.Propagate(err, msg)) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize firebase messaging client")) } return services.NewFirebaseFCMClient(messagingClient) } @@ -748,7 +745,7 @@ func (container *Container) Float64Histogram(name, unit, description string) ote ) histogram, err := meter.Float64Histogram(name, otelMetric.WithUnit(unit), otelMetric.WithDescription(description)) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create float64 histogram")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create float64 histogram")) } return histogram } @@ -1111,6 +1108,7 @@ func (container *Container) MessageThreadService() (service *services.MessageThr container.Logger(), container.Tracer(), container.MessageThreadRepository(), + container.PhoneRepository(), container.EventDispatcher(), ) } @@ -1527,7 +1525,7 @@ func (container *Container) RegisterWebsocketListeners() { container.logger.Debug(fmt.Sprintf("registering listeners for %T", listeners.WebsocketListener{})) if os.Getenv("PUSHER_SECRET") == "" { - container.logger.Warn(stacktrace.NewError("skipping websocket listeners because the PUSHER_SECRET env variable is not set")) + container.logger.Warn(stacktrace.NewErrorf("skipping websocket listeners because the PUSHER_SECRET env variable is not set")) return } @@ -1581,7 +1579,7 @@ func (container *Container) AttachmentRepository() repositories.AttachmentReposi container.logger.Debug("creating GoogleCloudStorageAttachmentRepository") client, err := storage.NewClient(context.Background(), option.WithAuthCredentialsJSON(option.ServiceAccount, container.FirebaseCredentials())) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create GCS client")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create GCS client")) } container.attachmentRepository = repositories.NewGoogleCloudStorageAttachmentRepository( container.Logger(), @@ -1769,7 +1767,7 @@ func (container *Container) PhoneRistrettoCache() *ristretto.Cache[string, *enti BufferItems: 64, }) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create phone ristretto cache")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create phone ristretto cache")) } container.phoneRistrettoCache = ristrettoCache return container.phoneRistrettoCache @@ -1787,7 +1785,7 @@ func (container *Container) UserRistrettoCache() *ristretto.Cache[string, entiti BufferItems: 64, }) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create user ristretto cache")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create user ristretto cache")) } container.userRistrettoCache = ristrettoCache return ristrettoCache @@ -1803,7 +1801,7 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp traceExporter, err := cloudtrace.New(cloudtrace.WithProjectID(os.Getenv("GCP_PROJECT_ID"))) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create cloud trace traceExporter")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create cloud trace traceExporter")) } tp := trace.NewTracerProvider( @@ -1815,7 +1813,7 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp metricExporter, err := mexporter.New(mexporter.WithProjectID(os.Getenv("GCP_PROJECT_ID"))) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create cloud metric traceExporter")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create cloud metric traceExporter")) } meterProvider := metric.NewMeterProvider( @@ -1826,10 +1824,10 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp return func() { if err = metricExporter.Shutdown(context.Background()); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot shutdown cloud metric metric exporter")) + container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown cloud metric metric exporter")) } if err = traceExporter.Shutdown(context.Background()); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot shutdown cloud trace trace exporter")) + container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown cloud trace trace exporter")) } } } @@ -1848,7 +1846,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa otlptracehttp.WithHeaders(traceHeaders), ) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP trace exporter")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create axiom OTLP trace exporter")) } tp := trace.NewTracerProvider( @@ -1874,7 +1872,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa otlpmetrichttp.WithHeaders(metricHeaders), ) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP metric exporter")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create axiom OTLP metric exporter")) } meterProvider := metric.NewMeterProvider( @@ -1885,10 +1883,10 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa return func() { if err := tp.Shutdown(context.Background()); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom trace provider")) + container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown axiom trace provider")) } if err := meterProvider.Shutdown(context.Background()); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom meter provider")) + container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown axiom meter provider")) } } } @@ -1939,7 +1937,7 @@ func axiomLogger(skipFrameCount int) *zerodriver.Logger { axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET_EVENTS")), ) if err != nil { - log.Fatal(stacktrace.Propagate(err, "cannot create axiom zerolog writer")) + log.Fatal(stacktrace.Propagatef(err, "cannot create axiom zerolog writer")) } zl := zerolog.New(axiomWriter).With().Timestamp().CallerWithSkipFrameCount(skipFrameCount).Logger() diff --git a/api/pkg/emails/event_payload_formatter.go b/api/pkg/emails/event_payload_formatter.go new file mode 100644 index 000000000..0a8638540 --- /dev/null +++ b/api/pkg/emails/event_payload_formatter.go @@ -0,0 +1,128 @@ +package emails + +import ( + "bytes" + "encoding/json" + "html" + "html/template" + "strings" +) + +const ( + eventPayloadCodeBlockStyle = "margin:0;padding:12px;border:1px solid #D0D7DE;border-radius:6px;background:#F6F8FA;color:#24292F;font-family:Consolas,Monaco,'Courier New',monospace;font-size:13px;line-height:1.5;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;" + jsonKeyStyle = "color:#0550AE;font-weight:600;" + jsonStringStyle = "color:#0A3069;" + jsonNumberStyle = "color:#953800;" + jsonLiteralStyle = "color:#8250DF;" +) + +func formatEventPayload(payload string) (string, template.HTML) { + formattedPayload, isJSON := indentEventPayloadJSON(payload) + content := html.EscapeString(formattedPayload) + if isJSON { + content = highlightEventPayloadJSON(formattedPayload) + } + + // #nosec G203 -- every dynamic payload token is escaped before the static wrapper is added. + richPayload := template.HTML(`
` + content + `
`) + return formattedPayload, richPayload +} + +func indentEventPayloadJSON(payload string) (string, bool) { + var formatted bytes.Buffer + if err := json.Indent(&formatted, []byte(payload), "", " "); err != nil { + return payload, false + } + + return formatted.String(), true +} + +func highlightEventPayloadJSON(payload string) string { + var highlighted strings.Builder + highlighted.Grow(len(payload)) + + for index := 0; index < len(payload); { + switch { + case payload[index] == '"': + end := eventPayloadJSONStringEnd(payload, index) + style := jsonStringStyle + if eventPayloadNextNonSpace(payload, end) == ':' { + style = jsonKeyStyle + } + writeEventPayloadToken(&highlighted, style, payload[index:end]) + index = end + case payload[index] == '-' || isEventPayloadDigit(payload[index]): + end := index + 1 + // json.Indent already validated this as JSON, so this continuation set only sees JSON number bytes. + for end < len(payload) && isEventPayloadNumberCharacter(payload[end]) { + end++ + } + writeEventPayloadToken(&highlighted, jsonNumberStyle, payload[index:end]) + index = end + case strings.HasPrefix(payload[index:], "true"): + writeEventPayloadToken(&highlighted, jsonLiteralStyle, "true") + index += len("true") + case strings.HasPrefix(payload[index:], "false"): + writeEventPayloadToken(&highlighted, jsonLiteralStyle, "false") + index += len("false") + case strings.HasPrefix(payload[index:], "null"): + writeEventPayloadToken(&highlighted, jsonLiteralStyle, "null") + index += len("null") + default: + highlighted.WriteString(html.EscapeString(payload[index : index+1])) + index++ + } + } + + return highlighted.String() +} + +func eventPayloadJSONStringEnd(payload string, start int) int { + escaped := false + for index := start + 1; index < len(payload); index++ { + switch { + case escaped: + escaped = false + case payload[index] == '\\': + escaped = true + case payload[index] == '"': + return index + 1 + } + } + + return len(payload) +} + +func eventPayloadNextNonSpace(payload string, start int) byte { + for index := start; index < len(payload); index++ { + switch payload[index] { + case ' ', '\n', '\r', '\t': + continue + default: + return payload[index] + } + } + + return 0 +} + +func isEventPayloadDigit(value byte) bool { + return value >= '0' && value <= '9' +} + +func isEventPayloadNumberCharacter(value byte) bool { + return isEventPayloadDigit(value) || + value == '-' || + value == '+' || + value == '.' || + value == 'e' || + value == 'E' +} + +func writeEventPayloadToken(builder *strings.Builder, style string, token string) { + builder.WriteString(``) + builder.WriteString(html.EscapeString(token)) + builder.WriteString(``) +} diff --git a/api/pkg/emails/event_payload_formatter_test.go b/api/pkg/emails/event_payload_formatter_test.go new file mode 100644 index 000000000..9617ebb0e --- /dev/null +++ b/api/pkg/emails/event_payload_formatter_test.go @@ -0,0 +1,113 @@ +package emails + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormatEventPayloadIndentsAndHighlightsJSON(t *testing.T) { + payload := `{"message":"hello","count":2,"ratio":1.5,"enabled":true,"disabled":false,"missing":null,"nested":{"value":"ok"}}` + + plain, rich := formatEventPayload(payload) + html := string(rich) + + assert.Equal(t, `{ + "message": "hello", + "count": 2, + "ratio": 1.5, + "enabled": true, + "disabled": false, + "missing": null, + "nested": { + "value": "ok" + } +}`, plain) + assert.Contains(t, html, `"message"`) + assert.Contains(t, html, `"hello"`) + assert.Contains(t, html, `2`) + assert.Contains(t, html, `1.5`) + assert.Contains(t, html, `true`) + assert.Contains(t, html, `false`) + assert.Contains(t, html, `null`) + assert.Contains(t, html, `white-space:pre-wrap`) +} + +func TestFormatEventPayloadEscapesPayloadHTML(t *testing.T) { + plain, rich := formatEventPayload(`{"message":"&"}`) + html := string(rich) + + assert.Contains(t, plain, `&"}`) + html := string(rich) + + assert.Contains(t, plain, `