Merge pull request #106 from ouchadam/release-candidate

[Auto] Release Candidate
This commit is contained in:
Adam Brown 2022-08-31 20:48:29 +01:00
commit c580bcbdab
44 changed files with 1622 additions and 135 deletions

View File

@ -24,4 +24,6 @@ jobs:
- uses: gradle/gradle-build-action@v2 - uses: gradle/gradle-build-action@v2
- name: Assemble debug variant - name: Assemble debug variant
run: ./gradlew assembleDebug --no-daemon run: |
./gradlew assembleDebug --no-daemon
./gradlew assembleDebug -Pfoss --no-daemon

31
.github/workflows/nightly.yml vendored Normal file
View File

@ -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})

50
.github/workflows/release-candidate.yml vendored Normal file
View File

@ -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)

2
.gitignore vendored
View File

@ -11,3 +11,5 @@
.cxx .cxx
local.properties local.properties
/benchmark-out /benchmark-out
**/node_modules
.secrets

View File

@ -16,8 +16,6 @@
- Focused on reliability and stability. - Focused on reliability and stability.
- Bare-bones feature set. - Bare-bones feature set.
##### _*Google play only with automatic crash reporting enabled_
--- ---
### Feature list ### Feature list
@ -29,6 +27,7 @@
- Push notifications (DMs always notify, Rooms notify once) - Push notifications (DMs always notify, Rooms notify once)
- Importing of E2E room keys from Element clients - Importing of E2E room keys from Element clients
- [UnifiedPush](https://unifiedpush.org/) - [UnifiedPush](https://unifiedpush.org/)
- FOSS variant
### Planned ### 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 #### Join the conversation @ https://matrix.to/#/#small-talk:iswell.cool

View File

@ -24,6 +24,10 @@ android {
} else { } else {
resConfigs "en" resConfigs "en"
} }
if (isFoss()) {
archivesBaseName = "$archivesBaseName-foss"
}
} }
bundle { bundle {
@ -46,8 +50,11 @@ android {
"proguard/serializationx.pro", "proguard/serializationx.pro",
"proguard/olm.pro" "proguard/olm.pro"
// actual releases are signed with a different config if (project.hasProperty("unsigned")) {
signingConfig = buildTypes.debug.signingConfig // releases are signed externally
} else {
signingConfig = buildTypes.debug.signingConfig
}
} }
} }
@ -82,6 +89,8 @@ dependencies {
implementation project(":domains:android:imageloader") implementation project(":domains:android:imageloader")
implementation project(":domains:olm") implementation project(":domains:olm")
firebase(it, "messaging")
implementation project(":matrix:matrix") implementation project(":matrix:matrix")
implementation project(":matrix:matrix-http-ktor") implementation project(":matrix:matrix-http-ktor")
implementation project(":matrix:services:auth") implementation project(":matrix:services:auth")

View File

@ -1,7 +1,6 @@
package app.dapk.st package app.dapk.st
import android.app.Application import android.app.Application
import android.content.Intent
import android.util.Log import android.util.Log
import app.dapk.st.core.CoreAndroidModule import app.dapk.st.core.CoreAndroidModule
import app.dapk.st.core.ModuleProvider 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.core.extensions.Scope
import app.dapk.st.directory.DirectoryModule import app.dapk.st.directory.DirectoryModule
import app.dapk.st.domain.StoreModule import app.dapk.st.domain.StoreModule
import app.dapk.st.firebase.messaging.MessagingModule
import app.dapk.st.graph.AppModule import app.dapk.st.graph.AppModule
import app.dapk.st.home.HomeModule import app.dapk.st.home.HomeModule
import app.dapk.st.login.LoginModule import app.dapk.st.login.LoginModule
import app.dapk.st.messenger.MessengerModule import app.dapk.st.messenger.MessengerModule
import app.dapk.st.notifications.NotificationsModule import app.dapk.st.notifications.NotificationsModule
import app.dapk.st.profile.ProfileModule import app.dapk.st.profile.ProfileModule
import app.dapk.st.push.firebase.FirebasePushService
import app.dapk.st.push.PushModule import app.dapk.st.push.PushModule
import app.dapk.st.settings.SettingsModule import app.dapk.st.settings.SettingsModule
import app.dapk.st.share.ShareEntryModule import app.dapk.st.share.ShareEntryModule
@ -75,6 +74,7 @@ class SmallTalkApplication : Application(), ModuleProvider {
ProfileModule::class -> featureModules.profileModule ProfileModule::class -> featureModules.profileModule
NotificationsModule::class -> featureModules.notificationsModule NotificationsModule::class -> featureModules.notificationsModule
PushModule::class -> featureModules.pushModule PushModule::class -> featureModules.pushModule
MessagingModule::class -> featureModules.messagingModule
MessengerModule::class -> featureModules.messengerModule MessengerModule::class -> featureModules.messengerModule
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
CoreAndroidModule::class -> appModule.coreAndroidModule CoreAndroidModule::class -> appModule.coreAndroidModule

View File

@ -16,6 +16,7 @@ import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.directory.DirectoryModule import app.dapk.st.directory.DirectoryModule
import app.dapk.st.domain.StoreModule import app.dapk.st.domain.StoreModule
import app.dapk.st.firebase.messaging.MessagingModule
import app.dapk.st.home.HomeModule import app.dapk.st.home.HomeModule
import app.dapk.st.home.MainActivity import app.dapk.st.home.MainActivity
import app.dapk.st.imageloader.ImageLoaderModule 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.olm.OlmWrapper
import app.dapk.st.profile.ProfileModule import app.dapk.st.profile.ProfileModule
import app.dapk.st.push.PushModule import app.dapk.st.push.PushModule
import app.dapk.st.push.messaging.MessagingServiceAdapter
import app.dapk.st.settings.SettingsModule import app.dapk.st.settings.SettingsModule
import app.dapk.st.share.ShareEntryModule import app.dapk.st.share.ShareEntryModule
import app.dapk.st.tracking.TrackingModule import app.dapk.st.tracking.TrackingModule
@ -208,6 +210,10 @@ internal class FeatureModules internal constructor(
domainModules.pushModule domainModules.pushModule
} }
val messagingModule by unsafeLazy {
domainModules.messaging
}
} }
internal class MatrixModules( internal class MatrixModules(
@ -428,23 +434,30 @@ internal class DomainModules(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
) { ) {
val pushModule by unsafeLazy { val pushHandler by unsafeLazy {
val store = storeModule.value val store = storeModule.value
val pushHandler = MatrixPushHandler( MatrixPushHandler(
workScheduler = workModule.workScheduler(), workScheduler = workModule.workScheduler(),
credentialsStore = store.credentialsStore(), credentialsStore = store.credentialsStore(),
matrixModules.sync, matrixModules.sync,
store.roomStore(), store.roomStore(),
) )
}
val messaging by unsafeLazy { MessagingModule(MessagingServiceAdapter(pushHandler), context) }
val pushModule by unsafeLazy {
PushModule( PushModule(
errorTracker, errorTracker,
pushHandler, pushHandler,
context, context,
dispatchers, 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))) } val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) }
} }
internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader { internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader {

View File

@ -118,7 +118,7 @@ ext.applyAndroidLibraryModule = { project ->
} }
ext.applyCrashlyticsIfRelease = { project -> ext.applyCrashlyticsIfRelease = { project ->
if (isReleaseBuild) { if (isReleaseBuild && !isFoss()) {
project.apply plugin: 'com.google.firebase.crashlytics' project.apply plugin: 'com.google.firebase.crashlytics'
project.afterEvaluate { project.afterEvaluate {
project.tasks.withType(com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask).configureEach { 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") 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())) { if (launchTask.contains("codeCoverageReport".toLowerCase())) {
apply from: 'tools/coverage.gradle' apply from: 'tools/coverage.gradle'
} }

View File

@ -6,8 +6,9 @@ dependencies {
implementation project(':domains:android:core') implementation project(':domains:android:core')
implementation project(':domains:store') implementation project(':domains:store')
implementation project(':matrix:services:push') 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.mavenCentral.kotlinSerializationJson
implementation Dependencies.jitPack.unifiedPush implementation Dependencies.jitPack.unifiedPush
} }

View File

@ -2,15 +2,6 @@
<manifest package="app.dapk.st.push" xmlns:android="http://schemas.android.com/apk/res/android"> <manifest package="app.dapk.st.push" xmlns:android="http://schemas.android.com/apk/res/android">
<application> <application>
<service
android:name=".firebase.FirebasePushService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<receiver android:exported="true" android:enabled="true" android:name=".unifiedpush.UnifiedPushMessageReceiver"> <receiver android:exported="true" android:enabled="true" android:name=".unifiedpush.UnifiedPushMessageReceiver">
<intent-filter> <intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/> <action android:name="org.unifiedpush.android.connector.MESSAGE"/>

View File

@ -7,7 +7,8 @@ import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.domain.Preferences import app.dapk.st.domain.Preferences
import app.dapk.st.domain.push.PushTokenRegistrarPreferences 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 import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar
class PushModule( class PushModule(
@ -16,15 +17,16 @@ class PushModule(
private val context: Context, private val context: Context,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val preferences: Preferences, private val preferences: Preferences,
private val messaging: Messaging,
) : ProvidableModule { ) : ProvidableModule {
private val registrars by unsafeLazy { private val registrars by unsafeLazy {
PushTokenRegistrars( PushTokenRegistrars(
context, context,
FirebasePushTokenRegistrar( MessagingPushTokenRegistrar(
errorTracker, errorTracker,
context,
pushHandler, pushHandler,
messaging,
), ),
UnifiedPushRegistrar(context), UnifiedPushRegistrar(context),
PushTokenRegistrarPreferences(preferences) PushTokenRegistrarPreferences(preferences)

View File

@ -2,7 +2,7 @@ package app.dapk.st.push
import android.content.Context import android.content.Context
import app.dapk.st.domain.push.PushTokenRegistrarPreferences 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 app.dapk.st.push.unifiedpush.UnifiedPushRegistrar
import org.unifiedpush.android.connector.UnifiedPush import org.unifiedpush.android.connector.UnifiedPush
@ -11,7 +11,7 @@ private val NONE = Registrar("None")
class PushTokenRegistrars( class PushTokenRegistrars(
private val context: Context, private val context: Context,
private val firebasePushTokenRegistrar: FirebasePushTokenRegistrar, private val messagingPushTokenRegistrar: MessagingPushTokenRegistrar,
private val unifiedPushRegistrar: UnifiedPushRegistrar, private val unifiedPushRegistrar: UnifiedPushRegistrar,
private val pushTokenStore: PushTokenRegistrarPreferences, private val pushTokenStore: PushTokenRegistrarPreferences,
) : PushTokenRegistrar { ) : PushTokenRegistrar {
@ -19,27 +19,36 @@ class PushTokenRegistrars(
private var selection: Registrar? = null private var selection: Registrar? = null
fun options(): List<Registrar> { fun options(): List<Registrar> {
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) { suspend fun makeSelection(option: Registrar) {
selection = option selection = option
pushTokenStore.store(option.id) pushTokenStore.store(option.id)
when (option) { when (option) {
NONE -> { NONE -> {
firebasePushTokenRegistrar.unregister() messagingPushTokenRegistrar.unregister()
unifiedPushRegistrar.unregister() unifiedPushRegistrar.unregister()
} }
FIREBASE_OPTION -> { FIREBASE_OPTION -> {
unifiedPushRegistrar.unregister() unifiedPushRegistrar.unregister()
firebasePushTokenRegistrar.registerCurrentToken() messagingPushTokenRegistrar.registerCurrentToken()
} }
else -> { else -> {
firebasePushTokenRegistrar.unregister() messagingPushTokenRegistrar.unregister()
unifiedPushRegistrar.registerSelection(option) unifiedPushRegistrar.registerSelection(option)
} }
} }
@ -47,7 +56,7 @@ class PushTokenRegistrars(
override suspend fun registerCurrentToken() { override suspend fun registerCurrentToken() {
when (selection) { when (selection) {
FIREBASE_OPTION -> firebasePushTokenRegistrar.registerCurrentToken() FIREBASE_OPTION -> messagingPushTokenRegistrar.registerCurrentToken()
NONE -> { NONE -> {
// do nothing // do nothing
} }
@ -58,10 +67,10 @@ class PushTokenRegistrars(
override fun unregister() { override fun unregister() {
when (selection) { when (selection) {
FIREBASE_OPTION -> firebasePushTokenRegistrar.unregister() FIREBASE_OPTION -> messagingPushTokenRegistrar.unregister()
NONE -> { NONE -> {
runCatching { runCatching {
firebasePushTokenRegistrar.unregister() messagingPushTokenRegistrar.unregister()
unifiedPushRegistrar.unregister() unifiedPushRegistrar.unregister()
} }
} }

View File

@ -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<String> { 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()

View File

@ -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.AppLogTag
import app.dapk.st.core.extensions.CrashScope import app.dapk.st.core.extensions.CrashScope
import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.log import app.dapk.st.core.log
import app.dapk.st.firebase.messaging.Messaging
import app.dapk.st.push.PushHandler import app.dapk.st.push.PushHandler
import app.dapk.st.push.PushTokenPayload import app.dapk.st.push.PushTokenPayload
import app.dapk.st.push.PushTokenRegistrar 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" private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify"
class FirebasePushTokenRegistrar( class MessagingPushTokenRegistrar(
override val errorTracker: ErrorTracker, override val errorTracker: ErrorTracker,
private val context: Context,
private val pushHandler: PushHandler, private val pushHandler: PushHandler,
private val messaging: Messaging,
) : PushTokenRegistrar, CrashScope { ) : PushTokenRegistrar, CrashScope {
override suspend fun registerCurrentToken() { override suspend fun registerCurrentToken() {
log(AppLogTag.PUSH, "FCM - register current token") log(AppLogTag.PUSH, "FCM - register current token")
context.packageManager.setComponentEnabledSetting( messaging.enable()
ComponentName(context, FirebasePushService::class.java),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP,
)
kotlin.runCatching { kotlin.runCatching {
FirebaseMessaging.getInstance().token().also { messaging.token().also {
pushHandler.onNewToken( pushHandler.onNewToken(
PushTokenPayload( PushTokenPayload(
token = it, token = it,
@ -48,14 +39,10 @@ class FirebasePushTokenRegistrar(
override fun unregister() { override fun unregister() {
log(AppLogTag.PUSH, "FCM - unregister") log(AppLogTag.PUSH, "FCM - unregister")
FirebaseMessaging.getInstance().deleteToken() messaging.deleteToken()
context.stopService(Intent(context, FirebasePushService::class.java)) messaging.disable()
context.packageManager.setComponentEnabledSetting(
ComponentName(context, FirebasePushService::class.java),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP,
)
} }
fun isAvailable() = messaging.isAvailable()
} }

View File

@ -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)
}
}

View File

@ -2,9 +2,5 @@ applyAndroidLibraryModule(project)
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation platform('com.google.firebase:firebase-bom:29.0.3') firebase(it, "crashlytics")
implementation 'com.google.firebase:firebase-crashlytics'
// is it worth the 400kb size increase?
// implementation 'com.google.firebase:firebase-analytics'
} }

View File

@ -4,16 +4,12 @@ import android.util.Log
import app.dapk.st.core.AppLogTag import app.dapk.st.core.AppLogTag
import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.log import app.dapk.st.core.log
import com.google.firebase.crashlytics.FirebaseCrashlytics
class CrashlyticsCrashTracker( class CrashTrackerLogger : ErrorTracker {
private val firebaseCrashlytics: FirebaseCrashlytics,
) : ErrorTracker {
override fun track(throwable: Throwable, extra: String) { override fun track(throwable: Throwable, extra: String) {
Log.e("ST", throwable.message, throwable) Log.e("ST", throwable.message, throwable)
log(AppLogTag.ERROR_NON_FATAL, "${throwable.message ?: "N/A"} extra=$extra") log(AppLogTag.ERROR_NON_FATAL, "${throwable.message ?: "N/A"} extra=$extra")
firebaseCrashlytics.recordException(throwable)
} }
} }

View File

@ -1,9 +1,8 @@
package app.dapk.st.tracking package app.dapk.st.tracking
import android.util.Log
import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.core.extensions.unsafeLazy
import com.google.firebase.crashlytics.FirebaseCrashlytics import app.dapk.st.firebase.crashlytics.CrashlyticsModule
class TrackingModule( class TrackingModule(
private val isCrashTrackingEnabled: Boolean, private val isCrashTrackingEnabled: Boolean,
@ -11,13 +10,18 @@ class TrackingModule(
val errorTracker: ErrorTracker by unsafeLazy { val errorTracker: ErrorTracker by unsafeLazy {
when (isCrashTrackingEnabled) { when (isCrashTrackingEnabled) {
true -> CrashlyticsCrashTracker(FirebaseCrashlytics.getInstance()) true -> compositeTracker(
false -> object : ErrorTracker { CrashTrackerLogger(),
override fun track(throwable: Throwable, extra: String) { CrashlyticsModule().errorTracker,
Log.e("error", throwable.message, throwable) )
} false -> CrashTrackerLogger()
}
} }
} }
} }
private fun compositeTracker(vararg loggers: ErrorTracker) = object : ErrorTracker {
override fun track(throwable: Throwable, extra: String) {
loggers.forEach { it.track(throwable, extra) }
}
}

View File

@ -0,0 +1,5 @@
plugins { id 'kotlin' }
dependencies {
implementation project(':core')
}

View File

@ -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
}
}
}
}

View File

@ -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'
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="app.dapk.st.firebase.crashlytics"/>

View File

@ -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)
}
}

View File

@ -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())
}
}

View File

@ -0,0 +1,6 @@
applyAndroidLibraryModule(project)
dependencies {
implementation project(':core')
implementation project(':matrix:common')
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="app.dapk.st.firebase.messaging"/>

View File

@ -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 ""
}
}

View File

@ -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()
}
}

View File

@ -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?)
}

View File

@ -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'
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="app.dapk.st.firebase.messaging" xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:name=".FirebasePushServiceDelegate"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
</application>
</manifest>

View File

@ -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.AppLogTag
import app.dapk.st.core.extensions.unsafeLazy 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.core.module
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId 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.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage 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 delegate by unsafeLazy { module<MessagingModule>().serviceDelegate }
private val handler by unsafeLazy { module<PushModule>().pushHandler() }
override fun onNewToken(token: String) { override fun onNewToken(token: String) {
log(AppLogTag.PUSH, "FCM onNewToken") delegate.onNewToken(token)
handler.onNewToken(
PushTokenPayload(
token = token,
gatewayUrl = SYGNAL_GATEWAY,
)
)
} }
override fun onMessageReceived(message: RemoteMessage) { override fun onMessageReceived(message: RemoteMessage) {
log(AppLogTag.PUSH, "FCM onMessage") log(AppLogTag.PUSH, "FCM onMessage")
val eventId = message.data["event_id"]?.let { EventId(it) } val eventId = message.data["event_id"]?.let { EventId(it) }
val roomId = message.data["room_id"]?.let { RoomId(it) } val roomId = message.data["room_id"]?.let { RoomId(it) }
handler.onMessageReceived(eventId, roomId) delegate.onMessageReceived(eventId, roomId)
} }
} }

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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?)
}

View File

@ -34,6 +34,11 @@ include ':domains:store'
include ':domains:olm-stub' include ':domains:olm-stub'
include ':domains:olm' 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:matrix'
include ':matrix:common' include ':matrix:common'
include ':matrix:matrix-http' include ':matrix:matrix-http'

152
tools/beta-release/app.js Normal file
View File

@ -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,
}
}

767
tools/beta-release/package-lock.json generated Normal file
View File

@ -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=="
}
}
}

View File

@ -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"
}
}

View File

@ -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))
}

View File

@ -67,30 +67,10 @@ def collectProjects(predicate) {
return subprojects.findAll { it.buildFile.isFile() && predicate(it) } 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) { task allCodeCoverageReport(type: JacocoReport) {
outputs.upToDateWhen { false } outputs.upToDateWhen { false }
rootProject.apply plugin: 'jacoco' 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 } dependsOn { ["app:assembleDebug"] + projects*.test }
initializeReport(it, projects, excludes) initializeReport(it, projects, excludes)
} }

15
tools/generate-release.sh Executable file
View File

@ -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

View File

@ -1,4 +1,4 @@
{ {
"name": "0.0.1-alpha03", "name": "0.0.1-alpha04",
"code": 5 "code": 7
} }