diff --git a/.github/workflows/assemble.yml b/.github/workflows/assemble.yml index 5dc3d99..ed0cc2a 100644 --- a/.github/workflows/assemble.yml +++ b/.github/workflows/assemble.yml @@ -24,4 +24,6 @@ jobs: - uses: gradle/gradle-build-action@v2 - name: Assemble debug variant - run: ./gradlew assembleDebug --no-daemon + run: | + ./gradlew assembleDebug --no-daemon + ./gradlew assembleDebug -Pfoss --no-daemon diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..8162c56 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,31 @@ +name: Nightly + +on: + schedule: + - cron: '0 19 * * *' + +jobs: + check-develop-beta-changes: + name: Check if develop is ahead of beta release + runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + node-version: + 16 + - run: npm ci + working-directory: ./tools/beta-release/ + + - uses: actions/github-script@v6 + with: + script: | + const { startReleaseProcess } = await import('${{ github.workspace }}/tools/beta-release/app.js') + await startReleaseProcess({github, context, core}) + diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml new file mode 100644 index 0000000..95605d4 --- /dev/null +++ b/.github/workflows/release-candidate.yml @@ -0,0 +1,50 @@ +name: Generate and publish Release Candidate + +on: + push: + branches: + - 'release' + +jobs: + publish-release-candidate: + name: Publish release candidate + runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '11' + - uses: actions/setup-node@v3 + with: + node-version: + 16 + - run: npm ci + working-directory: ./tools/beta-release/ + + - name: Write secrets + run: | + mkdir .secrets + touch .secrets/upload-key.jks + touch .secrets/service-account.json + echo -n '${{ secrets.UPLOAD_KEY }}' | base64 --decode >> .secrets/upload-key.jks + echo -n '${{ secrets.SERVICE_ACCOUNT }}' | base64 --decode >> .secrets/service-account.json + + - name: Assemble release variant + run: ./tools/generate-release.sh ${{ secrets.STORE_PASS }} + + - uses: actions/github-script@v6 + with: + script: | + const { publishRelease } = await import('${{ github.workspace }}/tools/beta-release/app.js') + const artifacts = { + bundle: '${{ github.workspace }}/app/build/outputs/bundle/release/app-release.aab', + mapping: '${{ github.workspace }}/app/build/outputs/mapping/release/mapping.txt', + } + await publishRelease(github, artifacts) + diff --git a/.gitignore b/.gitignore index 3b76762..b55bcc1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ .externalNativeBuild .cxx local.properties -/benchmark-out \ No newline at end of file +/benchmark-out +**/node_modules +.secrets \ No newline at end of file diff --git a/README.md b/README.md index b94975e..cb587b2 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,6 @@ - Focused on reliability and stability. - Bare-bones feature set. -##### _*Google play only with automatic crash reporting enabled_ - --- ### Feature list @@ -28,7 +26,8 @@ - Message bubbles, supporting text, replies and edits - Push notifications (DMs always notify, Rooms notify once) - Importing of E2E room keys from Element clients -- [UnifiedPush](https://unifiedpush.org/) +- [UnifiedPush](https://unifiedpush.org/) +- FOSS variant ### Planned @@ -57,4 +56,34 @@ --- + +### Building + + +##### Debug `.apk` + +```bash +./gradlew assembleDebug +``` + +##### Release (signed with debug key) `.apk` + +```bash +./gradlew assembleRelease +``` + +##### Unsigned release `.apk` + +```bash +./gradlew assembleRelease -Punsigned +``` + +##### Unsigned release (FOSS) `.apk` + +```bash +./gradlew assembleRelease -Punsigned -Pfoss +``` + +--- + #### Join the conversation @ https://matrix.to/#/#small-talk:iswell.cool diff --git a/app/build.gradle b/app/build.gradle index 5ed4c0c..0c81f66 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,6 +24,10 @@ android { } else { resConfigs "en" } + + if (isFoss()) { + archivesBaseName = "$archivesBaseName-foss" + } } bundle { @@ -46,8 +50,11 @@ android { "proguard/serializationx.pro", "proguard/olm.pro" - // actual releases are signed with a different config - signingConfig = buildTypes.debug.signingConfig + if (project.hasProperty("unsigned")) { + // releases are signed externally + } else { + signingConfig = buildTypes.debug.signingConfig + } } } @@ -82,6 +89,8 @@ dependencies { implementation project(":domains:android:imageloader") implementation project(":domains:olm") + firebase(it, "messaging") + implementation project(":matrix:matrix") implementation project(":matrix:matrix-http-ktor") implementation project(":matrix:services:auth") diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt index 0590253..1d15604 100644 --- a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt +++ b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt @@ -1,7 +1,6 @@ package app.dapk.st import android.app.Application -import android.content.Intent import android.util.Log import app.dapk.st.core.CoreAndroidModule import app.dapk.st.core.ModuleProvider @@ -11,13 +10,13 @@ import app.dapk.st.core.extensions.ResettableUnsafeLazy import app.dapk.st.core.extensions.Scope import app.dapk.st.directory.DirectoryModule import app.dapk.st.domain.StoreModule +import app.dapk.st.firebase.messaging.MessagingModule import app.dapk.st.graph.AppModule import app.dapk.st.home.HomeModule import app.dapk.st.login.LoginModule import app.dapk.st.messenger.MessengerModule import app.dapk.st.notifications.NotificationsModule import app.dapk.st.profile.ProfileModule -import app.dapk.st.push.firebase.FirebasePushService import app.dapk.st.push.PushModule import app.dapk.st.settings.SettingsModule import app.dapk.st.share.ShareEntryModule @@ -75,6 +74,7 @@ class SmallTalkApplication : Application(), ModuleProvider { ProfileModule::class -> featureModules.profileModule NotificationsModule::class -> featureModules.notificationsModule PushModule::class -> featureModules.pushModule + MessagingModule::class -> featureModules.messagingModule MessengerModule::class -> featureModules.messengerModule TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule CoreAndroidModule::class -> appModule.coreAndroidModule diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index 24e0232..8f20819 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -16,6 +16,7 @@ import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.directory.DirectoryModule import app.dapk.st.domain.StoreModule +import app.dapk.st.firebase.messaging.MessagingModule import app.dapk.st.home.HomeModule import app.dapk.st.home.MainActivity import app.dapk.st.imageloader.ImageLoaderModule @@ -55,6 +56,7 @@ import app.dapk.st.olm.OlmPersistenceWrapper import app.dapk.st.olm.OlmWrapper import app.dapk.st.profile.ProfileModule import app.dapk.st.push.PushModule +import app.dapk.st.push.messaging.MessagingServiceAdapter import app.dapk.st.settings.SettingsModule import app.dapk.st.share.ShareEntryModule import app.dapk.st.tracking.TrackingModule @@ -208,6 +210,10 @@ internal class FeatureModules internal constructor( domainModules.pushModule } + val messagingModule by unsafeLazy { + domainModules.messaging + } + } internal class MatrixModules( @@ -428,23 +434,30 @@ internal class DomainModules( private val dispatchers: CoroutineDispatchers, ) { - val pushModule by unsafeLazy { + val pushHandler by unsafeLazy { val store = storeModule.value - val pushHandler = MatrixPushHandler( + MatrixPushHandler( workScheduler = workModule.workScheduler(), credentialsStore = store.credentialsStore(), matrixModules.sync, store.roomStore(), ) + } + + val messaging by unsafeLazy { MessagingModule(MessagingServiceAdapter(pushHandler), context) } + + val pushModule by unsafeLazy { PushModule( errorTracker, pushHandler, context, dispatchers, - SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", dispatchers) + SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", dispatchers), + messaging.messaging, ) } val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) } + } internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader { diff --git a/build.gradle b/build.gradle index c28a7fb..004ac18 100644 --- a/build.gradle +++ b/build.gradle @@ -118,7 +118,7 @@ ext.applyAndroidLibraryModule = { project -> } ext.applyCrashlyticsIfRelease = { project -> - if (isReleaseBuild) { + if (isReleaseBuild && !isFoss()) { project.apply plugin: 'com.google.firebase.crashlytics' project.afterEvaluate { project.tasks.withType(com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask).configureEach { @@ -151,6 +151,19 @@ ext.androidImportFixturesWorkaround = { project, fixtures -> project.dependencies.testImplementation fixtures.files("build/libs/${fixtures.name}.jar") } +ext.isFoss = { + return rootProject.hasProperty("foss") +} + +ext.firebase = { dependencies, name -> + if (isFoss()) { + dependencies.implementation(project(":domains:firebase:$name-noop")) + } else { + dependencies.implementation(project(":domains:firebase:$name")) + } +} + + if (launchTask.contains("codeCoverageReport".toLowerCase())) { apply from: 'tools/coverage.gradle' -} \ No newline at end of file +} diff --git a/domains/android/push/build.gradle b/domains/android/push/build.gradle index dbcfe03..deafddc 100644 --- a/domains/android/push/build.gradle +++ b/domains/android/push/build.gradle @@ -6,8 +6,9 @@ dependencies { implementation project(':domains:android:core') implementation project(':domains:store') implementation project(':matrix:services:push') - implementation platform('com.google.firebase:firebase-bom:29.0.3') - implementation 'com.google.firebase:firebase-messaging' + + firebase(it, "messaging") + implementation Dependencies.mavenCentral.kotlinSerializationJson implementation Dependencies.jitPack.unifiedPush } diff --git a/domains/android/push/src/main/AndroidManifest.xml b/domains/android/push/src/main/AndroidManifest.xml index 61024b7..c308606 100644 --- a/domains/android/push/src/main/AndroidManifest.xml +++ b/domains/android/push/src/main/AndroidManifest.xml @@ -2,15 +2,6 @@ - - - - - - - diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt index 5b923c7..dc8c77e 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt @@ -7,7 +7,8 @@ import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.domain.Preferences import app.dapk.st.domain.push.PushTokenRegistrarPreferences -import app.dapk.st.push.firebase.FirebasePushTokenRegistrar +import app.dapk.st.firebase.messaging.Messaging +import app.dapk.st.push.messaging.MessagingPushTokenRegistrar import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar class PushModule( @@ -16,15 +17,16 @@ class PushModule( private val context: Context, private val dispatchers: CoroutineDispatchers, private val preferences: Preferences, + private val messaging: Messaging, ) : ProvidableModule { private val registrars by unsafeLazy { PushTokenRegistrars( context, - FirebasePushTokenRegistrar( + MessagingPushTokenRegistrar( errorTracker, - context, pushHandler, + messaging, ), UnifiedPushRegistrar(context), PushTokenRegistrarPreferences(preferences) diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt index 7ce0658..44a9679 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt @@ -2,7 +2,7 @@ package app.dapk.st.push import android.content.Context import app.dapk.st.domain.push.PushTokenRegistrarPreferences -import app.dapk.st.push.firebase.FirebasePushTokenRegistrar +import app.dapk.st.push.messaging.MessagingPushTokenRegistrar import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar import org.unifiedpush.android.connector.UnifiedPush @@ -11,7 +11,7 @@ private val NONE = Registrar("None") class PushTokenRegistrars( private val context: Context, - private val firebasePushTokenRegistrar: FirebasePushTokenRegistrar, + private val messagingPushTokenRegistrar: MessagingPushTokenRegistrar, private val unifiedPushRegistrar: UnifiedPushRegistrar, private val pushTokenStore: PushTokenRegistrarPreferences, ) : PushTokenRegistrar { @@ -19,27 +19,36 @@ class PushTokenRegistrars( private var selection: Registrar? = null fun options(): List { - return listOf(NONE, FIREBASE_OPTION) + UnifiedPush.getDistributors(context).map { Registrar(it) } + val messagingOption = when (messagingPushTokenRegistrar.isAvailable()) { + true -> FIREBASE_OPTION + else -> null + } + return listOfNotNull(NONE, messagingOption) + UnifiedPush.getDistributors(context).map { Registrar(it) } } - suspend fun currentSelection() = selection ?: (pushTokenStore.currentSelection()?.let { Registrar(it) } ?: FIREBASE_OPTION).also { selection = it } + suspend fun currentSelection() = selection ?: (pushTokenStore.currentSelection()?.let { Registrar(it) } ?: defaultSelection()).also { selection = it } + + private fun defaultSelection() = when (messagingPushTokenRegistrar.isAvailable()) { + true -> FIREBASE_OPTION + else -> NONE + } suspend fun makeSelection(option: Registrar) { selection = option pushTokenStore.store(option.id) when (option) { NONE -> { - firebasePushTokenRegistrar.unregister() + messagingPushTokenRegistrar.unregister() unifiedPushRegistrar.unregister() } FIREBASE_OPTION -> { unifiedPushRegistrar.unregister() - firebasePushTokenRegistrar.registerCurrentToken() + messagingPushTokenRegistrar.registerCurrentToken() } else -> { - firebasePushTokenRegistrar.unregister() + messagingPushTokenRegistrar.unregister() unifiedPushRegistrar.registerSelection(option) } } @@ -47,7 +56,7 @@ class PushTokenRegistrars( override suspend fun registerCurrentToken() { when (selection) { - FIREBASE_OPTION -> firebasePushTokenRegistrar.registerCurrentToken() + FIREBASE_OPTION -> messagingPushTokenRegistrar.registerCurrentToken() NONE -> { // do nothing } @@ -58,10 +67,10 @@ class PushTokenRegistrars( override fun unregister() { when (selection) { - FIREBASE_OPTION -> firebasePushTokenRegistrar.unregister() + FIREBASE_OPTION -> messagingPushTokenRegistrar.unregister() NONE -> { runCatching { - firebasePushTokenRegistrar.unregister() + messagingPushTokenRegistrar.unregister() unifiedPushRegistrar.unregister() } } diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebaseMessagingExtensions.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebaseMessagingExtensions.kt deleted file mode 100644 index 9ff0ac7..0000000 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebaseMessagingExtensions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.dapk.st.push.firebase - -import com.google.firebase.messaging.FirebaseMessaging -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -suspend fun FirebaseMessaging.token() = suspendCoroutine { continuation -> - this.token.addOnCompleteListener { task -> - when { - task.isSuccessful -> continuation.resume(task.result!!) - task.isCanceled -> continuation.resumeWith(Result.failure(CancelledTokenFetchingException())) - else -> continuation.resumeWith(Result.failure(task.exception ?: UnknownTokenFetchingFailedException())) - } - } -} - -private class CancelledTokenFetchingException : Throwable() -private class UnknownTokenFetchingFailedException : Throwable() \ No newline at end of file diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushTokenRegistrar.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/messaging/MessagingPushTokenRegistrar.kt similarity index 51% rename from domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushTokenRegistrar.kt rename to domains/android/push/src/main/kotlin/app/dapk/st/push/messaging/MessagingPushTokenRegistrar.kt index 2c9321a..760c313 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushTokenRegistrar.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/messaging/MessagingPushTokenRegistrar.kt @@ -1,37 +1,28 @@ -package app.dapk.st.push.firebase +package app.dapk.st.push.messaging -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager import app.dapk.st.core.AppLogTag import app.dapk.st.core.extensions.CrashScope import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.log +import app.dapk.st.firebase.messaging.Messaging import app.dapk.st.push.PushHandler import app.dapk.st.push.PushTokenPayload import app.dapk.st.push.PushTokenRegistrar -import app.dapk.st.push.unifiedpush.UnifiedPushMessageReceiver -import com.google.firebase.messaging.FirebaseMessaging private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify" -class FirebasePushTokenRegistrar( +class MessagingPushTokenRegistrar( override val errorTracker: ErrorTracker, - private val context: Context, private val pushHandler: PushHandler, + private val messaging: Messaging, ) : PushTokenRegistrar, CrashScope { override suspend fun registerCurrentToken() { log(AppLogTag.PUSH, "FCM - register current token") - context.packageManager.setComponentEnabledSetting( - ComponentName(context, FirebasePushService::class.java), - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP, - ) + messaging.enable() kotlin.runCatching { - FirebaseMessaging.getInstance().token().also { + messaging.token().also { pushHandler.onNewToken( PushTokenPayload( token = it, @@ -48,14 +39,10 @@ class FirebasePushTokenRegistrar( override fun unregister() { log(AppLogTag.PUSH, "FCM - unregister") - FirebaseMessaging.getInstance().deleteToken() - context.stopService(Intent(context, FirebasePushService::class.java)) - - context.packageManager.setComponentEnabledSetting( - ComponentName(context, FirebasePushService::class.java), - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP, - ) + messaging.deleteToken() + messaging.disable() } + fun isAvailable() = messaging.isAvailable() + } \ No newline at end of file diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/messaging/MessagingServiceAdapter.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/messaging/MessagingServiceAdapter.kt new file mode 100644 index 0000000..cceed3d --- /dev/null +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/messaging/MessagingServiceAdapter.kt @@ -0,0 +1,31 @@ +package app.dapk.st.push.messaging + +import app.dapk.st.core.AppLogTag +import app.dapk.st.core.log +import app.dapk.st.firebase.messaging.ServiceDelegate +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.push.PushHandler +import app.dapk.st.push.PushTokenPayload + +private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify" + +class MessagingServiceAdapter( + private val handler: PushHandler, +) : ServiceDelegate { + + override fun onNewToken(token: String) { + log(AppLogTag.PUSH, "FCM onNewToken") + handler.onNewToken( + PushTokenPayload( + token = token, + gatewayUrl = SYGNAL_GATEWAY, + ) + ) + } + + override fun onMessageReceived(eventId: EventId?, roomId: RoomId?) { + log(AppLogTag.PUSH, "FCM onMessage") + handler.onMessageReceived(eventId, roomId) + } +} diff --git a/domains/android/tracking/build.gradle b/domains/android/tracking/build.gradle index 8545a2f..25e3ca2 100644 --- a/domains/android/tracking/build.gradle +++ b/domains/android/tracking/build.gradle @@ -2,9 +2,5 @@ applyAndroidLibraryModule(project) dependencies { implementation project(':core') - implementation platform('com.google.firebase:firebase-bom:29.0.3') - implementation 'com.google.firebase:firebase-crashlytics' - - // is it worth the 400kb size increase? -// implementation 'com.google.firebase:firebase-analytics' + firebase(it, "crashlytics") } diff --git a/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashlyticsCrashTracker.kt b/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashTrackerLogger.kt similarity index 62% rename from domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashlyticsCrashTracker.kt rename to domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashTrackerLogger.kt index 158884a..cd2a99d 100644 --- a/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashlyticsCrashTracker.kt +++ b/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/CrashTrackerLogger.kt @@ -4,16 +4,12 @@ import android.util.Log import app.dapk.st.core.AppLogTag import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.log -import com.google.firebase.crashlytics.FirebaseCrashlytics -class CrashlyticsCrashTracker( - private val firebaseCrashlytics: FirebaseCrashlytics, -) : ErrorTracker { +class CrashTrackerLogger : ErrorTracker { override fun track(throwable: Throwable, extra: String) { Log.e("ST", throwable.message, throwable) log(AppLogTag.ERROR_NON_FATAL, "${throwable.message ?: "N/A"} extra=$extra") - firebaseCrashlytics.recordException(throwable) } } diff --git a/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/TrackingModule.kt b/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/TrackingModule.kt index e097607..42642f4 100644 --- a/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/TrackingModule.kt +++ b/domains/android/tracking/src/main/kotlin/app/dapk/st/tracking/TrackingModule.kt @@ -1,9 +1,8 @@ package app.dapk.st.tracking -import android.util.Log import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.unsafeLazy -import com.google.firebase.crashlytics.FirebaseCrashlytics +import app.dapk.st.firebase.crashlytics.CrashlyticsModule class TrackingModule( private val isCrashTrackingEnabled: Boolean, @@ -11,13 +10,18 @@ class TrackingModule( val errorTracker: ErrorTracker by unsafeLazy { when (isCrashTrackingEnabled) { - true -> CrashlyticsCrashTracker(FirebaseCrashlytics.getInstance()) - false -> object : ErrorTracker { - override fun track(throwable: Throwable, extra: String) { - Log.e("error", throwable.message, throwable) - } - } + true -> compositeTracker( + CrashTrackerLogger(), + CrashlyticsModule().errorTracker, + ) + false -> CrashTrackerLogger() } } +} + +private fun compositeTracker(vararg loggers: ErrorTracker) = object : ErrorTracker { + override fun track(throwable: Throwable, extra: String) { + loggers.forEach { it.track(throwable, extra) } + } } \ No newline at end of file diff --git a/domains/firebase/crashlytics-noop/build.gradle b/domains/firebase/crashlytics-noop/build.gradle new file mode 100644 index 0000000..5ab1042 --- /dev/null +++ b/domains/firebase/crashlytics-noop/build.gradle @@ -0,0 +1,5 @@ +plugins { id 'kotlin' } + +dependencies { + implementation project(':core') +} diff --git a/domains/firebase/crashlytics-noop/src/main/kotlin/app/dapk/st/firebase/crashlytics/CrashlyticsModule.kt b/domains/firebase/crashlytics-noop/src/main/kotlin/app/dapk/st/firebase/crashlytics/CrashlyticsModule.kt new file mode 100644 index 0000000..0759d8e --- /dev/null +++ b/domains/firebase/crashlytics-noop/src/main/kotlin/app/dapk/st/firebase/crashlytics/CrashlyticsModule.kt @@ -0,0 +1,16 @@ +package app.dapk.st.firebase.crashlytics + +import app.dapk.st.core.extensions.ErrorTracker +import app.dapk.st.core.extensions.unsafeLazy + +class CrashlyticsModule { + + val errorTracker: ErrorTracker by unsafeLazy { + object : ErrorTracker { + override fun track(throwable: Throwable, extra: String) { + // no op + } + } + } + +} \ No newline at end of file diff --git a/domains/firebase/crashlytics/build.gradle b/domains/firebase/crashlytics/build.gradle new file mode 100644 index 0000000..f502422 --- /dev/null +++ b/domains/firebase/crashlytics/build.gradle @@ -0,0 +1,7 @@ +applyAndroidLibraryModule(project) + +dependencies { + implementation project(':core') + implementation platform('com.google.firebase:firebase-bom:29.0.3') + implementation 'com.google.firebase:firebase-crashlytics' +} diff --git a/domains/firebase/crashlytics/src/main/AndroidManifest.xml b/domains/firebase/crashlytics/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1820009 --- /dev/null +++ b/domains/firebase/crashlytics/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/domains/firebase/crashlytics/src/main/kotlin/app/dapk/st/firebase/crashlytics/CrashlyticsCrashTracker.kt b/domains/firebase/crashlytics/src/main/kotlin/app/dapk/st/firebase/crashlytics/CrashlyticsCrashTracker.kt new file mode 100644 index 0000000..3c634a2 --- /dev/null +++ b/domains/firebase/crashlytics/src/main/kotlin/app/dapk/st/firebase/crashlytics/CrashlyticsCrashTracker.kt @@ -0,0 +1,14 @@ +package app.dapk.st.firebase.crashlytics + +import app.dapk.st.core.extensions.ErrorTracker +import com.google.firebase.crashlytics.FirebaseCrashlytics + +class CrashlyticsCrashTracker( + private val firebaseCrashlytics: FirebaseCrashlytics, +) : ErrorTracker { + + override fun track(throwable: Throwable, extra: String) { + firebaseCrashlytics.recordException(throwable) + } +} + diff --git a/domains/firebase/crashlytics/src/main/kotlin/app/dapk/st/firebase/crashlytics/CrashlyticsModule.kt b/domains/firebase/crashlytics/src/main/kotlin/app/dapk/st/firebase/crashlytics/CrashlyticsModule.kt new file mode 100644 index 0000000..73db9b0 --- /dev/null +++ b/domains/firebase/crashlytics/src/main/kotlin/app/dapk/st/firebase/crashlytics/CrashlyticsModule.kt @@ -0,0 +1,13 @@ +package app.dapk.st.firebase.crashlytics + +import app.dapk.st.core.extensions.ErrorTracker +import app.dapk.st.core.extensions.unsafeLazy +import com.google.firebase.crashlytics.FirebaseCrashlytics + +class CrashlyticsModule { + + val errorTracker: ErrorTracker by unsafeLazy { + CrashlyticsCrashTracker(FirebaseCrashlytics.getInstance()) + } + +} \ No newline at end of file diff --git a/domains/firebase/messaging-noop/build.gradle b/domains/firebase/messaging-noop/build.gradle new file mode 100644 index 0000000..10bf518 --- /dev/null +++ b/domains/firebase/messaging-noop/build.gradle @@ -0,0 +1,6 @@ +applyAndroidLibraryModule(project) + +dependencies { + implementation project(':core') + implementation project(':matrix:common') +} diff --git a/domains/firebase/messaging-noop/src/main/AndroidManifest.xml b/domains/firebase/messaging-noop/src/main/AndroidManifest.xml new file mode 100644 index 0000000..15c2b55 --- /dev/null +++ b/domains/firebase/messaging-noop/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/domains/firebase/messaging-noop/src/main/kotlin/app/dapk/st/firebase/messaging/Messaging.kt b/domains/firebase/messaging-noop/src/main/kotlin/app/dapk/st/firebase/messaging/Messaging.kt new file mode 100644 index 0000000..aea7843 --- /dev/null +++ b/domains/firebase/messaging-noop/src/main/kotlin/app/dapk/st/firebase/messaging/Messaging.kt @@ -0,0 +1,23 @@ +package app.dapk.st.firebase.messaging + +class Messaging { + + fun isAvailable() = false + + fun enable() { + // do nothing + } + + fun disable() { + // do nothing + } + + fun deleteToken() { + // do nothing + } + + suspend fun token(): String { + return "" + } + +} \ No newline at end of file diff --git a/domains/firebase/messaging-noop/src/main/kotlin/app/dapk/st/firebase/messaging/MessagingModule.kt b/domains/firebase/messaging-noop/src/main/kotlin/app/dapk/st/firebase/messaging/MessagingModule.kt new file mode 100644 index 0000000..c025ca5 --- /dev/null +++ b/domains/firebase/messaging-noop/src/main/kotlin/app/dapk/st/firebase/messaging/MessagingModule.kt @@ -0,0 +1,17 @@ +package app.dapk.st.firebase.messaging + +import android.content.Context +import app.dapk.st.core.ProvidableModule +import app.dapk.st.core.extensions.unsafeLazy + +@Suppress("UNUSED") +class MessagingModule( + val serviceDelegate: ServiceDelegate, + val context: Context, +) : ProvidableModule { + + val messaging by unsafeLazy { + Messaging() + } + +} diff --git a/domains/firebase/messaging-noop/src/main/kotlin/app/dapk/st/firebase/messaging/ServiceDelegate.kt b/domains/firebase/messaging-noop/src/main/kotlin/app/dapk/st/firebase/messaging/ServiceDelegate.kt new file mode 100644 index 0000000..bbc6980 --- /dev/null +++ b/domains/firebase/messaging-noop/src/main/kotlin/app/dapk/st/firebase/messaging/ServiceDelegate.kt @@ -0,0 +1,9 @@ +package app.dapk.st.firebase.messaging + +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.RoomId + +interface ServiceDelegate { + fun onNewToken(token: String) + fun onMessageReceived(eventId: EventId?, roomId: RoomId?) +} \ No newline at end of file diff --git a/domains/firebase/messaging/build.gradle b/domains/firebase/messaging/build.gradle new file mode 100644 index 0000000..6622273 --- /dev/null +++ b/domains/firebase/messaging/build.gradle @@ -0,0 +1,9 @@ +applyAndroidLibraryModule(project) + +dependencies { + implementation project(':core') + implementation project(':domains:android:core') + implementation project(':matrix:common') + implementation platform('com.google.firebase:firebase-bom:29.0.3') + implementation 'com.google.firebase:firebase-messaging' +} diff --git a/domains/firebase/messaging/src/main/AndroidManifest.xml b/domains/firebase/messaging/src/main/AndroidManifest.xml new file mode 100644 index 0000000..02ee9b7 --- /dev/null +++ b/domains/firebase/messaging/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushService.kt b/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/FirebasePushServiceDelegate.kt similarity index 52% rename from domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushService.kt rename to domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/FirebasePushServiceDelegate.kt index e1d79db..6d99b03 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushService.kt +++ b/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/FirebasePushServiceDelegate.kt @@ -1,4 +1,4 @@ -package app.dapk.st.push.firebase +package app.dapk.st.firebase.messaging import app.dapk.st.core.AppLogTag import app.dapk.st.core.extensions.unsafeLazy @@ -6,31 +6,21 @@ import app.dapk.st.core.log import app.dapk.st.core.module import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId -import app.dapk.st.push.PushModule -import app.dapk.st.push.PushTokenPayload import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage -private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify" +class FirebasePushServiceDelegate : FirebaseMessagingService() { -class FirebasePushService : FirebaseMessagingService() { - - private val handler by unsafeLazy { module().pushHandler() } + private val delegate by unsafeLazy { module().serviceDelegate } override fun onNewToken(token: String) { - log(AppLogTag.PUSH, "FCM onNewToken") - handler.onNewToken( - PushTokenPayload( - token = token, - gatewayUrl = SYGNAL_GATEWAY, - ) - ) + delegate.onNewToken(token) } override fun onMessageReceived(message: RemoteMessage) { log(AppLogTag.PUSH, "FCM onMessage") val eventId = message.data["event_id"]?.let { EventId(it) } val roomId = message.data["room_id"]?.let { RoomId(it) } - handler.onMessageReceived(eventId, roomId) + delegate.onMessageReceived(eventId, roomId) } } diff --git a/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/Messaging.kt b/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/Messaging.kt new file mode 100644 index 0000000..5852c4d --- /dev/null +++ b/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/Messaging.kt @@ -0,0 +1,54 @@ +package app.dapk.st.firebase.messaging + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailabilityLight +import com.google.firebase.messaging.FirebaseMessaging +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class Messaging( + private val instance: FirebaseMessaging, + private val context: Context, +) { + + fun isAvailable() = GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS + + fun enable() { + context.packageManager.setComponentEnabledSetting( + ComponentName(context, FirebasePushServiceDelegate::class.java), + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP, + ) + } + + fun disable() { + context.stopService(Intent(context, FirebasePushServiceDelegate::class.java)) + context.packageManager.setComponentEnabledSetting( + ComponentName(context, FirebasePushServiceDelegate::class.java), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP, + ) + + } + + fun deleteToken() { + instance.deleteToken() + } + + suspend fun token() = suspendCoroutine { continuation -> + instance.token.addOnCompleteListener { task -> + when { + task.isSuccessful -> continuation.resume(task.result!!) + task.isCanceled -> continuation.resumeWith(Result.failure(CancelledTokenFetchingException())) + else -> continuation.resumeWith(Result.failure(task.exception ?: UnknownTokenFetchingFailedException())) + } + } + } + + private class CancelledTokenFetchingException : Throwable() + private class UnknownTokenFetchingFailedException : Throwable() +} \ No newline at end of file diff --git a/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/MessagingModule.kt b/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/MessagingModule.kt new file mode 100644 index 0000000..e0bb5ef --- /dev/null +++ b/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/MessagingModule.kt @@ -0,0 +1,17 @@ +package app.dapk.st.firebase.messaging + +import android.content.Context +import app.dapk.st.core.ProvidableModule +import app.dapk.st.core.extensions.unsafeLazy +import com.google.firebase.messaging.FirebaseMessaging + +class MessagingModule( + val serviceDelegate: ServiceDelegate, + val context: Context, +) : ProvidableModule { + + val messaging by unsafeLazy { + Messaging(FirebaseMessaging.getInstance(), context) + } + +} diff --git a/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/ServiceDelegate.kt b/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/ServiceDelegate.kt new file mode 100644 index 0000000..bbc6980 --- /dev/null +++ b/domains/firebase/messaging/src/main/kotlin/app/dapk/st/firebase/messaging/ServiceDelegate.kt @@ -0,0 +1,9 @@ +package app.dapk.st.firebase.messaging + +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.RoomId + +interface ServiceDelegate { + fun onNewToken(token: String) + fun onMessageReceived(eventId: EventId?, roomId: RoomId?) +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index d457b3a..4b8bbf4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,6 +34,11 @@ include ':domains:store' include ':domains:olm-stub' include ':domains:olm' +include ':domains:firebase:crashlytics' +include ':domains:firebase:crashlytics-noop' +include ':domains:firebase:messaging' +include ':domains:firebase:messaging-noop' + include ':matrix:matrix' include ':matrix:common' include ':matrix:matrix-http' diff --git a/tools/beta-release/app.js b/tools/beta-release/app.js new file mode 100644 index 0000000..bfbbd4d --- /dev/null +++ b/tools/beta-release/app.js @@ -0,0 +1,152 @@ +import { release } from './release.js' + +const config = { + owner: "ouchadam", + repo: "small-talk", + pathToVersionFile: "version.json", + rcBranchesFrom: "main", + rcMergesTo: "release", + packageName: "app.dapk.st" +} + +const rcBranchName = "release-candidate" + +export const startReleaseProcess = async ({ github, context, core }) => { + console.log("script start") + if (await doesNotHaveInProgressRelease(github) && await isWorkingBranchAhead(github)) { + await startRelease(github) + } else { + console.log(`Release skipped due to being behind`) + } + return "" +} + +export const publishRelease = async (github, artifacts) => { + const versionFile = await readVersionFile(github, "release") + await release( + github, + versionFile.content, + config.packageName, + artifacts, + config, + ).catch((error) => console.log(error)) +} + +const isWorkingBranchAhead = async (github) => { + const result = await github.rest.repos.compareCommitsWithBasehead({ + owner: config.owner, + repo: config.repo, + basehead: `${config.rcMergesTo}...${config.rcBranchesFrom}`, + per_page: 1, + page: 1, + }) + return result.data.status === "ahead" +} + +const doesNotHaveInProgressRelease = async (github) => { + const releasePrs = await github.rest.pulls.list({ + owner: config.owner, + repo: config.repo, + state: "open", + base: config.rcMergesTo + }) + + const syncPrs = await github.rest.pulls.list({ + owner: config.owner, + repo: config.repo, + state: "open", + base: config.rcBranchesFrom, + head: `${config.owner}:${config.rcMergesTo}` + }) + + return releasePrs.data.length === 0 && syncPrs.data.length === 0 +} + +const startRelease = async (github) => { + console.log(`creating release candidate from head of ${config.rcBranchesFrom}`) + + await createBranch(github, "release-candidate", config.rcBranchesFrom) + await incrementVersionFile(github, rcBranchName) + + const createdPr = await github.rest.pulls.create({ + owner: config.owner, + repo: config.repo, + title: "[Auto] Release Candidate", + head: rcBranchName, + base: config.rcMergesTo, + body: "todo", + }) + + github.graphql( + ` + mutation ($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) { + enablePullRequestAutoMerge(input: { + pullRequestId: $pullRequestId, + mergeMethod: $mergeMethod + }) { + pullRequest { + autoMergeRequest { + enabledAt + enabledBy { + login + } + } + } + } + } + `, + { + pullRequestId: createdPr.data.node_id, + mergeMethod: "MERGE" + } + ) +} + +const createBranch = async (github, branchName, fromBranch) => { + const mainRef = await github.rest.git.getRef({ + owner: config.owner, + repo: config.repo, + ref: `heads/${fromBranch}`, + }) + + await github.rest.git.createRef({ + owner: config.owner, + repo: config.repo, + ref: `refs/heads/${branchName}`, + sha: mainRef.data.object.sha, + }) +} + +const incrementVersionFile = async (github, branchName) => { + const versionFile = await readVersionFile(github, branchName) + + const updatedVersionFile = { + ...versionFile.content, + code: versionFile.content.code + 1, + } + const encodedContentUpdate = Buffer.from(JSON.stringify(updatedVersionFile, null, 2)).toString('base64') + await github.rest.repos.createOrUpdateFileContents({ + owner: config.owner, + repo: config.repo, + content: encodedContentUpdate, + path: config.pathToVersionFile, + sha: versionFile.sha, + branch: branchName, + message: "updating version for release", + }) +} + +const readVersionFile = async (github, branch) => { + const result = await github.rest.repos.getContent({ + owner: config.owner, + repo: config.repo, + path: config.pathToVersionFile, + ref: branch, + }) + + const content = Buffer.from(result.data.content, result.data.encoding).toString() + return { + content: JSON.parse(content), + sha: result.data.sha, + } +} \ No newline at end of file diff --git a/tools/beta-release/package-lock.json b/tools/beta-release/package-lock.json new file mode 100644 index 0000000..a35258b --- /dev/null +++ b/tools/beta-release/package-lock.json @@ -0,0 +1,767 @@ +{ + "name": "beta-release", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "beta-release", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@googleapis/androidpublisher": "^3.0.0" + } + }, + "node_modules/@googleapis/androidpublisher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/androidpublisher/-/androidpublisher-3.0.0.tgz", + "integrity": "sha512-r4JfmLlcu/VI4hObZuQ8RW5OeWRFtOxqE9xU8C2GAp3GTu2ZcemEICu69xy/rchpzMNQY4lrr8WiqUG1LE1L5Q==", + "dependencies": { + "googleapis-common": "^5.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", + "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.4.tgz", + "integrity": "sha512-x6lDDm/tBAzX9kmsPcZsNbvDs3Zey3+scsxaZElS8xWLgUMAg/oFLeewfUz0mu1CblHhhsu15jGkraldkFh8KQ==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "dependencies": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-p12-pem": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", + "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/googleapis-common": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.1.0.tgz", + "integrity": "sha512-RXrif+Gzhq1QAzfjxulbGvAY3FPj8zq/CYcvgjzDbaBNCD6bUl+86I7mUs4DKWHGruuK26ijjR/eDpWIDgNROA==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "google-auth-library": "^7.14.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + }, + "dependencies": { + "@googleapis/androidpublisher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/androidpublisher/-/androidpublisher-3.0.0.tgz", + "integrity": "sha512-r4JfmLlcu/VI4hObZuQ8RW5OeWRFtOxqE9xU8C2GAp3GTu2ZcemEICu69xy/rchpzMNQY4lrr8WiqUG1LE1L5Q==", + "requires": { + "googleapis-common": "^5.0.1" + } + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bignumber.js": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.0.tgz", + "integrity": "sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==" + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-text-encoding": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.4.tgz", + "integrity": "sha512-x6lDDm/tBAzX9kmsPcZsNbvDs3Zey3+scsxaZElS8xWLgUMAg/oFLeewfUz0mu1CblHhhsu15jGkraldkFh8KQ==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", + "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "requires": { + "node-forge": "^1.3.1" + } + }, + "googleapis-common": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.1.0.tgz", + "integrity": "sha512-RXrif+Gzhq1QAzfjxulbGvAY3FPj8zq/CYcvgjzDbaBNCD6bUl+86I7mUs4DKWHGruuK26ijjR/eDpWIDgNROA==", + "requires": { + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "google-auth-library": "^7.14.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^8.0.0" + } + }, + "gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/tools/beta-release/package.json b/tools/beta-release/package.json new file mode 100644 index 0000000..b36676e --- /dev/null +++ b/tools/beta-release/package.json @@ -0,0 +1,11 @@ +{ + "name": "beta-release", + "version": "1.0.0", + "main": "app.js", + "license": "MIT", + "type": "module", + "private": true, + "dependencies": { + "@googleapis/androidpublisher": "^3.0.0" + } +} diff --git a/tools/beta-release/release.js b/tools/beta-release/release.js new file mode 100644 index 0000000..94789ce --- /dev/null +++ b/tools/beta-release/release.js @@ -0,0 +1,201 @@ +import * as google from '@googleapis/androidpublisher'; +import * as fs from "fs"; +import * as http from 'https'; +import * as url from 'url'; + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +const auth = new google.auth.GoogleAuth({ + keyFile: '.secrets/service-account.json', + scopes: ['https://www.googleapis.com/auth/androidpublisher'], +}) + +const androidPublisher = google.androidpublisher({ + version: 'v3', + auth: auth, +}) + +const universalApkPath = `${__dirname}/universal.apk` + +export const release = async (github, version, applicationId, artifacts, config) => { + const appEditId = await startPlayRelease(applicationId) + + console.log("Uploading bundle...") + await uploadBundle(appEditId, applicationId, artifacts.bundle) + + console.log("Uploading mapping...") + await uploadMappingFile(appEditId, version.code, applicationId, artifacts.mapping) + + console.log("Assign artifacts to beta release...") + await addReleaseToTrack(appEditId, version, applicationId) + + console.log("Commiting draft release...") + await androidPublisher.edits.commit({ + editId: appEditId, + packageName: applicationId, + }).catch((error) => Promise.reject(error.response.data)) + + console.log("Downloading generated universal apk...") + await dowloadSignedUniversalApk( + version, + applicationId, + await auth.getAccessToken(), + universalApkPath + ) + + const releaseResult = await github.rest.repos.createRelease({ + owner: config.owner, + repo: config.repo, + tag_name: version.name, + prerelease: true, + generate_release_notes: true, + }) + + console.log(releaseResult.data.id) + + await github.rest.repos.uploadReleaseAsset({ + owner: config.owner, + repo: config.repo, + release_id: releaseResult.data.id, + name: `universal-${version.name}.apk`, + data: fs.readFileSync(universalApkPath) + }) + + console.log("Promoting beta draft release to live...") + await promoteDraftToLive(applicationId) +} + +const startPlayRelease = async (applicationId) => { + const result = await androidPublisher.edits.insert({ + packageName: applicationId + }).catch((error) => Promise.reject(error.response.data)) + return result.data.id +} + +const uploadBundle = async (appEditId, applicationId, bundleReleaseFile) => { + const res = await androidPublisher.edits.bundles.upload({ + packageName: applicationId, + editId: appEditId, + media: { + mimeType: 'application/octet-stream', + body: fs.createReadStream(bundleReleaseFile) + } + }).catch((error) => Promise.reject(error.response.data)) + + return res.data +} + +const uploadMappingFile = async (appEditId, versionCode, applicationId, mappingFilePath) => { + await androidPublisher.edits.deobfuscationfiles.upload({ + packageName: applicationId, + editId: appEditId, + apkVersionCode: versionCode, + deobfuscationFileType: 'proguard', + media: { + mimeType: 'application/octet-stream', + body: fs.createReadStream(mappingFilePath) + } + }).catch((error) => Promise.reject(error.response.data)) +} + +const addReleaseToTrack = async (appEditId, version, applicationId) => { + const result = await androidPublisher.edits.tracks + .update({ + editId: appEditId, + packageName: applicationId, + track: "beta", + requestBody: { + track: "beta", + releases: [ + { + name: version.name, + status: "draft", + releaseNotes: { + language: "en-GB", + text: "Bug fixes and improvments - See https://github.com/ouchadam/small-talk/releases for more details", + }, + versionCodes: [version.code] + } + ] + } + }) + .catch((error) => Promise.reject(error.response.data)) + return result.data; +} + + +const dowloadSignedUniversalApk = async (version, applicationId, authToken, outputFile) => { + console.log("fetching universal apk") + + const apkRes = await androidPublisher.generatedapks.list({ + packageName: applicationId, + versionCode: version.code, + }) + + const apks = apkRes.data.generatedApks + + console.log(`found ${apks.length} apks`) + apks.forEach((apk) => { + console.log(apk) + }) + + const id = apks[0].generatedUniversalApk.downloadId + + console.log(`downloading: ${id}`) + + const downloadUrl = `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${applicationId}/generatedApks/${version.code}/downloads/${id}:download?alt=media` + const options = { + headers: { + "Authorization": `Bearer ${authToken}` + } + } + + await downloadToFile(downloadUrl, options, outputFile) +} + +const downloadToFile = async (url, options, outputFile) => { + return new Promise((resolve, error) => { + http.get(url, options, (response) => { + const file = fs.createWriteStream(outputFile) + response.pipe(file) + + file.on("finish", () => { + file.close() + resolve() + }) + + file.on("error", (cause) => { + error(cause) + }) + }).on("error", (cause) => { + error(cause) + }) + }) +} + +const promoteDraftToLive = async () => { + const fappEditId = await startPlayRelease(applicationId) + + await androidPublisher.edits.tracks + .update({ + editId: fappEditId, + packageName: applicationId, + track: "beta", + requestBody: { + track: "beta", + releases: [ + { + status: "completed", + } + ] + } + }) + .catch((error) => Promise.reject(error.response.data)) + + + await androidPublisher.edits.commit({ + editId: fappEditId, + packageName: applicationId, + }).catch((error) => Promise.reject(error.response.data)) +} + diff --git a/tools/coverage.gradle b/tools/coverage.gradle index 15d22bd..009e835 100644 --- a/tools/coverage.gradle +++ b/tools/coverage.gradle @@ -67,30 +67,10 @@ def collectProjects(predicate) { return subprojects.findAll { it.buildFile.isFile() && predicate(it) } } -//task unitCodeCoverageReport(type: JacocoReport) { -// outputs.upToDateWhen { false } -// rootProject.apply plugin: 'jacoco' -// def excludedProjects = [ -// 'olm-stub', -// 'test-harness' -// ] -// def projects = collectProjects { !excludedProjects.contains(it.name) } -// dependsOn { ["app:assembleDebug"] + projects*.test } -// initializeReport(it, projects, excludes) -//} -// -//task harnessCodeCoverageReport(type: JacocoReport) { -// outputs.upToDateWhen { false } -// rootProject.apply plugin: 'jacoco' -// def projects = collectProjects { true } -// dependsOn { ["app:assembleDebug", project(":test-harness").test] } -// initializeReport(it, projects, excludes) -//} - task allCodeCoverageReport(type: JacocoReport) { outputs.upToDateWhen { false } rootProject.apply plugin: 'jacoco' - def projects = collectProjects { !it.name.contains("stub") } + def projects = collectProjects { !it.name.contains("stub") && !it.name.contains("-noop") } dependsOn { ["app:assembleDebug"] + projects*.test } initializeReport(it, projects, excludes) } diff --git a/tools/generate-release.sh b/tools/generate-release.sh new file mode 100755 index 0000000..16ae11f --- /dev/null +++ b/tools/generate-release.sh @@ -0,0 +1,15 @@ +#! /bin/bash + +./gradlew clean bundleRelease -Punsigned --no-daemon --no-configuration-cache --no-build-cache + +WORKING_DIR=app/build/outputs/bundle/release +RELEASE_AAB=$WORKING_DIR/app-release.aab + +cp $RELEASE_AAB $WORKING_DIR/app-release-unsigned.aab + +echo "signing $RELEASE_AAB" +jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \ + -keystore .secrets/upload-key.jks \ + -storepass $1 \ + $RELEASE_AAB \ + key0 diff --git a/version.json b/version.json index c8f2797..60997cb 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "name": "0.0.1-alpha03", - "code": 5 + "name": "0.0.1-alpha04", + "code": 7 } \ No newline at end of file