diff --git a/.gitignore b/.gitignore index dc8e5e70..24bbd7ce 100644 --- a/.gitignore +++ b/.gitignore @@ -560,3 +560,6 @@ obj/ /zeapp/app/release/output-metadata.json /zeapp/app/debug/output-metadata.json /zeapp/versions.properties + +*.db +zeapp/profiles/*png diff --git a/zeapp/android/build.gradle.kts b/zeapp/android/build.gradle.kts index d84b57c3..90490c0d 100644 --- a/zeapp/android/build.gradle.kts +++ b/zeapp/android/build.gradle.kts @@ -118,10 +118,11 @@ android { kotlinOptions { jvmTarget = "1.8" - freeCompilerArgs += listOf( - "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", - "-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi", - ) + freeCompilerArgs += + listOf( + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi", + ) } buildFeatures { @@ -130,18 +131,21 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + kotlinCompilerExtensionVersion = + libs.versions.androidx.compose.compiler + .get() } packaging { resources { - excludes += arrayOf( - "META-INF/AL2.0", - "META-INF/LGPL2.1", - "META-INF/*.kotlin_module", - "META-INF/LICENSE.*", - "META-INF/LICENSE-notice.*", - ) + excludes += + arrayOf( + "META-INF/AL2.0", + "META-INF/LGPL2.1", + "META-INF/*.kotlin_module", + "META-INF/LICENSE.*", + "META-INF/LICENSE-notice.*", + ) } } } @@ -219,19 +223,26 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs::class).confi tasks.create("generateContributorsAsset") { val command = "git shortlog -sne --all" - val process = ProcessBuilder() - .command(command.split(" ")) - .directory(rootProject.projectDir) - .redirectOutput(Redirect.PIPE) - .redirectError(Redirect.PIPE) - .start() + val process = + ProcessBuilder() + .command(command.split(" ")) + .directory(rootProject.projectDir) + .redirectOutput(Redirect.PIPE) + .redirectError(Redirect.PIPE) + .start() process.waitFor(60, TimeUnit.SECONDS) val result = process.inputStream.bufferedReader().readText() - val contributors = result.lines() - .joinToString(separator = System.lineSeparator()) { it.substringAfter("\t") } + val contributors = + result + .lines() + .joinToString(separator = System.lineSeparator()) { it.substringAfter("\t") } - val assetDir = layout.buildDirectory.dir("generated/assets").get().asFile + val assetDir = + layout.buildDirectory + .dir("generated/assets") + .get() + .asFile assetDir.createDirectory() File(assetDir, "test.txt").writeText(contributors) } diff --git a/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParser.kt b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParser.kt new file mode 100644 index 00000000..4f3775f2 --- /dev/null +++ b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParser.kt @@ -0,0 +1,117 @@ +package de.berlindroid.zeapp.zeservices + +import android.util.Base64 +import java.util.UUID +import javax.inject.Inject + +private const val SPACE_ESCAPED = "\$SPACE#" + +/** + * Parses the badge configuration using the following format: + * + * ``` + * wifi_attached=False user.uuid=4d3f6ca7‑d256‑4f84‑a6c6‑099a26055d4c \ + * user.description=Edward$SPACE#Bernard,$SPACE#a$SPACE#veteran \ + * user.name=Edward$SPACE#Bernard developer_mode=True \ + * user.iconB64=eNpjYGBgUJnkqaIg6MDAAmTX/+U+WGf//399OwNjYfv/gk1AQ==" + * ``` + */ +class ZeBadgeConfigParser + @Inject + constructor() { + fun parse(configString: String): ParseResult { + val configMap = + configString + .split("\\s+".toRegex()) + .map { it.split("=", limit = 2) } + .filter { it.size == 2 } + .associate { it[0] to it[1] } + + val userId = parseUserId(configMap) + val userName = parseUserName(configMap) + val userDescription = parseUserDescription(configMap) + val userProfilePhoto = parseUserProfilePhoto(configMap) + val userInfo = + if ( + userId != null && userName != null && userDescription != null && userProfilePhoto != null + ) { + UserInfo(userId, userName, userDescription, userProfilePhoto) + } else { + null + } + + return ParseResult( + userInfo = userInfo, + isWiFiAttached = parseWiFiAttached(configMap), + isDeveloperMode = parseDeveloperMode(configMap), + ) + } + + private fun parseWiFiAttached(configMap: Map): Boolean = configMap["wifi_attached"]?.toBoolean() ?: false + + private fun parseDeveloperMode(configMap: Map): Boolean = configMap["developer_mode"]?.toBoolean() ?: false + + private fun parseUserId(configMap: Map): UUID? = configMap["user.uuid"]?.let { UUID.fromString(it) } + + private fun parseUserProfilePhoto(configMap: Map): ByteArray? = + configMap["user.iconB64"]?.let { Base64.decode(it, Base64.DEFAULT) } + + private fun parseUserName(configMap: Map): String? = configMap["user.name"]?.unescape() + + private fun parseUserDescription(configMap: Map): String? = configMap["user.description"]?.unescape() + + private fun String.unescape() = replace(SPACE_ESCAPED, " ") + } + +data class ParseResult( + val userInfo: UserInfo?, + val isWiFiAttached: Boolean, + val isDeveloperMode: Boolean, +) { + fun flatten(): Map { + val map = mutableMapOf() + userInfo?.flatten()?.forEach { (key, value) -> + map["user.$key"] = value + } + map["isWiFiAttached"] = isWiFiAttached + map["isDeveloperMode"] = isDeveloperMode + return map + } +} + +data class UserInfo( + val id: UUID, + val name: String, + val description: String, + val profilePhoto: ByteArray, +) { + fun flatten() = + mapOf( + "id" to id.toString(), + "name" to name, + "description" to description, + "profilePhoto" to Base64.encode(profilePhoto, Base64.DEFAULT), + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserInfo + + if (id != other.id) return false + if (name != other.name) return false + if (description != other.description) return false + if (!profilePhoto.contentEquals(other.profilePhoto)) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + profilePhoto.contentHashCode() + return result + } +} diff --git a/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt index ab114125..e5444b56 100644 --- a/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt +++ b/zeapp/android/src/main/java/de/berlindroid/zeapp/zeservices/ZeBadgeManager.kt @@ -10,6 +10,7 @@ import de.berlindroid.zekompanion.base64 import de.berlindroid.zekompanion.buildBadgeManager import de.berlindroid.zekompanion.toBinary import de.berlindroid.zekompanion.zipit +import kotlinx.coroutines.delay import timber.log.Timber import javax.inject.Inject @@ -17,6 +18,7 @@ private const val SPACE_REPLACEMENT = "\$SPACE#" class ZeBadgeManager @Inject constructor( @ApplicationContext private val context: Context, + private val badgeConfigParser: ZeBadgeConfigParser, ) { private val badgeManager = buildBadgeManager(Environment(context)) @@ -74,10 +76,10 @@ class ZeBadgeManager @Inject constructor( payload = "", ) - if (badgeManager.sendPayload(payload).isSuccess) { - return badgeManager.readResponse() + return if (badgeManager.sendPayload(payload).isSuccess) { + badgeManager.readResponse() } else { - return Result.failure(NoSuchElementException()) + Result.failure(NoSuchElementException()) } } @@ -85,6 +87,17 @@ class ZeBadgeManager @Inject constructor( * Return the current active configuration. */ suspend fun listConfiguration(): Result> { + badgeManager.sendPayload( + BadgePayload( + type = "config_load", + meta = "", + payload = "", + ), + ) + + badgeManager.readResponse() + delay(300) + val payload = BadgePayload( type = "config_list", meta = "", @@ -93,23 +106,19 @@ class ZeBadgeManager @Inject constructor( if (badgeManager.sendPayload(payload).isSuccess) { val response = badgeManager.readResponse() + delay(300) + if (response.isSuccess) { - val config = response.getOrDefault("") - Timber.v("Badge sent response: successfully received configuration: '${config.replace("\n", "\\n")}'.") - - val kv = mapOf( - *config.split(" ").mapNotNull { - if ("=" in it) { - val (key, value) = it.split("=") - val typedValue = pythonToKotlin(value) - key to typedValue - } else { - Timber.v("Config '$it' is malformed, ignoring it.") - null - } - }.toTypedArray(), + val config = response.getOrDefault("").replace("\r\n", "") + Timber.v( + "Badge sent response: successfully received configuration: " + + "'${config.replace("\n", "\\n")}'.", ) - return Result.success(kv) + + val parseResult = badgeConfigParser.parse(config) + Timber.v("Badge config parsed: $parseResult") + + return Result.success(parseResult.flatten()) } return Result.failure(IllegalStateException("Could not read response.")) } else { @@ -136,8 +145,10 @@ class ZeBadgeManager @Inject constructor( ) if (badgeManager.sendPayload(payload).isSuccess) { + badgeManager.readResponse() + delay(300) - if (badgeManager.sendPayload( + return if (badgeManager.sendPayload( BadgePayload( type = "config_save", meta = "", @@ -145,9 +156,9 @@ class ZeBadgeManager @Inject constructor( ), ).isSuccess ) { - return Result.success(true) + Result.success(true) } else { - return Result.failure(IllegalStateException("Could not save the config to ZeBadge.")) + Result.failure(IllegalStateException("Could not save the config to ZeBadge.")) } } else { return Result.failure(NoSuchElementException("Could not update the runtime configuration on ZeBadge.")) @@ -157,30 +168,9 @@ class ZeBadgeManager @Inject constructor( fun isConnected(): Boolean = badgeManager.isConnected() } -private fun pythonToKotlin(value: String): Any? = when { - value.startsWith("\"") -> - value - .replace("\"", "") - .replace(SPACE_REPLACEMENT, " ") - - - value.startsWith("\'") -> - value - .replace("\'", "") - .replace(SPACE_REPLACEMENT, " ") - - - value == "None" -> null - value.toIntOrNull() != null -> value.toInt() - value.toFloatOrNull() != null -> value.toFloat() - value == "True" -> true - value == "False" -> false - else -> value -} - private fun kotlinToPython(value: Any?): String = when (value) { null -> "None" - is String -> "\"${value.replace(" ", SPACE_REPLACEMENT)}\"" + is String -> value.replace(" ", SPACE_REPLACEMENT) is Boolean -> if (value) "True" else "False" else -> "$value" } diff --git a/zeapp/android/src/test/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParserTest.kt b/zeapp/android/src/test/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParserTest.kt new file mode 100644 index 00000000..228000ca --- /dev/null +++ b/zeapp/android/src/test/java/de/berlindroid/zeapp/zeservices/ZeBadgeConfigParserTest.kt @@ -0,0 +1,88 @@ +package de.berlindroid.zeapp.zeservices + +import android.util.Base64 +import assertk.assertFailure +import assertk.assertions.messageContains +import io.mockk.every +import io.mockk.mockkStatic +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.util.UUID + +class ZeBadgeConfigParserTest { + private val parser = ZeBadgeConfigParser() + + @Before + fun setup() { + mockkStatic(Base64::class) + every { Base64.decode(any(), any()) } returns byteArrayOf(1, 2, 3, 4) + } + + @Test + fun `parse valid config string`() { + val configString = + "wifi_attached=False user.uuid=4d3f6ca7-d256-4f84-a6c6-099a26055d4c " + + "user.description=Edward\$SPACE#Bernard,\$SPACE#a\$SPACE#veteran " + + "user.name=Edward\$SPACE#Bernard developer_mode=True " + + "user.iconB64=eNpjYGBgUJnkqaIg6MDAAmTX/+U+WGd//399OwNjYfv/gk1AQ==" + + val result = parser.parse(configString) + + assertNotNull(result.userInfo) + assertEquals(UUID.fromString("4d3f6ca7-d256-4f84-a6c6-099a26055d4c"), result.userInfo?.id) + assertEquals("Edward Bernard", result.userInfo?.name) + assertEquals("Edward Bernard, a veteran", result.userInfo?.description) + assertFalse(result.isWiFiAttached) + assertTrue(result.isDeveloperMode) + assertNotNull(result.userInfo?.profilePhoto) + assertArrayEquals(byteArrayOf(1, 2, 3, 4), result.userInfo?.profilePhoto) + } + + @Test + fun `parse config string with missing user info`() { + val configString = "wifi_attached=True developer_mode=False" + + val result = parser.parse(configString) + + assertNull(result.userInfo) + assertTrue(result.isWiFiAttached) + assertFalse(result.isDeveloperMode) + } + + @Test + fun `parse config string with partial user info`() { + val configString = + "user.uuid=4d3f6ca7-d256-4f84-a6c6-099a26055d4c " + + "user.name=John\$SPACE#Doe wifi_attached=True" + + val result = parser.parse(configString) + + assertNull(result.userInfo) + assertTrue(result.isWiFiAttached) + assertFalse(result.isDeveloperMode) + } + + @Test + fun `parse config string with invalid UUID`() { + val configString = + "user.uuid=invalid-uuid user.name=John\$SPACE#Doe " + + "user.description=Test user.iconB64=eNpjYGBg" + + assertFailure { parser.parse(configString) } + .messageContains("invalid-uuid") + } + + @Test + fun `parse config string with multiple spaces between key-value pairs`() { + val configString = + "wifi_attached=True developer_mode=True " + + "user.uuid=4d3f6ca7-d256-4f84-a6c6-099a26055d4c" + + val result = parser.parse(configString) + + assertNull(result.userInfo) + assertTrue(result.isWiFiAttached) + assertTrue(result.isDeveloperMode) + } +} diff --git a/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BadgeManager.kt b/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BadgeManager.kt index 72187fde..dee6fad2 100644 --- a/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BadgeManager.kt +++ b/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BadgeManager.kt @@ -16,7 +16,7 @@ data class BadgePayload( /** * Convert the payload to a format the badge understands */ - fun toBadgeCommand(): String = "${if (debug) "debug:" else ""}$type:$meta:${payload}/r/n" + fun toBadgeCommand(): String = "${if (debug) "debug:" else ""}$type:$meta:${payload}" } interface BadgeManager { diff --git a/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BinaryManipulation.kt b/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BinaryManipulation.kt index 88b8ae2d..5281ead4 100644 --- a/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BinaryManipulation.kt +++ b/zeapp/badge/src/commonMain/kotlin/de/berlindroid/zekompanion/BinaryManipulation.kt @@ -195,7 +195,23 @@ fun IntBuffer.toBinary(): ByteBuffer { return output.toBuffer() } +/** + * Take a binary bytebuffer and convert it into an int buffer. + */ +fun ByteBuffer.fromBinaryToRGB(): IntBuffer { + val output = mutableListOf() + forEach { byte -> + for (index in 0 until 8) { + val bit = (byte.toInt() shr (7 - index)) and 1 + output.add(rgb(bit * 255, bit * 255, bit * 255)) + } + } + + return output.toBuffer() +} + private fun List.toBuffer(): ByteBuffer = ByteBuffer.wrap(toTypedArray().toByteArray()) +private fun List.toBuffer(): IntBuffer = IntBuffer.wrap(toTypedArray().toIntArray()) /** * Map all values of an IntBuffer diff --git a/zeapp/badge/src/commonTest/kotlin/de/berlindroid/zekompanion/BitmapTest.kt b/zeapp/badge/src/commonTest/kotlin/de/berlindroid/zekompanion/BitmapTest.kt index 93c75e7c..624bc8c8 100644 --- a/zeapp/badge/src/commonTest/kotlin/de/berlindroid/zekompanion/BitmapTest.kt +++ b/zeapp/badge/src/commonTest/kotlin/de/berlindroid/zekompanion/BitmapTest.kt @@ -10,6 +10,7 @@ import java.awt.image.BufferedImage import java.awt.image.BufferedImage.TYPE_INT_RGB import java.io.File import java.io.FileNotFoundException +import java.nio.ByteBuffer import java.nio.IntBuffer import javax.imageio.ImageIO @@ -84,6 +85,55 @@ class BitmapTest { assertThat(output).isEqualTo("eNpjYHBYIMHHcHCBV/19ABJOBBA=") } + + @Test + fun binaryBitMaskToRGBImageSmall() { + val w = rgb(255, 255, 255) + val b = rgb(0, 0, 0) + + val width = 4 + val height = 2 + + val input = IntBuffer.wrap( + intArrayOf( + w, b, w, b, + b, w, b, w, + ), + ) + + val bits = input.toBinary() + + val output = bits.fromBinaryToRGB() + + assertThat(input.toBufferedImage(width, height)).allPixelEqualTo(output.toBufferedImage(width, height)) + + } + + @Test + fun binaryBitMaskToRGBImageHuge() { + val w = rgb(255, 255, 255) + val b = rgb(0, 0, 0) + + val width = 8 + val height = 5 + + val input = IntBuffer.wrap( + intArrayOf( + b, b, b, b, b, b, b, b, b, + b, b, w, b, b, b, w, b, b, + b, b, w, w, b, w, w, b, b, + b, b, w, b, w, b, w, b, b, + b, b, w, b, b, b, w, b, b, + ), + ) + + val bits = input.toBinary() + + val output = bits.fromBinaryToRGB() + + assertThat(input.toBufferedImage(width, height)).allPixelEqualTo(output.toBufferedImage(width, height)) + + } } fun Assert.allPixelEqualTo(other: BufferedImage) = given { actual -> diff --git a/zeapp/gradle/libs.versions.toml b/zeapp/gradle/libs.versions.toml index f4f4e7ab..de89e03f 100644 --- a/zeapp/gradle/libs.versions.toml +++ b/zeapp/gradle/libs.versions.toml @@ -14,7 +14,7 @@ license-report-gradle = { id = "com.jaredsburrows.license", version.ref = "licen [versions] -android-gradle-plugin = "8.3.1" +android-gradle-plugin = "8.2.0" androidx-activity = "1.8.2" androidx-compose-bom = "2024.03.00" androidx-compose-compiler = "1.5.3" @@ -38,7 +38,7 @@ license-report-gradle = "0.9.3" material = "1.11.0" material3-wsc = "1.2.1" mik3y-usb-serial = "3.7.0" -retrofit = "2.9.0" +retrofit = "2.11.0" sl4j = "2.0.12" timber = "5.0.1" uiautomator = "2.3.0" @@ -48,7 +48,7 @@ junit4 = "4.13.2" junit4-ui = "1.6.4" ktor = "2.3.9" uiTestManifest = "1.6.4" -mockk = "1.13.8" +mockk = "1.13.11" kotlinxCoroutinesTest = "1.7.3" [libraries] @@ -91,6 +91,7 @@ material = { group = "com.google.android.material", name = "material", version.r material3-wsc = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material3-wsc" } mik3y-usb-serial-android = { module = "com.github.mik3y:usb-serial-for-android", version.ref = "mik3y-usb-serial" } retrofit2-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } +retrofit2-converter-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } retrofit2-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } zxing = { module = "com.google.zxing:core", version.ref = "zxing" } diff --git a/zeapp/profiles/.ignore b/zeapp/profiles/.ignore new file mode 100644 index 00000000..a20b29a7 --- /dev/null +++ b/zeapp/profiles/.ignore @@ -0,0 +1 @@ +please diff --git a/zeapp/server/build.gradle.kts b/zeapp/server/build.gradle.kts index 82feccd8..b497c249 100644 --- a/zeapp/server/build.gradle.kts +++ b/zeapp/server/build.gradle.kts @@ -23,6 +23,8 @@ dependencies { implementation(libs.ktor.content.serialization) implementation(libs.kotlinx.serialization.json) implementation(libs.sl4j.simple) + implementation(libs.retrofit2.retrofit) + implementation(libs.retrofit2.converter.serialization) } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt index fd2ea587..2415b9fd 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/Main.kt @@ -2,26 +2,68 @@ package de.berlindroid.zekompanion.server -import de.berlindroid.zekompanion.server.routers.imageBin -import de.berlindroid.zekompanion.server.routers.imagePng -import de.berlindroid.zekompanion.server.routers.index +import de.berlindroid.zekompanion.server.ai.AI +import de.berlindroid.zekompanion.server.routers.* +import de.berlindroid.zekompanion.server.user.UserRepository +import de.berlindroid.zekompanion.server.zepass.ZePassRepository import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.http.content.* import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.response.* import io.ktor.server.routing.* -import io.ktor.server.tomcat.Tomcat +import io.ktor.server.tomcat.* import java.io.File import java.security.KeyStore -private const val LOCAL_HTTP_PORT = 8000 -private const val LOCAL_TLS_PORT = 8443 private const val SSL_PASSWORD_ENV = "SSL_CERTIFICATE_PASSWORD" private const val KEYSTORE_RESOURCE_FILE = "/tmp/keystore.jks" +fun main() { + val users = UserRepository.load() + val zepass = ZePassRepository.load() -fun main(args: Array) { + embeddedBadgeServer(users, zepass) + .start(wait = false) + + embeddedWebServer(users, zepass) + .start(wait = true) +} + +private fun embeddedBadgeServer( + users: UserRepository, + zepass: ZePassRepository, +): TomcatApplicationEngine { + return embeddedServer( + Tomcat, + environment = applicationEngineEnvironment { + connector { + port = 1337 + } + + module { + install(ContentNegotiation) { + json() + } + + routing { + get("/") { + call.respondText("yes") + } + + postPost(zepass, users) + getOptimizedPosts(zepass, users) + } + } + }, + ) +} + +private fun embeddedWebServer( + users: UserRepository, + zepass: ZePassRepository, +): TomcatApplicationEngine { val keyPassword = try { System.getenv(SSL_PASSWORD_ENV) } catch (e: Exception) { @@ -29,10 +71,9 @@ fun main(args: Array) { } val keyStore: KeyStore? = loadKeyStore(keyPassword) - val serverPort = extractServerPort(args, keyStore) - println("Serving on port $serverPort.") + val ai = AI() - embeddedServer( + return embeddedServer( Tomcat, environment = applicationEngineEnvironment { injectTLSIfNeeded(keyStore, keyPassword) @@ -45,14 +86,31 @@ fun main(args: Array) { routing { staticResources("/", "static") { index() + exclude { file -> + file.path.endsWith("db") + } } imageBin() imagePng() + + // Callable from ZeFlasher only? + adminCreateUser(users, ai) + adminListUsers(users) + adminDeleteUser(users) + + // TODO: Check if callable from ZeBadge (no ssl) + updateUser(users) + getUser(users) + getUserProfileImageBinary(users) + getUserProfileImagePng(users) + + postPost(zepass, users) + getPosts(zepass) } } }, - ).start(wait = true) + ) } private fun ApplicationEngineEnvironmentBuilder.injectTLSIfNeeded(keyStore: KeyStore?, keyPassword: String?) { @@ -68,19 +126,6 @@ private fun ApplicationEngineEnvironmentBuilder.injectTLSIfNeeded(keyStore: KeyS } } -private fun extractServerPort(args: Array, keyStore: KeyStore?): Int { - val serverPort = if (args.isNotEmpty()) { - args.first().toInt() - } else { - if (keyStore != null) { - LOCAL_TLS_PORT - } else { - LOCAL_HTTP_PORT - } - } - return serverPort -} - private fun loadKeyStore(keyPassword: String?): KeyStore? { var keyStore: KeyStore? = null diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt new file mode 100644 index 00000000..0d903150 --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/AI.kt @@ -0,0 +1,98 @@ +package de.berlindroid.zekompanion.server.ai + +import de.berlindroid.zekompanion.base64 +import de.berlindroid.zekompanion.ditherFloydSteinberg +import de.berlindroid.zekompanion.server.ext.ImageExt.toPixels +import de.berlindroid.zekompanion.toBinary +import de.berlindroid.zekompanion.zipit +import java.awt.Image +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.ByteBuffer +import javax.imageio.ImageIO + +const val USER_PROFILE_PICTURE_SIZE = 32 + +private fun rescaleImage( + image: BufferedImage, + targetWidth: Int = USER_PROFILE_PICTURE_SIZE, + targetHeight: Int = USER_PROFILE_PICTURE_SIZE, +): BufferedImage { + val scaledImage = image.getScaledInstance(targetWidth, targetHeight, Image.SCALE_SMOOTH) + val destinationImage = BufferedImage(targetWidth, targetHeight, image.type) + val graphics = destinationImage.createGraphics() + graphics.drawImage(scaledImage, 0, 0, null) + graphics.dispose() + return destinationImage +} + +class AI( + private val gemini: Gemini = Gemini(), + private val dale: Dalle = Dalle(), + private val firstNames: MutableList = mutableListOf(), + private val lastNames: MutableList = mutableListOf(), + private val prefixes: List = listOf( + "Al", + "Bint", + "Ibn", + "Mac", + "Mc", + "Nic", + "Ní", + "da ", + "de ", + "di ", + "le ", + "van ", + "von ", + ), +) { + init { + val names = this.javaClass.classLoader.getResource("names.txt")?.readText() + names?.lines()?.forEachIndexed { index, name -> + try { + val (first, last) = name.split(" ") + if (first !in firstNames) { + firstNames += first + } + + if (last !in lastNames) { + lastNames += last + } + } catch (e: IndexOutOfBoundsException) { + println("Couldn't split name '${name}' at index ${index}. Ignoring it.") + } + } + } + + suspend fun createUserName(): String = "${firstNames.random()} ${createMaybeRandomPrefix()}${lastNames.random()}" + + private fun createMaybeRandomPrefix(): String = when (Math.random()) { + in 0.95..1.0 -> prefixes.random() + else -> "" + } + + suspend fun createUserDescription(name: String): String = gemini.getDescription(name) + suspend fun createUserChatPhrase(name: String, description: String): String = gemini.getChatPhrase(name, description) + + suspend fun createUserProfileImages(uuid: String, name: String, description: String): String? { + val image = dale.requestImageGeneration( + name = name, + description = description, + ) + + return if (image != null) { + ImageIO.write(image, "png", File("./profiles/${uuid}.png")) + + rescaleImage(image) + .toPixels() + .ditherFloydSteinberg(USER_PROFILE_PICTURE_SIZE, USER_PROFILE_PICTURE_SIZE) + .toBinary() + .zipit() + .base64() + } else { + null + } + } +} diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt new file mode 100644 index 00000000..fb4078fe --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Dalle.kt @@ -0,0 +1,99 @@ +package de.berlindroid.zekompanion.server.ai + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.MediaType +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST +import java.awt.image.BufferedImage +import java.net.URL +import java.util.concurrent.TimeUnit +import javax.imageio.ImageIO + +private const val OPENAI_TOKEN_ENV = "DALE_AUTH_TOKEN" + + +@Serializable +data class ImageRequest( + @SerialName("n") val imageCount: Int = 1, + val size: String = "256x256", + val model: String, + val prompt: String, +) + +@Serializable +data class GeneratedImages( + val created: Int, + val data: List, +) { + @Serializable + data class ImageLocation( + val url: String, + ) +} + +interface OpenAIService { + @POST("/v1/images/generations") + suspend fun generateImage( + @Header("Content-Type") contentType: String = "application/json", + @Header("Authorization") authorization: String, + @Body request: ImageRequest, + ): GeneratedImages +} + +private const val TIMEOUT: Long = 90 + +class Dalle( + private val json: Json = Json { ignoreUnknownKeys = true }, + private val service: OpenAIService = Retrofit.Builder() + .baseUrl("https://api.openai.com/") + .addConverterFactory( + json.asConverterFactory( + MediaType.parse("application/json; charset=UTF8")!!, + ), + ) + .client( + OkHttpClient().newBuilder() + .callTimeout(TIMEOUT, TimeUnit.SECONDS) + .readTimeout(TIMEOUT, TimeUnit.SECONDS) + .writeTimeout(TIMEOUT, TimeUnit.SECONDS) + .connectTimeout(TIMEOUT, TimeUnit.SECONDS) + .build(), + ) + .build() + .create(OpenAIService::class.java), + + private val token: String = System.getenv(OPENAI_TOKEN_ENV) ?: ("" + println("OAI token not found!")), +) { + suspend fun requestImageGeneration( + name: String, + description: String, + ): BufferedImage? { + try { + val maybeImages = service.generateImage( + request = ImageRequest( + model = "dall-e-3", + prompt = "Please create a profile picture for a fantastical school book showing \"${name}\". " + + "The picture should be a decorated wooden frame hanging on a blank wall " + + "and show $name in a representative pose, doing their favorite thing. " + + "$description.", + ), + authorization = "Bearer $token", + ) + + val location = maybeImages.data.firstOrNull() ?: return null + println("Avatar of '$name' generated at ${location.url}.") + + return ImageIO.read(URL(location.url)) + } catch (e: Exception) { + e.printStackTrace() + println("Could not generate image!") + return null + } + } +} diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt new file mode 100644 index 00000000..99679378 --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ai/Gemini.kt @@ -0,0 +1,134 @@ +package de.berlindroid.zekompanion.server.ai + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.MediaType +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Query + +private const val AI_TOKEN_ENV = "AI_AUTH_TOKEN" + +@Serializable +data class PromptBody( + val systemInstruction: Content, + val contents: List, +) { + @Serializable + data class Content( + val parts: List, + ) { + @Serializable + data class Part( + val text: String, + ) + } +} + +@Serializable +data class PromptResponse( + val candidates: List, +) { + @Serializable + data class Candidate( + val content: Content, + val finishReason: String, + val index: Int, + ) { + @Serializable + data class Content( + val parts: List, + val role: String, + ) { + @Serializable + data class Part( + val text: String, + ) + } + } +} + +interface SimpleGeminiService { + @POST("v1beta/models/gemini-1.5-flash:generateContent") + suspend fun prompt( + @Query("key") key: String, + @Body prompt: PromptBody, + ): PromptResponse +} + + +class Gemini( + private val json: Json = Json { ignoreUnknownKeys = true }, + private val service: SimpleGeminiService = Retrofit.Builder() + .baseUrl("https://generativelanguage.googleapis.com/") + .addConverterFactory( + json.asConverterFactory( + MediaType.parse("application/json; charset=UTF8")!!, + ), + ) + .build() + .create(SimpleGeminiService::class.java), + private val token: String = System.getenv(AI_TOKEN_ENV) ?: "", +) { + + suspend fun getDescription(name: String): String { + return geminiIt( + systemInstruction = "You are a dungeons and dragons dungeon master, assembling a new dnd campaign " + + "at the Droidcon in Berlin conference in 2024. You'll answer following questions with a " + + "description of a character's background story. Their all Android developers, either " + + "excited new comers or old hands with years of experience. Please keep it brief, " + + "interesting and quirky.", + prompts = listOf( + "Please give \"${name}\" a background story of their character.", + ), + ) + } + + suspend fun getChatPhrase(name: String, description: String): String { + return geminiIt( + systemInstruction = "You are a one word wonder. Answer the following prompts with one word.", + prompts = listOf( + "Hey, can you create a one word catchphrase for $name who is described as $description.", + ), + ) + } + + private suspend fun geminiIt(prompts: List, systemInstruction: String = ""): String { + try { + val response = service.prompt( + token, + PromptBody( + systemInstruction = + PromptBody.Content( + parts = listOf( + PromptBody.Content.Part( + text = systemInstruction, + ), + ), + ), + contents = listOf( + PromptBody.Content( + parts = prompts.map { + PromptBody.Content.Part( + text = it, + ) + }, + ), + ), + ), + ) + + return response.candidates.joinToString(separator = ",") { candidate -> + candidate.content.parts.joinToString { part -> + part.text.replace(Regex("[_*#\n]"), "").trim() + } + } + } catch (e: Exception) { + e.printStackTrace() + print("Couldn't gemini!") + return "" + } + } +} diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ext/ImageExt.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ext/ImageExt.kt index 957a210b..6b4a1105 100644 --- a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ext/ImageExt.kt +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/ext/ImageExt.kt @@ -15,7 +15,7 @@ import java.nio.IntBuffer import javax.imageio.ImageIO object ImageExt { - private fun BufferedImage.toPixels(): IntBuffer { + fun BufferedImage.toPixels(): IntBuffer { val output = IntBuffer.allocate(width * height) getRGB(0, 0, width, height, output.array(), 0, width) return output diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt new file mode 100644 index 00000000..ba0ac60a --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/Helper.kt @@ -0,0 +1,39 @@ +package de.berlindroid.zekompanion.server.routers + +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call +import io.ktor.server.request.header +import io.ktor.server.response.respondText +import io.ktor.util.pipeline.PipelineContext + +private const val AUTH_TOKEN_ENV = "ZESERVER_AUTH_TOKEN" +private const val AUTH_TOKEN_HEADER = "ZeAuth" + +suspend fun PipelineContext.withParameter( + key: String, + block: suspend PipelineContext.(value: String) -> Unit, +) { + if (call.parameters.contains(key)) { + val value = call.parameters[key] + if (value != null) { + block(value) + } + } +} + +suspend fun PipelineContext.ifAuthorized(block: suspend PipelineContext.() -> Unit) { + val authHeader = call.request.header(AUTH_TOKEN_HEADER) + val authEnv = System.getenv(AUTH_TOKEN_ENV) + + if (authEnv.isNullOrBlank()) { + println("Auth env ('${AUTH_TOKEN_ENV}') environment var is 'null' or empty, you will not be able to do admin level tasks. " + + "Set the env var and restart the server.") + } + + if (authEnv.isNullOrBlank() || authHeader == null || authEnv != authHeader) { + call.respondText(status = HttpStatusCode.Forbidden, text = "Forbidden") + } else { + block() + } +} diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt new file mode 100644 index 00000000..3b5e8398 --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/UserRouter.kt @@ -0,0 +1,148 @@ +package de.berlindroid.zekompanion.server.routers + +import de.berlindroid.zekompanion.server.ai.AI +import de.berlindroid.zekompanion.server.user.User +import de.berlindroid.zekompanion.server.user.UserRepository +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.io.File +import java.util.* + + +fun Route.adminCreateUser(users: UserRepository, ai: AI) = + post("/api/user") { + runCatching { + ifAuthorized { + val uuid = UUID.randomUUID().toString() + val name = ai.createUserName() + val description = ai.createUserDescription(name) + val chatPhrase = ai.createUserChatPhrase(name, description) + + val b64 = ai.createUserProfileImages(uuid, name, description) + + val user = User( + uuid = uuid, + name = name, + description = description, + profileB64 = b64, + chatPhrase = chatPhrase, + ) + + val uuidAdded = users.createUser(user) + + if (uuidAdded != null) { + call.respond(status = HttpStatusCode.Created, users.getUser(uuidAdded)!!) + } else { + call.respondText("invalid", status = HttpStatusCode.Forbidden) + } + + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}.") + } + } + +fun Route.adminListUsers(users: UserRepository) = + get("/api/user") { + runCatching { + ifAuthorized { + call.respond(status = HttpStatusCode.OK, users.getUsers()) + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.adminDeleteUser(users: UserRepository) = + delete("/api/user/{UUID}") { + runCatching { + ifAuthorized { + withParameter("UUID") { uuid -> + call.respond(status = HttpStatusCode.OK, users.deleteUser(uuid)) + } + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.updateUser(users: UserRepository) = + put("/api/user/{UUID}") { + runCatching { + withParameter("UUID") { uuid -> + val newUser = call.receiveNullable() ?: throw IllegalArgumentException("No user payload found.") + val userUpdated = users.updateUser(newUser.copy(uuid = uuid)) + + if (userUpdated) { + call.respondText(text = "OK") + } else { + call.respondText("invalid", status = HttpStatusCode.NotAcceptable) + } + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}", status = HttpStatusCode.NotAcceptable) + } + } + +fun Route.getUser(users: UserRepository) = + get("/api/user/{UUID}") { + runCatching { + withParameter("UUID") { uuid -> + val user = users.getUser(uuid) + if (user != null) { + call.respond(status = HttpStatusCode.OK, user) + } else { + call.respondText(status = HttpStatusCode.NotFound, text = "Not Found.") + } + } + call.respondText(status = HttpStatusCode.UnprocessableEntity, text = "No UUID.") + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.getUserProfileImagePng(users: UserRepository) = + get("/api/user/{UUID}/png") { + runCatching { + withParameter("UUID") { uuid -> + val user = users.getUser(uuid) + if (user != null) { + call.respondFile( + File("./profiles/${uuid}.png"), + ) + } else { + call.respondText(status = HttpStatusCode.NotFound, text = "Not Found.") + } + } + call.respondText(status = HttpStatusCode.UnprocessableEntity, text = "No UUID.") + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.getUserProfileImageBinary(users: UserRepository) = + get("/api/user/{UUID}/b64") { + runCatching { + withParameter("UUID") { uuid -> + val user = users.getUser(uuid) + if (user != null) { + call.respondText { user.profileB64 ?: "" } + } else { + call.respondText(status = HttpStatusCode.NotFound, text = "Not Found.") + } + } + call.respondText(status = HttpStatusCode.UnprocessableEntity, text = "No UUID.") + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt new file mode 100644 index 00000000..d6441aa2 --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/routers/ZePassRouter.kt @@ -0,0 +1,57 @@ +package de.berlindroid.zekompanion.server.routers + +import de.berlindroid.zekompanion.server.user.UserRepository +import de.berlindroid.zekompanion.server.zepass.Post +import de.berlindroid.zekompanion.server.zepass.ZePassRepository +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.util.* + + +fun Route.postPost(zepass: ZePassRepository, users: UserRepository) = + post("/api/zepass") { + runCatching { + val uuid = call.receive() + val user = users.getUser(uuid) + if (user != null) { + val postUUID = UUID.randomUUID().toString() + zepass.newPost( + Post( + uuid = postUUID, + posterUUID = uuid, + message = user.chatPhrase ?: "", + ), + ) + + call.respond(status = HttpStatusCode.OK, postUUID) + } else { + call.respondText(status = HttpStatusCode.Unauthorized, text = "Nope.") + } + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.getPosts(zepass: ZePassRepository) = + get("/api/zepass") { + runCatching { + call.respond(zepass.getPosts()) + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } + +fun Route.getOptimizedPosts(zepass: ZePassRepository, users: UserRepository) = + get("/api/zepass") { + runCatching { + call.respond(zepass.getOptimizedPosts(users)) + }.onFailure { + it.printStackTrace() + call.respondText("Error: ${it.message}") + } + } diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt new file mode 100644 index 00000000..e8f45dfa --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/user/UserRepository.kt @@ -0,0 +1,92 @@ +package de.berlindroid.zekompanion.server.user + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File +import java.io.FileNotFoundException + +private const val DB_FILENAME = "./user.db" + +@Serializable +data class User( + val uuid: String, + val name: String, + val description: String, + val profileB64: String?, + val chatPhrase: String?, +) + +class UserRepository( + private val users: MutableList = mutableListOf(), +) { + companion object { + fun load(): UserRepository = try { + val users = File(DB_FILENAME).readText() + + try { + UserRepository( + users = Json.decodeFromString(users), + ) + } catch (e: SerializationException) { + println("Couldn't read users file. Creating a new one.") + UserRepository() + } + } catch (notFound: FileNotFoundException) { + UserRepository() + } + + fun save(repo: UserRepository) = File(DB_FILENAME).writer().use { + it.write(Json.encodeToString(repo.users)) + } + } + + fun createUser(user: User): String? { + val existingUser = users.find { it.uuid == user.uuid } + if (existingUser != null) { + println("User '${user.uuid}' already exists.") + return null + } + + users.add(user) + + save(this) + + return user.uuid + } + + fun getUser(uuid: String): User? { + return users.find { it.uuid == uuid } + } + + fun getUsers(): List { + return users.toList() + } + + fun updateUser(newUser: User): Boolean { + val index = users.indexOfFirst { it.uuid == newUser.uuid } + if (index < 0) { + return false + } + + users[index] = newUser + + save(this) + + return true + } + + fun deleteUser(uuid: String): Boolean { + val user = users.find { it.uuid == uuid } + if (user == null) { + return false + } + + users.remove(user) + + save(this) + + return true + } +} diff --git a/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt new file mode 100644 index 00000000..caae3dac --- /dev/null +++ b/zeapp/server/src/main/kotlin/de/berlindroid/zekompanion/server/zepass/ZePassRepository.kt @@ -0,0 +1,62 @@ +package de.berlindroid.zekompanion.server.zepass + +import de.berlindroid.zekompanion.server.user.UserRepository +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File +import java.io.FileNotFoundException + +private const val POSTS_FILENAME = "./zepass.db" + +@Serializable +data class Post( + val uuid: String, + val posterUUID: String, + val message: String, +) + +@Serializable +data class OptimizedPosts( + val message: String, + val profileB64: String?, +) + +class ZePassRepository private constructor( + private val posts: MutableList = mutableListOf(), +) { + companion object { + fun load(): ZePassRepository = try { + ZePassRepository( + posts = Json.decodeFromString(File(POSTS_FILENAME).readText()), + ) + } catch (notFound: FileNotFoundException) { + println("Couldn't find '$POSTS_FILENAME'.") + ZePassRepository() + } + + fun save(repo: ZePassRepository) = File(POSTS_FILENAME).writer().use { + it.write(Json.encodeToString(repo.posts)) + } + } + + fun newPost(post: Post): String { + posts.add(post) + save(this) + + return post.uuid + } + + fun getPosts(): List { + return posts.toList() + } + + fun getOptimizedPosts(users: UserRepository): List { + return posts.map { + OptimizedPosts( + message = it.message, + profileB64 = users.getUser(it.posterUUID)?.profileB64, + ) + } + } +} diff --git a/zeapp/server/src/main/resources/names.txt b/zeapp/server/src/main/resources/names.txt new file mode 100644 index 00000000..21f1c2e9 --- /dev/null +++ b/zeapp/server/src/main/resources/names.txt @@ -0,0 +1,1012 @@ +Abby Howell +Abdullah Hendricks +Abdullah O’Donnell +Abram Hobbs +Abram Russell +Ada Kirk +Adalee Hahn +Adalee Moyer +Adaline Bond +Adaline Wood +Adalyn Bruce +Adalyn Carey +Adalynn Baker +Adalynn York +Adam Jacobson +Adan Berry +Adan Golden +Adan Porter +Addilyn Villanueva +Addisyn Conley +Adelyn Herrera +Adelynn Banks +Aden Green +Adley Case +Adley Jarvis +Adrianna Esparza +Ahmad Boone +Ahmed Green +Ahmir Lam +Ailani Fry +Ailani Rangel +Ainhoa Scott +Ainsley Reynolds +Aislinn Velasquez +Aitana Weiss +Alaia Peterson +Alan Santana +Alani Wolfe +Alaya Massey +Albert Maynard +Albert McFarland +Aleena Snow +Alena Melton +Alessandro Figueroa +Alessia Galindo +Alexandria Davenport +Alexia Mathews +Alfred Hurst +Ali Reese +Ali Sandoval +Alia Walls +Alianna McMahon +Alice Arellano +Alicia Solis +Alina Spence +Alisen Erdogan +Alison Hayes +Alisson Atkinson +Aliyah Gibson +Allison Francis +Alora Beard +Alora Melendez +Alvin Patterson +Amara Boyle +Amari Bernal +Amari Ibarra +Amelia Cruz +Amelie Jackson +Amina Delacruz +Amirah Castaneda +Amoura Ward +Amy Alfaro +Ander Foley +Ander Franco +Andi Garrison +Andre Richardson +Andrea Camacho +Andrea Huerta +Andrew Medrano +Angel Parsons +Angelo Underwood +Anika Mata +Anika Wise +Aniya Shepherd +Anna Bentley +Anna Sherman +Annabelle Burnett +Annika Cobb +Apollo Greene +Arabella Dixon +Ari Barker +Ari Frye +Ariah Montes +Ariana Hurley +Arianna Valenzuela +Ariel Garza +Ariella Duran +Aries Carey +Ariya Luna +Ariyah McCoy +Ariyah Salazar +Arjun Hancock +Arjun Ortiz +Artemis Pitts +Asa Ayers +Aspyn Norman +Atlas Booth +Atreus Briggs +Atreus Keith +Atreus Rice +Atticus Dickson +Aubree Becker +Aubree Lee +Aubrie Atkins +Aubrie Roberts +Aubriella Robertson +Aubriella Sparks +Audrey Roberson +August Patton +Augustus Lowe +Aurora Rice +Aurora Romero +Austin Bernal +Avi Stark +Axton Novak +Ayaan Sharp +Aydin White +Ayla Morgan +Aylin Sellers +Azariah Hubbard +Azariah Melton +Aziel Mahoney +Barbara Taylor +Baylor McCoy +Beatrice Miles +Beatrice Wood +Beckett Mathews +Bella Bodemann +Bellamy McCormick +Bellamy Robinson +Benjamin Porter +Bentlee Moses +Berkley Osborne +Bishop McDowell +Bishop Vaughan +Bobby Lee +Bode Jordan +Bonnie Browning +Bradley Stark +Braelyn Montes +Braxton Allen +Braxton Mayer +Braylee Goodwin +Braylee Simpson +Brendan Vang +Bria Watkins +Brianna Lester +Briar Kemp +Bridger Hull +Bridget Glover +Briggs Potts +Brinley Cole +Bristol Knight +Bristol Nichols +Brixton Underwood +Brody Reyna +Bruce Escobar +Bryce Powell +Brylee Hull +Brynn Sharp +Bryson Stevens +Caleb Whitney +Callahan Sherman +Calum Long +Camden Blair +Cameron Stout +Camryn Ahmed +Cannon Daniel +Carl Pierce +Carly Brandt +Carly Figueroa +Carly Marquez +Carmelo Poole +Carolina Doyle +Carolina Miranda +Caroline Valdez +Carolyn Colon +Carolyn Sosa +Carson Hayde +Carson Rubio +Carson Vargas +Carter Bennett +Case Barrera +Case McPherson +Casen Garcia +Casen Hicks +Casen Jefferson +Cason Archer +Cason Avery +Cassandra Gross +Cassandra Liu +Castiel Avalos +Cataleya Williamson +Catherine Graham +Catherine Moody +Cayden Schmitt +Cedric Blanchard +Celeste Gilmore +Celine Campbell +Cesar McPherson +Charleigh Myers +Charles Callahan +Charlotte Bates +Chaya Long +Christian Roberts +Christina Maddox +Christopher Richmond +Christopher Singh +Cillian Hartman +Cketti Cketti +Claire Ruiz +Clara Tran +Clare Garza +Clay House +Clay Vo +Cleo Combs +Cleo Mejia +Coen Rosario +Cohen Hodges +Cohen Meza +Cole Hurst +Colin Moran +Colin Snow +Collin McCarthy +Collin Waters +Collins Randall +Collins Randolph +Colt Hodges +Conner Gibbs +Connor Murillo +Conrad Bernal +Conrad Rhodes +Cooper Hawkins +Cooper Pittman +Cora Paul +Crew Ibarra +Cruz Lawrence +Cullen Allen +Cullen Boone +Daisy Curry +Dalary Santana +Dalary Santana +Daleyza Bruce +Damian Booth +Damian Leal +Damian Sloan +Damien Pratt +Damir Scott +Damir Walters +Dane Woodard +Dani Greer +Dariel Ross +Dariel Vang +Dario Hahn +Darius Mills +Darren Flowers +Darren Glass +Darren Wallace +Dash Palmer +David Macias +Davina Hebert +Davis Moyer +Deandre Hardin +Deborah Garner +Delilah Mueller +Denise Gregory +Dennis Richard +Denver Waters +Destiny Stevens +Dillon Reyes +Dima Dima +Dominic Woodward +Dominik Woodard +Donald Wu +Donovan Glover +Donovan Johns +Dorothy Floyd +Douglas Mora +Drake Park +Drew Carey +Drew Hutchinson +Duke Garza +Duncan Ward +Duncan Watson +Dustin Henderson +Easton McKee +Easton Pitts +Edith Arellano +Edith Zavala +Eduardo Henson +Edward Pratt +Eithan Espinoza +Eithan Keith +Eithan Nichols +Eli Humphrey +Elisa Garcia +Eliseo Alvarez +Eliseo Delacruz +Eliza Mosley +Elle Olson +Ellen Waters +Elliot Lopez +Elliott Blackwell +Elliott Merritt +Ellis McBride +Ellis Roth +Ellison Hensley +Elsa Carroll +Elsie Bradford +Elyse Gibson +Elyse Pugh +Elyse Shepherd +Ember Macias +Ember Vazquez +Emberly Jarvis +Emely Dillon +Emerson Martinez +Emersyn Novak +Emiliano Dominguez +Emir Russell +Emmaline Gilmore +Emmaline Hayden +Emmalynn Weiss +Emmeline Rojas +Emmeline Stark +Emmett Coleman +Emmett Davenport +Emmy Stokes +Ensley Barnes +Ensley Mahoney +Erick Parsons +Erick Walter +Erik Byrd +Erik Kent +Erin Kelly +Esperanza Gill +Eugene Alvarez +Eugene Barry +Eve Montoya +Eve Underwood +Evelynn Nicholson +Evie Boyer +Ezra Villegas +Fabian Bradley +Faith Leonard +Fallon Cervantes +Faye Dyer +Felipe Aguirre +Fernando Hobbs +Fernando Wade +Finley Chang +Finley Cordova +Florence Reed +Ford Quinn +Forrest Daniels +Frances Barnes +Frances Finley +Francesca Morrison +Francisco Barry +Franco Long +Frankie Marquez +Franklin Greer +Frederick Jones +Freya Santana +Freyja Montes +Frida Nguyen +Gabriel Salinas +Gabriel Tran +Galilea Carter +Georgia Vazquez +Gia Calderon +Gianna Kennedy +Giavanna Olson +Giovanna Russo +Giovanni Singh +Giselle Garza +Giselle Goodwin +Giuliana Goodman +Giuliana Underwood +Gloria Matthews +Gracelyn Kemp +Gracelyn Terrell +Gracelynn Roth +Gracie Maldonado +Graham Daniels +Graysen Burch +Graysen Reyna +Gregory Randall +Grey Bernard +Grey Murray +Greyson Caldwell +Greyson Carpenter +Guillermo Ramos +Gunnar Maynard +Gunnar Wood +Hadassah Davenport +Hadassah Stokes +Hailey McIntyre +Haley Figueroa +Halle Carrillo +Halo Hendricks +Hamza Kirk +Harlan Ortiz +Harlow O’brien +Harmony Butler +Harper Walker +Harry Robertson +Harry Shaw +Harvey Juarez +Hasan Hosgel +Hattie Mercado +Hattie Zuniga +Hayden Conway +Heath Moon +Heath Pearson +Heaven Terrell +Holland Morse +Hope Huang +Houston Arroyo +Hunter Fuentes +Huxley Larsen +Huxley Quintero +Isaac Horton +Isaias Benson +Ismael Barr +Ismael Cunningham +Ivy Burns +Ivy Kirby +Jace Calhoun +Jace Charles +Jack Donovan +Jack Esquivel +Jackson Parra +Jacob Miranda +Jacoby Everett +Jacoby Monroe +Jacqueline Ford +Jad Barker +Jade Gardner +Jade McClure +Jaden Byrd +Jaelynn Drake +Jake House +Jakob Anthony +Jakob Golden +Jalen Cervantes +Jalen Pitts +Jamal Michael +Jamari Quintero +James Bond +James Webster +Jameson Barrett +Jamie Dawson +Jamir Ortega +Jane Greer +Janelle Hoover +Jared Moses +Jasiah Garner +Javad Javad +Javier Fuentes +Jaxen Owen +Jaxen Page +Jaylee Duarte +Jayson Edwards +Jaziel Chan +Jazlyn Ali +Jazmine Garrison +Jeffrey Rojas +Jemma Vaughan +Jenna Koch +Jenna Vance +Jennifer Smith +Jeremy Barry +Jesse Ashley +Jesse Roy +Jesse Simon +Jessie McCann +Jett Randolph +Jimena Sherman +Jimmy Lam +Joel Cohen +Joel Palacios +Johan Mahoney +Jon Baxter +Jordan May +Jordyn Grant +Jordyn Novak +Josiah Brewer +Journee Lindsey +Journi Simpson +Jovanni Pruitt +Joy Sherman +Judah Cabrera +Judah Goodman +Judah Lane +Judith Hood +Judson James +Judson Monroe +Julia Copeland +Julia Pacheco +Julian Leblanc +Julian Rhodes +Julien Hancock +Juliet Hudson +Julieta Jefferson +June Bell +June Vang +Junior Pacheco +Juniper Branch +Justice Bowman +Justice Sellers +Justin Crosby +Kabir Pacheco +Kade Daugherty +Kadence Booker +Kai Dominguez +Kailey Coffey +Kaisley Austin +Kaison Cisneros +Kaison Suarez +Kaiya Le +Kaiya Wolfe +Kalani Weber +Kali Cox +Kamari Bullock +Kamari Goodman +Kamilah Bass +Kamilah Davila +Kamiyah Watson +Kamryn Allen +Kane Guerra +Karina Ibarra +Karina Owen +Karsyn Page +Karter Charles +Karter Morrow +Kase Cline +Kashton Padilla +Katelyn Benton +Katelyn Cruz +Katherine Watkins +Kaylee Lucero +Kayleigh Callahan +Kaylie Munoz +Kayson Phelps +Keenan Guerra +Kehlani Castaneda +Keily McGee +Keily Mendoza +Kellan Boyd +Kellan Osborne +Kelly Hernandez +Kelly Watkins +Kelsey Wang +Kendall Tran +Kennedi Morrison +Kensley McDaniel +Kenzie Pena +Kenzo Zimmerman +Keyla Hurley +Keyla McKenzie +Khalani Rosas +Khari Gentry +Kian Chen +Kiara Richmond +Kiara Villanueva +Kimber Underwood +Kimberly Summers +Kinslee Chapman +Kira Graves +Knox Rogers +Koa Jennings +Koa Kennedy +Kobe Koch +Koda Hanson +Koda Navarro +Kody Stafford +Kora Duran +Korbin Vaughan +Korbyn Pennington +Kori Winters +Kristopher George +Kyle Ellison +Kyler Grimes +Kylie Morrow +Kynlee Leach +Kyra Choi +Kyree Keith +Kyree McGee +Kyree Pennington +Kyree Spence +Lacey Matthews +Lacey Shaw +Lainey Randolph +Lainey Santos +Lana Pollard +Landen Delgado +Landry Pugh +Laney Carter +Lara Cross +Lara Joseph +Larry Marshall +Lauren Harvey +Lauren Melendez +Lawrence Boyer +Lawrence Mendoza +Lawson Casey +Lawson Newman +Layla Love +Layla Stanley +Layne Gaines +Layne Tate +Leah Guerrero +Leandro Benson +Lee Chung +Legend Mann +Leilani Avalos +Leilani Vance +Lennon Ball +Lennon Hensley +Leo Lynn +Leon Espinosa +Leonardo Lim +Leroy Espinosa +Lewis Gallegos +Lewis Gregory +Lia Farley +Liam Santiago +Liana Gregory +Lilah Patton +Lilian Lu +Lilith Castillo +Lillian Ballard +Lilly Phan +Lina Hopkins +Lina Parker +Lina Wade +Lorelai Villa +Lorelai Zhang +Lorelei Long +Louis Lyons +Louis Novak +Louis Tsai +Louisa Briggs +Lucia Beck +Lucille Dyer +Lucille Lee +Lucy McCann +Luella Charles +Luella Hudson +Luis Beck +Luke Bradford +Luke Richards +Lyanna Rose +Lyric Baxter +Mabel Barnes +Mac Roach +Macie Blevins +Mack Orozco +Macy Thompson +Madalynn Singh +Madden Graves +Madden Stephens +Maddison Luna +Madeleine Bernal +Madeleine Hart +Madelyn Daugherty +Madilynn Hess +Madilynn McMahon +Madisyn Kelly +Madisyn Serrano +Magdalena Dean +Maggie Phan +Maia Rojas +Maisie Villa +Maison Carter +Maison Knight +Major Blackburn +Makai White +Malachi Espinoza +Malachi Harmon +Malakai Ellis +Malani Parsons +Malaya Carroll +Maleah Vance +Manuel Chan +Marcus Arellano +Maren Miller +Maria McClure +Mariam English +Mariam Griffith +Mariam Wood +Mariana Garcia +Marie Drake +Marie Richard +Mario Bodemann +Marley Fuller +Marlon Gillespie +Marlon Guevara +Martin Young +Marvin Norman +Mason Bravo +Mathew Monroe +Matthew Holt +Matthias Geisler +Matthias Logan +Matthias Marshall +Maverick Duffy +Maverick O’Donnell +Mavis Sutton +Maximilian Kelley +Maximus Graham +Maximus Phan +Maxwell Wood +Mckenzie Ortega +Meghan Fry +Meghan Patel +Melany Todd +Melvin Gardner +Memphis Rangel +Mercy Bernal +Mercy Rivera +Mikaela Cole +Mikayla Bonilla +Mikayla Neal +Milan Lloyd +Milana Barrera +Milana Fleming +Milani Robles +Millie Esquivel +Milos Marinkovic +Mina Anderson +Miro Miro +Mitchell Santana +Mohamed Donovan +Mohamed Madden +Mohamed Silva +Mohammad Dennis +Moises Cruz +Mordechai Kaur +Moshe Atkins +Muhammad Boone +Murphy McBride +Murphy Payne +Myra King +Myra Wilkerson +Nala Bishop +Nala Dyer +Nala Newton +Nancy Allison +Nancy Campbell +Nancy Pope +Nash Case +Nash Crawford +Nash Lang +Natalia Buck +Natalia Frederick +Nathanael English +Nathaniel Michael +Nathaniel Reese +Naya Blackburn +Nicolas Pittman +Nikolas Burnett +Nikolas Colon +Noah Brandt +Noe Gallegos +Noe Vazquez +Noel McConnell +Noelle Leal +Noemi Gill +Novalee Preston +Nyla Frost +Oakleigh O’Connell +Oakley Frye +Oaklynn Benitez +Oaklynn Wang +Omar Zimmerman +Ophelia Morrow +Oscar Cantu +Oscar Craig +Otto Villarreal +Pablo Rush +Paislee Hess +Paisley Trujillo +Palmer McClain +Paloma Hoffman +Paris Potts +Paris Roberson +Parker Nixon +Paxton Dougherty +Pedro McCarty +Penny Yoder +Peter Huff +Peter Rubio +Peyton Morrow +Philip Jordan +Phoebe Nava +Phoenix Leal +Pierce French +Preston Hardin +Preston Mills +Promise Delgado +Promise Woodard +Queen Cortez +Quincy Fernandez +Quinn Koch +Quinn McIntyre +Quinton Hale +Raegan Franklin +Raegan Hart +Raegan Logan +Raegan Pacheco +Raelynn Ellis +Raelynn Singh +Rafael Stanley +Raina McCullough +Randy Sosa +Raphael Gardner +Ray Hail +Raya Stevens +Rayden Sellers +Raylan Dalton +Rayna Farley +Reagan Travis +Reece Khan +Reece Peterson +Reece Richards +Reese Carlson +Reese Scott +Regina Mercado +Reign Sosa +Reina Becker +Remi Schaefer +Remington Lu +Remy Foley +Renata Grimes +Rhea Hobbs +Ricardo Ali +Richard Charles +Ricky French +Riley Barrett +Riley Lynn +Riley Roman +Riley Vargas +River Coleman +River Gould +River Hanna +Rivka Meyers +Robin Lawrence +Rocco Simmons +Rodrigo Mitchell +Roger Stanley +Rohan Elliott +Ronald Manning +Ronald Williamson +Ronan Perry +Ronin Harding +Rory Carey +Rosa Suarez +Rosalie Morris +Rosemary Hendrix +Rosemary Olsen +Rowen Rasmussen +Roy Melton +Roy Nicholson +Royal Atkins +Royalty King +Royce Dominguez +Ryan Duarte +Ryan Williamson +Ryann Barron +Ryder Barrera +Ryland Molina +Ryleigh Nguyen +Ryleigh Vazquez +Sabrina Hodge +Sage Macias +Sage Valencia +Saint Flynn +Salem Lugo +Salvador Rivera +Salvatore McDonald +Samara Petersen +Samir Rojas +Samira Ramirez +Samson Andersen +Santana Crawford +Santana Owen +Santiago Morgan +Santino Joseph +Saoirse Phillips +Sarah Duarte +Sariah Bell +Sariah Bradley +Savanna Cervantes +Savanna Correa +Sawyer Snyder +Scarlett Briggs +Scott Reeves +Scout Barber +Sebastian Lawson +Selena Fleming +Selene Garner +Shelby Pennington +Shepherd Hail +Shepherd Pugh +Simon Jensen +Simone Ahmed +Sincere Hall +Skye Watson +Skyler Bradshaw +Skyler Hester +Skyler Kemp +Sloan Larson +Solomon Woodward +Sophia Davila +Soren Gross +Spencer Abbott +Spencer Salazar +Stefan Vaughan +Steven Crosby +Sullivan Cline +Sylvia Olsen +Talon Huang +Tatum Blair +Tatum Christensen +Tatum Hinton +Teresa Estrada +Thalia Blake +Thea Herrera +Theodore Brown +Thiago Cline +Thiago McMillan +Tony Hinton +Travis Bowers +Travis Gray +Trenton Estrada +Trey Munoz +Trinity Beltran +Trinity Dunlap +Turner Barnett +Tyler Gill +Tyler Webb +Uriah Bauer +Uriah Hansen +Vada Pope +Vada Reyes +Valentin Pearson +Valeria Curtis +Van Dominguez +Van Newman +Vanessa Padilla +Veronica Knox +Vihaan English +Vihaan Schmidt +Vincent Erickson +Vincenzo Roy +Violeta Wiley +Violette Pratt +Vivian Hopkins +Vivienne Cline +Vivienne Poole +Wade Pitts +Walker Truong +Warren Nava +Watson McCormick +Watson Parks +Waverly Blackburn +Waverly Hill +Waverly Snyder +Westin Andrade +Westley Sexton +Whitney Carpenter +Willie Case +Willow Mueller +Winnie Day +Winter Huber +Xiomara Brewer +Xzavier Romero +Yareli McFarland +Yareli Tapia +Zachary Decker +Zachary Villalobos +Zahir Griffith +Zahir Parra +Zakai Curry +Zariyah Cline +Zariyah Pierce +Zaylee Walker +Zayn Edwards +Zechariah Schroeder +Zeke Stevenson +Zelda Reed +Zendaya May +Ziggy Fartface +Zoe Ali +Zoe Bradford +Zoey Riley +Zoie Olsen +Zola Phelps +Zoya Sellers +Zyaire Friedman diff --git a/zehardware/src/app_fetch.py b/zehardware/src/app_fetch.py deleted file mode 100644 index 7b0924b7..00000000 --- a/zehardware/src/app_fetch.py +++ /dev/null @@ -1,52 +0,0 @@ -import zeos -import wifi -from message import Message - - -class FetchApp: - def __init__(self, os: zeos.ZeBadgeOs): - self.os = os - self.subscription_ids = [] - - def run(self): - self.subscription_ids += [ - self.os.subscribe(wifi.MessageKey.SCAN, _scanned), - self.os.subscribe(wifi.MessageKey.CONNECT_RESULT, _connected), - self.os.subscribe(wifi.MessageKey.GET_RESULT, _getted), - ] - - self.os.messages.append(Message(zeos.MessageKey.INFO, 'scanning wifi')) - self.os.messages.append(Message(wifi.MessageKey.SCAN_RESULT)) - - def unrun(self): - for subscription in self.subscription_ids: - self.os.unsubscribe(subscription) - - -def _scanned(os: zeos.ZeBadgeOs, message): - if message.value: - os.messages.append( - Message(zeos.MessageKey.INFO, f"Found '{"', '".join(message.value)}' WiFis\nConnecting to wifi ...")) - os.messages.append(Message(wifi.MessageKey.CONNECT, { - 'ssid': os.config.wifi.ssid, - 'pwd': os.config.wifi.pwd - })) - else: - os.messages.append(Message(zeos.MessageKey.INFO, 'Could not scan wifi')) - - -def _connected(os: zeos.ZeBadgeOs, message): - if message.value: - os.messages.append(Message(zeos.MessageKey.INFO, 'GET sample data ...')) - os.messages.append(Message(wifi.MessageKey.GET, { - 'ip': os.config.wifi.ip, - 'url': os.config.wifi.url, - 'host': os.config.wifi.host, - 'port': os.config.wifi.port, - })) - else: - os.messages.append(Message(zeos.MessageKey.ERROR, 'Could not connect')) - - -def _getted(os: zeos.ZeBadgeOs, message): - os.messages.append(Message(zeos.MessageKey.INFO, f'data received: {message.value.body}')) diff --git a/zehardware/src/app_store_and_show.py b/zehardware/src/app_store_and_show.py index f0245383..866b589a 100644 --- a/zehardware/src/app_store_and_show.py +++ b/zehardware/src/app_store_and_show.py @@ -1,5 +1,5 @@ -import zeos import ui +import zeos from message import Message @@ -24,7 +24,6 @@ def unrun(self): self.os.unsubscribe(subscription_id) def _buttons_changed(self, changed): - self.os.messages.append(Message("INFO", f"{changed}")) if 'up' in changed and not changed['up']: self._load_previous() if 'down' in changed and not changed['down']: diff --git a/zehardware/src/app_zepass.py b/zehardware/src/app_zepass.py new file mode 100644 index 00000000..0ec7a334 --- /dev/null +++ b/zehardware/src/app_zepass.py @@ -0,0 +1,152 @@ +import json + +import displayio +import terminalio +from adafruit_display_text import label + +import ui +import wifi +import zeos +from message import Message +from ui import MessageKey as UIKeys + + +class ZePassApp: + def __init__(self, os: zeos.ZeBadgeOs): + self.os = os + self.subscription_ids = [] + self.method = "" + + def run(self): + self.subscription_ids += [ + self.os.subscribe(wifi.MessageKey.CONNECT_RESULT, self._connected), + self.os.subscribe(wifi.MessageKey.GET_RESULT, self._response_received), + self.os.subscribe(wifi.MessageKey.POST_RESULT, self._response_received), + + self.os.subscribe( + zeos.MessageKey.BUTTON_CHANGED, + lambda os, message: self._buttons_changed(message.value) + ), + ] + + def unrun(self): + for subscription in self.subscription_ids: + self.os.unsubscribe(subscription) + + def _buttons_changed(self, changed): + if 'up' in changed and not changed['up']: + self._fetch_all_posts() + + if 'down' in changed and not changed['down']: + self._create_new_post() + + def _fetch_all_posts(self): + config = { + 'ssid': self.os.config['wifi.ssid'], + 'pwd': self.os.config['wifi.pwd'] + } + self.method = "GET" + self.os.messages.append(Message(zeos.MessageKey.INFO, f'Connecting to wifi {config} ')) + self.os.messages.append( + Message( + wifi.MessageKey.CONNECT, + config + ) + ) + + def _connected(self, os: zeos.ZeBadgeOs, message): + if message.value: + config = { + 'ip': os.config['wifi.ip'], + 'url': os.config['wifi.url'], + 'host': os.config['wifi.host'], + 'port': os.config['wifi.port'], + } + + if self.method == "GET": + os.messages.append(Message(zeos.MessageKey.INFO, f'Connected, GETing posts {config}.')) + os.messages.append(Message(wifi.MessageKey.GET, config)) + elif self.method == "POST": + config['body'] = os.config['user.uuid'] + os.messages.append(Message(zeos.MessageKey.INFO, f'Connected, POSTing {config}.')) + os.messages.append(Message(wifi.MessageKey.POST, config)) + else: + os.messages.append( + Message( + zeos.MessageKey.ERROR, + f"Zepass method {self.method} not understood." + ) + ) + self.method = '' + + else: + os.messages.append(Message(zeos.MessageKey.ERROR, 'Could not connect')) + + def _create_new_post(self): + self.method = "POST" + + config = { + 'ssid': self.os.config['wifi.ssid'], + 'pwd': self.os.config['wifi.pwd'] + } + self.os.messages.append(Message(zeos.MessageKey.INFO, f'Trying to connect: {config}')) + self.os.messages.append( + Message( + wifi.MessageKey.CONNECT, + config + ) + ) + + def _response_received(self, os: zeos.ZeBadgeOs, message): + if self.method == "GET": + self._update_all_posts( + message.value.body + ) + + elif self.method == "POST": + self.method = None + self._fetch_all_posts() + + def _update_all_posts(self, raw_posts): + group = displayio.Group() + font = terminalio.FONT + + try: + posts = json.loads(raw_posts) + except ValueError: + if "+IPD" in raw_posts: + posts = json.loads(raw_posts.split(':', 1)[1]) + else: + print(f'Could not parse response: {raw_posts}') + return + + for index, post in enumerate(posts): + # TODO: ADD FANCY USER LOGO HERE + post_area = label.Label( + font, + scale=2, + text=post['message'], + background_color=0x000000, + color=0xFFFFFF, + ) + x_offset =(index % 2) * 16 + post_area.x = x_offset + 40 + post_area.y = 16 + index * 32 + + group.append(post_area) + if 'profileB64' in post and post['profileB64']: + profile = post['profileB64'] + bitmap, palette = ui.decode_serialized_bitmap(profile, 32, 32) + profile_grid = displayio.TileGrid(bitmap, pixel_shader=palette) + profile_grid.x = x_offset + profile_grid.y = index * 32 + group.append(profile_grid) + else: + print(f'no profile for message {index}.') + + self.os.messages.append( + Message( + UIKeys.SHOW_GROUP, + group + ) + ) diff --git a/zehardware/src/config.py b/zehardware/src/config.py index 859bac03..bf608cdd 100644 --- a/zehardware/src/config.py +++ b/zehardware/src/config.py @@ -1,28 +1,33 @@ +import util + _SPACE_REPLACEMENT_ = "$SPACE#" +def _sanatize(config): + return config.replace('\r\n', '').replace('\n', ' ') -def save_config(config, filename: str = '/ze.conf'): +def save_config(config, filename: str = 'ze.conf'): file = open(filename, 'w') if file: file.write( - fields_to_str(config) + fields_to_str(_sanatize(config)) ) def update_config(config, content: str): if content: - _execute_assignments_on_obj(config, content) + str_to_fields(config, _sanatize(content)) else: print('No content to update.') -def load_config(config, filename: str = '/ze.conf') -> bool: +def load_config(config, filename: str = 'ze.conf') -> bool: try: file = open(filename, 'r') if file: - _execute_assignments_on_obj(config, file.readlines()) + str_to_fields(config, file.read()) return True - except Exception: + except Exception as e: + print(util.exception_to_readable(e)) return False @@ -30,39 +35,26 @@ def fields_to_str(obj) -> str: result = "" for field in obj: value = obj[field] - - if isinstance(value, str): - value = f"{value.replace(' ', _SPACE_REPLACEMENT_)}" + try: + value = value.replace(' ', _SPACE_REPLACEMENT_) + except Exception: + '' result += f'{field}={value} ' return result -def _execute_assignments_on_obj(obj, assignments): - assignments = assignments.split(' ') - - for assignment in assignments: - assignment = assignment.replace(_SPACE_REPLACEMENT_, ' ') - if '=' in assignment: - key, *values = assignment.split('=') - key = key.strip() - value = '='.join(values) - - if value: - value = _ensure_typed_value(value) - - obj[key] = value +def str_to_fields(obj, assignments): + l = list(filter(lambda y: len(y) == 2, map(lambda x: x.replace(_SPACE_REPLACEMENT_, ' ').replace('\n', '').split('=',1), assignments.split(' ')))) + read = dict(l) + for key in read: + value = read[key] + obj[key] = _ensure_typed_value(value) def _ensure_typed_value(value): - if '"' in value: - # TODO: Replace only start and end quotes. - value = str(value.replace('"', "")) - elif "'" in value: - # TODO: Replace only start and end quotes. - value = str(value.replace("'", "")) - elif value == "True": + if value == "True": value = True elif value == "False": value = False @@ -70,12 +62,12 @@ def _ensure_typed_value(value): try: value = float(value) except ValueError: - value = None + value = str(value) else: try: value = int(value) except ValueError: - value = None + value = str(value) return value @@ -100,16 +92,15 @@ def __test__(): expected = { "last_app": None, "developer_mode": True, - "wifi.ssid": "Your cat has an ssid", + "wifi.ssid": "Your cat has an ssid.", "wifi.pwd": "ask pete! !@", - "wifi.port": 1233.890 + "wifi.port": 1233.890, } cfg = fields_to_str(expected) print(cfg) - actual = {} - _execute_assignments_on_obj(actual, cfg) + str_to_fields(actual, cfg) __test_compare(actual, expected) diff --git a/zehardware/src/wifi.py b/zehardware/src/wifi.py index 52f8698b..969b212f 100644 --- a/zehardware/src/wifi.py +++ b/zehardware/src/wifi.py @@ -1,7 +1,9 @@ +import time + import board import busio import microcontroller -import time + from message import Message @@ -12,6 +14,8 @@ class MessageKey: CONNECT_RESULT = "CONNECT_RESULT" GET = "GET" GET_RESULT = "GET_RESULT" + POST = "POST" + POST_RESULT = "POST_RESULT" class Network: @@ -31,7 +35,7 @@ def __init__(self, status, headers, body): self.body = body def __str__(self): - return f"{self.status}:{self.headers}= {self.body}" + return f"{self.status} {self.headers}\n{self.body}" class ZeWifi: @@ -78,6 +82,9 @@ def scan(self) -> map[str:list[Network]] | None: def connect(self, ssid: str, pwd: str) -> bool: available_networks = self.scan() + if not available_networks: + available_networks = self.scan() + if available_networks and ssid in available_networks: found = sorted(available_networks[ssid], key=lambda x: x.strength)[-1] @@ -93,7 +100,8 @@ def connect(self, ssid: str, pwd: str) -> bool: print(f"Network '{ssid}' was not found. These are available: '{"' ".join(available_networks.keys())}'.") return False - def http_get(self, ip: str, url: str, host: str = "", port: int = 80) -> HttpResponse | None: + def _http_method(self, method: str, ip: str, url: str, host: str = "", port: int = 80, + body: str = "") -> HttpResponse | None: response = "" while len(response) == 0 or "STATUS:3" not in response.decode(): # connect to ip @@ -108,7 +116,17 @@ def http_get(self, ip: str, url: str, host: str = "", port: int = 80) -> HttpRes response = self.uart.read() # create http payload - payload = f"GET {url} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: ZeWeb/0.1337.0\r\nAccept: */*\r\n\r\n" + payload = (f"{method} {url} HTTP/1.1\r\n" + + f"Host: {ip}:{port}\r\n" + + f"User-Agent: ZeBadge/0.1337.0\r\n" + + f"Accept: */*\r\n") + + if len(body) > 0: + payload += (f"Content-type: application/json\r\n" + + f"Content-Length: {len(body)}\r\n") + + payload += f"\r\n{body}" + self.uart.write(f'AT+CIPSEND={len(payload)}\r\n') self.uart.read() @@ -126,6 +144,12 @@ def http_get(self, ip: str, url: str, host: str = "", port: int = 80) -> HttpRes return response + def http_get(self, ip: str, url: str, host: str = "", port: int = 80) -> HttpResponse | None: + return self._http_method("GET", ip, url, host, port) + + def http_post(self, ip: str, url: str, host: str = "", port: int = 80, body: str = "") -> HttpResponse | None: + return self._http_method("POST", ip, url, host, port, body) + wifi = None @@ -139,18 +163,47 @@ def init(os) -> bool: wifi = ZeWifi() - if wifi.scan(): + scan = wifi.scan() + if not scan: + scan = wifi.scan() + + if scan: os.subscribe(MessageKey.SCAN, lambda _, message: os.messages.append( Message(MessageKey.SCAN_RESULT, wifi.scan()))) os.subscribe(MessageKey.CONNECT, lambda _, message: os.messages.append( - Message(MessageKey.CONNECT_RESULT, wifi.connect(message.value['ssid'], message.value['pwd'])))) + Message( + MessageKey.CONNECT_RESULT, + wifi.connect( + message.value['ssid'], + message.value['pwd'] + ) + ) + )) os.subscribe(MessageKey.GET, lambda _, message: os.messages.append( - Message(MessageKey.GET_RESULT, - wifi.http_get(message.value['ip'], message.value['url'], message.value['host'], - message.value['port']) - ) + Message( + MessageKey.GET_RESULT, + wifi.http_get( + message.value['ip'], + message.value['url'], + message.value['host'], + message.value['port'] + ) + ) + )) + + os.subscribe(MessageKey.POST, lambda _, message: os.messages.append( + Message( + MessageKey.POST_RESULT, + wifi.http_post( + message.value['ip'], + message.value['url'], + message.value['host'], + message.value['port'], + message.value['body'], + ) + ) )) return True else: @@ -158,7 +211,7 @@ def init(os) -> bool: def _parse_response(response: str) -> HttpResponse | None: - parts = response.replace('\r\n', '\n').splitlines()[5:-1] + parts = response.replace('\r\n', '\n').splitlines()[5:] status_code = parts.pop(0) if status_code.startswith('+IPD'): @@ -169,27 +222,18 @@ def _parse_response(response: str) -> HttpResponse | None: status_code = 444 headers = {} - body = "" for index, header in enumerate(parts): - key, *values = header.split(":") - if len(key) == 0: - continue - - if len(values) > 1: - value = ":".join(values) - elif len(values) == 1: - value = values[0] - else: - value = "" - - # found the end? - if key.startswith('+IPD'): - # yes - body = value + try: + key, value = header.split(":", 1) + except ValueError: break - else: - # nope, keep iterating through headers - headers[key] = value.strip() + key = key.strip() + value = value.strip() + + # nope, keep iterating through headers + headers[key] = value.strip() + + body = parts[-1] response = HttpResponse(status_code, headers, body) return response diff --git a/zehardware/src/zeos.py b/zehardware/src/zeos.py index dd29578c..c2a58cdd 100644 --- a/zehardware/src/zeos.py +++ b/zehardware/src/zeos.py @@ -1,26 +1,23 @@ -import board -import gc -import serial -import sys +import os as systemos import time import traceback -import ui -import usb_cdc - -import os as systemos +import board +import usb_cdc from digitalio import DigitalInOut from digitalio import Direction from digitalio import Pull -from message import Message -from config import save_config +import serial +import ui +from app_developer_idle_clicker import DeveloperIdleClickerApp +from app_zepass import ZePassApp +from app_store_and_show import StoreAndShowApp +from config import fields_to_str from config import load_config +from config import save_config from config import update_config -from config import fields_to_str -from app_fetch import FetchApp -from app_store_and_show import StoreAndShowApp -from app_developer_idle_clicker import DeveloperIdleClickerApp +from message import Message class MessageKey: @@ -60,11 +57,14 @@ def __init__(self): self.buttons = SystemButtons() self.tasks.append(_update_system_buttons) + self._reset_subscribers() + self._subscribe_to_system_buttons() # add defaults self.config = {} - # load_config(self.config) - self.config['uid'] = 'some-thing-amazing' + load_config(self.config) + + self._init_interfaces() # applications self._app_a = None @@ -72,10 +72,6 @@ def __init__(self): self._app_c = None self._init_apps() - self._reset_subscribers() - self._init_interfaces() - self._subscribe_to_system_buttons() - self.system_subscribers = self.subscribers.copy() def _reset_subscribers(self): @@ -149,7 +145,7 @@ def run(self): subscriber = subscriber_ids[subscriber_id] subscriber(self, message) else: - print(f'?', end='') + print(f'?({message.topic})', end='') self.led_on = not self.led_on self.led.value = self.led_on @@ -180,23 +176,34 @@ def _init_interfaces(self): import keyboard keyboard.init(self) - self.config["keyboard_attached"] = has_keyboard + self.config["keyboard.attached"] = has_keyboard else: + self.config["keyboard.attached"] = False + print("... no i2c found, trying wifi") import wifi if not wifi.init(self): print("... no wifi found.") - self.config["wifi_attached"] = False + self.config["wifi.attached"] = False else: - self.config["wifi_attached"] = True + print("... wifi !!!") + self.config["wifi.attached"] = True - self.config["developer_mode"] = not (usb_cdc.data is None) + self.config["developer.mode"] = not (usb_cdc.data is None) def _init_apps(self): self._app_a = StoreAndShowApp(self) - self._app_b = FetchApp(self) - self._app_c = DeveloperIdleClickerApp(self) + + if self.config["wifi.attached"]: + self._app_b = None + self._app_c = ZePassApp(self) + elif self.config['keyboard.attached']: + self._app_b = None + self._app_c = DeveloperIdleClickerApp(self) + else: + self._app_b = None + self._app_c = DeveloperIdleClickerApp(self) self._start_app(self._app_a) diff --git a/zehardware/zefirmware/zeflash.py b/zehardware/zefirmware/zeflash.py new file mode 100755 index 00000000..cdeaa8fc --- /dev/null +++ b/zehardware/zefirmware/zeflash.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +# +# FLASH ME IF YOU CAN!! +# +import datetime +import os +import requests +import subprocess +import shutil +import time + +OPEN_AI_TOKEN = os.getenv("OPEN_AI_TOKEN") +SERVER_TOKEN = os.getenv("ZESERVER_AUTH_TOKEN") +BASE_URL = "https://zebadge.app/" + +BASE_BADGE_PATH = "/Volumes" + + +def hsv(h, s, v): + """ + For h (0..360), s (0..1), v(0..1) create a r(0..1) g(0..1) b(0..1) tuple. + """ + r = 0 + g = 0 + b = 0 + + c = v * s + x = c * (1.0 - abs((((h / 60.0) % 2.0) - 1.0))) + m = v - c + + if 0 <= h < 60: + r = c + g = x + b = 0 + elif 60 <= h < 120: + r = x + g = c + b = 0 + elif 120 <= h < 180: + r = 0 + g = c + b = x + elif 180 <= h < 240: + r = 0 + g = x + b = c + elif 240 <= h < 300: + r = x + g = 0 + b = c + elif 300 <= h < 360: + r = c + g = 0 + b = x + + return r + m, g + m, b + m + + +def rgb_to_termcolor(rgb): + """ + Convert the given rgb (given from 0 to 1) to a terminal color from 16 to 216 + """ + r, g, b = rgb + return 16 + int(r * 5) * 36 + int(g * 5) * 6 + int(b * 5) + + +def colorize(text): + result = "" + for (index, char) in enumerate(text): + code = rgb_to_termcolor(hsv(index * 360.0 / len(text), 0.3, 1.0)) + result += f"\033[38;5;{code}m{char}" + result += f"\033[m" + return result + + +def request_new_user(): + assert SERVER_TOKEN, f"SERVER_AUTH not set: '{SERVER_TOKEN}'." + + users = requests.get( + url=f"{BASE_URL}/api/user", + headers={ + 'Content-Type': 'application/json', + 'ZeAuth': SERVER_TOKEN + }, + ).json() + + print(f"Found {len(users)} users. Adding another one.") + + new_user = requests.post( + url=f"{BASE_URL}/api/user", + headers={ + 'Content-Type': 'application/json', + 'ZeAuth': SERVER_TOKEN + }, + json={} + ) + + if new_user.ok: + user = new_user.json() + print(f".. user '{colorize(user['name'])}' created.") + return user + else: + print(f".. couldn't create user: '{colorize(new_user.json())}'.") + + return None + + +def find_mount_point(name): + mount = subprocess.run(["mount"], capture_output=True, check=True) + if mount.returncode != 0: + print( + f'"mount" returned error code {mount.returncode}.\n\nCheck outputs\nstd:{mount.stdout}\nerr:{mount.stderr}\n') + return None + + output = mount.stdout.decode() + mount.stderr.decode() + if name in output: + return list(map(lambda y: y.split()[2], filter(lambda x: name in x, output.splitlines())))[0] + else: + return None + + +def find_base_badge_path(): + rpi = find_mount_point('RPI') + if rpi: + return rpi + + cirpy = find_mount_point('CIRCUIT') + if cirpy: + return cirpy + + return None + + +def nuke(): + nuke_ware = list(filter(lambda x: 'nuke' in x, os.listdir("./"))) + + if not nuke_ware: + print(colorize("No nuke firmware found!")) + return False + + nuke_ware = nuke_ware[0] + + path = find_mount_point('RPI') + if not path: + print( + f"Please put badge in flash mode.\n" + f"{colorize('restart')} ZeBadge and " + f"hold {colorize('boot / usr')} button." + ) + + while not find_mount_point('RPI'): + print('.', flush=True, end='') + time.sleep(0.1) + print() + path = find_mount_point('RPI') + + print(colorize('nuking')) + + time.sleep(0.2) + shutil.copy(nuke_ware, path) + time.sleep(0.3) + + while not find_mount_point('RPI'): + print('.', flush=True, end='') + time.sleep(0.1) + print() + + +def flash(): + zepython = list(filter(lambda x: 'zepython' in x, os.listdir("./"))) + + if not zepython: + print(colorize("No zepython firmware found!")) + return False + + zepython = zepython[0] + + path = find_mount_point('RPI') + if not path: + print(f"Please put badge in flash mode.\n{colorize('restart')} and hold {colorize('boot / usr')} button.") + + while not find_mount_point('RPI'): + print('.', flush=True, end='') + time.sleep(0.1) + print() + + path = find_mount_point('RPI') + + print(colorize('flashing')) + + complete = False + + def wait_on_exception(): + print('x', flush=True, end='') + time.sleep(1) + + while not complete: + try: + shutil.copy(zepython, path) + complete = True + except PermissionError: + wait_on_exception() + except OSError: + wait_on_exception() + + while not find_mount_point('CIRC'): + print('.', flush=True, end='') + time.sleep(0.1) + + print() + + +def copy_code(): + circuit = find_mount_point('CIRC') + while not circuit: + print('.', flush=True, end='') + time.sleep(0.1) + circuit = find_mount_point('CIRC') + + print(colorize("copy libs")) + shutil.copytree('../lib', circuit + "/lib/", dirs_exist_ok=True) + + print(colorize("copy code")) + source_files = os.listdir('../src/') + for src in source_files: + try: + shutil.copy('../src/' + src, circuit) + print('.', flush=True, end='') + except IsADirectoryError: + print(':', flush=True, end='') + print() + + print(colorize("copy bitmap resources")) + resource_files = os.listdir('../resources/') + for res in resource_files: + try: + if res.endswith('bmp'): + shutil.copy('../resources/' + res, circuit) + print('.', flush=True, end='') + else: + print(',', flush=True, end='') + except IsADirectoryError: + print(':', flush=True, end='') + + print() + + +def inject_user(user): + circuit = find_mount_point('CIRC') + while not circuit: + print('.', flush=True, end='') + time.sleep(0.1) + circuit = find_mount_point('CIRC') + + user_config = " ".join(list(map(lambda x: f'user.{x}={user[x].replace(' ', "$SPACE#")}', user))) + user_config += \ + f"wifi.ssid=droidcon24 " \ + f"wifi.pwd=helloDrdoicon24 " \ + f"wifi.ip=35.208.138.148 " \ + f"wifi.port=1337 " \ + f"wifi.url=/api/zepass " \ + f"wifi.host=zebadge.app " + + print(f"{colorize("injecting user")}: {user_config}") + + open(circuit + "/ze.conf", "w+").write( + user_config + ) + + +if __name__ == '__main__': + badge_path = find_base_badge_path() + if badge_path: + print(f"Found a badge on '{colorize(badge_path)}', happy days.") + else: + print(colorize("No Badge found!")) + exit(-1) + + nuke() + + flash() + + copy_code() + + inject_user(request_new_user()) + + print(colorize("!! DONE !!"))