From e4cdd290347050edd7847b183914c9ccfaca0f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Przemys=C5=82aw=20Pluta?= Date: Wed, 1 Jul 2020 18:31:38 +0200 Subject: [PATCH] Create working PoC --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 1 + .../ttsserver/core/lua/lib/HTTPLibrary.kt | 22 ++ .../ttsserver/core/lua/lib/TTSLibrary.kt | 30 +++ .../core/lua/loader/EndpointLoader.kt | 63 +++++ .../core/lua/sandbox/SandboxFactory.kt | 31 +++ .../core/sonos/worker/SonosWorker.kt | 6 +- .../ttsserver/core/web/endpoint/Endpoint.kt | 102 +++++-- .../core/web/endpoint/EndpointMatcher.kt | 4 +- .../core/web/endpoint/EndpointType.kt | 6 + .../ttsserver/core/web/endpoint/Endpointx.kt | 22 ++ .../core/web/endpoint/ResponseType.kt | 6 + .../ttsserver/core/web/server/WebServer.kt | 250 ++++++++---------- .../core/web/server/WebServerFactory.kt | 19 +- .../ttsserver/core/web/uri/UriTemplate.kt | 7 +- .../ttsserver/di/component/AppComponent.kt | 3 +- .../ttsserver/di/module/LuaModule.kt | 33 +++ .../ttsserver/di/module/TTSModule.kt | 7 +- 18 files changed, 444 insertions(+), 169 deletions(-) create mode 100644 app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/lib/HTTPLibrary.kt create mode 100644 app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/lib/TTSLibrary.kt create mode 100644 app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/loader/EndpointLoader.kt create mode 100644 app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/sandbox/SandboxFactory.kt create mode 100644 app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointType.kt create mode 100644 app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpointx.kt create mode 100644 app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/ResponseType.kt create mode 100644 app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/LuaModule.kt diff --git a/app/build.gradle b/app/build.gradle index 7a99dd6..68e3258 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation 'com.google.dagger:dagger-android:2.15' implementation 'com.google.dagger:dagger-android-support:2.15' implementation 'com.github.adrielcafe:AndroidAudioConverter:0.0.8' + implementation 'org.luaj:luaj-jse:3.0.1' kapt 'com.google.dagger:dagger-android-processor:2.15' kapt 'com.google.dagger:dagger-compiler:2.15' testImplementation 'junit:junit:4.12' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 795f887..63ba4a5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ + { + val scripts = context.getExternalFilesDir("Endpoints")?.listFiles() ?: emptyArray() + + return scripts + .map { sandboxFactory.createSandbox().loadfile(it.absolutePath).call() } + .map { it as? LuaTable ?: throw IllegalArgumentException("Expected single table to be returned") } + .map { createEndpoint(it) } + } + + private fun createEndpoint(luaTable: LuaTable) = Endpoint( + uri = parseUri(luaTable), + method = parseMethod(luaTable), + type = parseType(luaTable), + accepts = parseAccepts(luaTable), + consumer = parseConsumer(luaTable) + ) + + private fun parseUri(luaTable: LuaTable) = luaTable.get("uri") + .takeIf { it !is LuaNil } + ?.let { it as? LuaString ?: throw IllegalArgumentException("'uri' must be of string type'") } + ?.tojstring() + ?.let { UriTemplate.parse(it) } + ?: throw IllegalArgumentException("'uri' field is required") + + private fun parseConsumer(luaTable: LuaTable) = luaTable.get("consumer") + .takeIf { it !is LuaNil } + ?.let { it as? LuaClosure ?: throw IllegalArgumentException("'consumer' must be a function'") } + ?: throw IllegalArgumentException("'consumer' field is required") + + private fun parseAccepts(luaTable: LuaTable) = luaTable.get("accepts") + .takeIf { it !is LuaNil } + ?.let { it as? LuaString ?: throw IllegalArgumentException("'accepts' must be of string type'") } + ?.tojstring() + ?: "text/plain" + + private fun parseType(luaTable: LuaTable) = luaTable.get("type") + .takeIf { it !is LuaNil } + ?.let { it as? LuaString ?: throw IllegalArgumentException("'type' must be of string type'") } + ?.let { EndpointType.valueOf(it.tojstring()) } + ?: EndpointType.DEFAULT + + private fun parseMethod(luaTable: LuaTable) = luaTable.get("method") + .takeIf { it !is LuaNil } + ?.let { it as? LuaString ?: throw IllegalArgumentException("'method' must be of string type'") } + ?.let { Method.valueOf(it.tojstring()) } + ?: Method.GET +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/sandbox/SandboxFactory.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/sandbox/SandboxFactory.kt new file mode 100644 index 0000000..690cd1d --- /dev/null +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/sandbox/SandboxFactory.kt @@ -0,0 +1,31 @@ +package com.bartlomiejpluta.ttsserver.core.lua.sandbox + +import com.bartlomiejpluta.ttsserver.core.lua.lib.HTTPLibrary +import com.bartlomiejpluta.ttsserver.core.lua.lib.TTSLibrary +import org.luaj.vm2.Globals +import org.luaj.vm2.LoadState +import org.luaj.vm2.compiler.LuaC +import org.luaj.vm2.lib.PackageLib +import org.luaj.vm2.lib.StringLib +import org.luaj.vm2.lib.TableLib +import org.luaj.vm2.lib.jse.JseBaseLib +import org.luaj.vm2.lib.jse.JseMathLib +import org.luaj.vm2.lib.jse.JseOsLib + +class SandboxFactory( + private val httpLibrary: HTTPLibrary, + private val ttsLibrary: TTSLibrary +) { + fun createSandbox() = Globals().also { + it.load(JseBaseLib()) + it.load(PackageLib()) + it.load(TableLib()) + it.load(StringLib()) + it.load(JseMathLib()) + it.load(JseOsLib()) + it.load(httpLibrary) + it.load(ttsLibrary) + LoadState.install(it) + LuaC.install(it) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/sonos/worker/SonosWorker.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/sonos/worker/SonosWorker.kt index b0cef44..115e9da 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/sonos/worker/SonosWorker.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/sonos/worker/SonosWorker.kt @@ -4,7 +4,7 @@ import android.content.SharedPreferences import cafe.adriel.androidaudioconverter.model.AudioFormat import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine import com.bartlomiejpluta.ttsserver.core.web.dto.SonosDTO -import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint +import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpointx import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService import com.bartlomiejpluta.ttsserver.service.state.ServiceState import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey @@ -20,8 +20,8 @@ class SonosWorker( private val preferences: SharedPreferences, private val queue: BlockingQueue ) : Runnable { - private val gongUrl: String get() = address + Endpoint.GONG.trimmedUri - private val announcementUrl: String get() = address + Endpoint.SONOS_CACHE.trimmedUri + private val gongUrl: String get() = address + Endpointx.GONG.trimmedUri + private val announcementUrl: String get() = address + Endpointx.SONOS_CACHE.trimmedUri private var snapshot: Snapshot? = null override fun run() = try { diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpoint.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpoint.kt index b10cb45..c3ec146 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpoint.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpoint.kt @@ -1,22 +1,92 @@ package com.bartlomiejpluta.ttsserver.core.web.endpoint -enum class Endpoint(val uri: String, val id: Int) { - UNKNOWN("/", 1), - SAY("/say", 2), - WAVE("/wave", 3), - AAC("/aac", 4), - MP3("/mp3", 5), - M4A("/m4a", 6), - WMA("/wma", 7), - FLAC("/flac", 8), - SONOS("/sonos", 9), - SONOS_CACHE("/sonos/*", 10), - GONG("/gong.wav", 11); +import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate +import fi.iki.elonen.NanoHTTPD.* +import org.luaj.vm2.LuaClosure +import org.luaj.vm2.LuaTable +import org.luaj.vm2.LuaValue +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream - val trimmedUri: String - get() = uri.replace("*", "") +class Endpoint( + private val uri: UriTemplate, + private val type: EndpointType, + private val accepts: String, + private val method: Method, + private val consumer: LuaClosure +) { - companion object { - fun of(ordinal: Int) = values().firstOrNull { it.ordinal == ordinal } ?: UNKNOWN + fun hit(session: IHTTPSession): Response? { + if (session.method != method) { + return null + } + + val matchingResult = uri.match(session.uri) + if (!matchingResult.matched) { + return null + } + + val params = LuaValue.tableOf().also { params -> + matchingResult.variables + .map { LuaValue.valueOf(it.key) to LuaValue.valueOf(it.value) } + .forEach { params.set(it.first, it.second) } + } + + + val response = consumer.call(LuaValue.valueOf(extractBody(session)), params).checktable() + return parseResponse(response) + } + + private fun parseResponse(response: LuaValue) = response + .let { + it as? LuaTable + ?: throw IllegalArgumentException("Invalid type for response - expected table") + } + .let { provideResponse(it) } + + + private fun provideResponse(response: LuaTable) = + when (response.get("type").checkjstring()) { + ResponseType.TEXT.name -> getTextResponse(response) + ResponseType.FILE.name -> getFileResponse(response) + else -> throw IllegalArgumentException("Unknown value for type in response") + } + + private fun getTextResponse(response: LuaTable) = newFixedLengthResponse( + getStatus(response), + getMimeType(response), + getData(response) + ) + + private fun getFileResponse(response: LuaTable): Response? { + val file = File(response.get("file").checkstring().tojstring()) + val stream = BufferedInputStream(FileInputStream(file)) + val length = file.length() + return newFixedLengthResponse( + getStatus(response), + getMimeType(response), + stream, + length + ) + } + + private fun getStatus(response: LuaTable): Response.Status { + val status = response.get("status").checkint() + return Response.Status + .values() + .firstOrNull { it.requestStatus == status } + ?: throw IllegalArgumentException("Unsupported status: $status") + } + + private fun getMimeType(response: LuaTable) = response.get("mime").checkstring().tojstring() + + private fun getData(response: LuaTable) = response.get("data").checkstring().tojstring() + + private fun extractBody(session: IHTTPSession): String { + return mutableMapOf().let { + session.parseBody(it) + it["postData"] ?: "" + } } } \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointMatcher.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointMatcher.kt index 1eb1626..8f2e3e1 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointMatcher.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointMatcher.kt @@ -7,11 +7,11 @@ object EndpointMatcher { private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) init { - Endpoint.values().forEach { + Endpointx.values().forEach { uriMatcher.addURI("", it.uri, it.ordinal) } } fun match(uri: String) = - Endpoint.of(uriMatcher.match(Uri.parse("content://$uri"))) + Endpointx.of(uriMatcher.match(Uri.parse("content://$uri"))) } \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointType.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointType.kt new file mode 100644 index 0000000..1708406 --- /dev/null +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointType.kt @@ -0,0 +1,6 @@ +package com.bartlomiejpluta.ttsserver.core.web.endpoint + +enum class EndpointType { + DEFAULT, + QUEUE +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpointx.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpointx.kt new file mode 100644 index 0000000..e98398a --- /dev/null +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpointx.kt @@ -0,0 +1,22 @@ +package com.bartlomiejpluta.ttsserver.core.web.endpoint + +enum class Endpointx(val uri: String, val id: Int) { + UNKNOWN("/", 1), + SAY("/say", 2), + WAVE("/wave", 3), + AAC("/aac", 4), + MP3("/mp3", 5), + M4A("/m4a", 6), + WMA("/wma", 7), + FLAC("/flac", 8), + SONOS("/sonos", 9), + SONOS_CACHE("/sonos/*", 10), + GONG("/gong.wav", 11); + + val trimmedUri: String + get() = uri.replace("*", "") + + companion object { + fun of(ordinal: Int) = values().firstOrNull { it.ordinal == ordinal } ?: UNKNOWN + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/ResponseType.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/ResponseType.kt new file mode 100644 index 0000000..ba98028 --- /dev/null +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/ResponseType.kt @@ -0,0 +1,6 @@ +package com.bartlomiejpluta.ttsserver.core.web.endpoint + +enum class ResponseType { + TEXT, + FILE +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt index 56d1b02..6e384b7 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt @@ -3,19 +3,14 @@ package com.bartlomiejpluta.ttsserver.core.web.server import android.content.Context import android.content.Intent import android.content.SharedPreferences -import android.net.Uri import androidx.localbroadcastmanager.content.LocalBroadcastManager -import cafe.adriel.androidaudioconverter.model.AudioFormat +import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader +import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine -import com.bartlomiejpluta.ttsserver.core.tts.exception.TTSException import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus -import com.bartlomiejpluta.ttsserver.core.web.dto.BaseDTO -import com.bartlomiejpluta.ttsserver.core.web.dto.SonosDTO import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint -import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointMatcher import com.bartlomiejpluta.ttsserver.core.web.exception.WebException -import com.bartlomiejpluta.ttsserver.core.web.mime.MimeType import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService import com.bartlomiejpluta.ttsserver.service.state.ServiceState import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey @@ -23,10 +18,6 @@ import com.bartlomiejpluta.ttsserver.ui.preference.model.TimeRange import fi.iki.elonen.NanoHTTPD import fi.iki.elonen.NanoHTTPD.Response.Status.* import org.json.JSONObject -import java.io.BufferedInputStream -import java.io.File -import java.io.FileInputStream -import java.util.* class WebServer( @@ -34,7 +25,8 @@ class WebServer( private val context: Context, private val preferences: SharedPreferences, private val tts: TTSEngine, - private val sonos: SonosQueue + private val sonos: SonosQueue, + private val endpoints: List ) : NanoHTTPD(port) { private val speakersSilenceSchedulerEnabled: Boolean get() = preferences.getBoolean(PreferenceKey.ENABLE_SPEAKERS_SILENCE_SCHEDULER, false) @@ -72,20 +64,12 @@ class WebServer( } } - private fun dispatch(it: IHTTPSession): Response { - return when (EndpointMatcher.match(it.uri)) { - Endpoint.SAY -> say(it) - Endpoint.WAVE -> file(it, AudioFormat.WAV) - Endpoint.AAC -> file(it, AudioFormat.AAC) - Endpoint.MP3 -> file(it, AudioFormat.MP3) - Endpoint.M4A -> file(it, AudioFormat.M4A) - Endpoint.WMA -> file(it, AudioFormat.WMA) - Endpoint.FLAC -> file(it, AudioFormat.FLAC) - Endpoint.SONOS -> sonos(it) - Endpoint.SONOS_CACHE -> sonosCache(it) - Endpoint.GONG -> gong(it) - Endpoint.UNKNOWN -> throw WebException(NOT_FOUND) + private fun dispatch(session: IHTTPSession): Response { + for (endpoint in endpoints) { + endpoint.hit(session)?.let { return it } } + + throw WebException(NOT_FOUND) } private fun handleWebException(e: WebException) = @@ -100,28 +84,28 @@ class WebServer( return newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT, stacktrace) } - private fun say(session: IHTTPSession): Response { - if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) { - throw WebException(NOT_FOUND) - } - - if (session.method != Method.POST) { - throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed") - } - - if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { - throw WebException(BAD_REQUEST, "Only JSON data is accepted") - } - - if (speakersSilenceSchedulerEnabled && speakersSilenceSchedule.inRange(Calendar.getInstance())) { - return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "") - } - - val dto = extractBody(session) { BaseDTO(it) } - - tts.performTTS(dto.text, dto.language) - return newFixedLengthResponse(OK, MIME_JSON, SUCCESS_RESPONSE) - } +// private fun say(session: IHTTPSession): Response { +// if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) { +// throw WebException(NOT_FOUND) +// } +// +// if (session.method != Method.POST) { +// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed") +// } +// +// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { +// throw WebException(BAD_REQUEST, "Only JSON data is accepted") +// } +// +// if (speakersSilenceSchedulerEnabled && speakersSilenceSchedule.inRange(Calendar.getInstance())) { +// return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "") +// } +// +// val dto = extractBody(session) { BaseDTO(it) } +// +// tts.performTTS(dto.text, dto.language) +// return newFixedLengthResponse(OK, MIME_JSON, SUCCESS_RESPONSE) +// } private fun extractBody(session: IHTTPSession, provider: (String) -> T): T { return mutableMapOf().let { @@ -130,92 +114,92 @@ class WebServer( } } - private fun file(session: IHTTPSession, audioFormat: AudioFormat): Response { - if (!preferences.getBoolean(PreferenceKey.ENABLE_FILE_ENDPOINTS, true)) { - throw WebException(NOT_FOUND) - } - - if (session.method != Method.POST) { - throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed") - } - - if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { - throw WebException(BAD_REQUEST, "Only JSON data is accepted") - } - - val dto = extractBody(session) { BaseDTO(it) } - - val (stream, size) = tts.fetchTTSStream(dto.text, dto.language, audioFormat) - return newFixedLengthResponse(OK, MimeType.forAudioFormat(audioFormat).mimeType, stream, size) - } - - private fun sonos(session: IHTTPSession): Response { - if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) { - throw WebException(NOT_FOUND) - } - - if (session.method != Method.POST) { - throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed") - } - - if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { - throw WebException(BAD_REQUEST, "Only JSON data is accepted") - } - - if (sonosSilenceSchedulerEnabled && sonosSilenceSchedule.inRange(Calendar.getInstance())) { - return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "") - } - - val dto = extractBody(session) { SonosDTO(it) } - - sonos.push(dto) - - return newFixedLengthResponse(ACCEPTED, MIME_JSON, QUEUED_RESPONSE) - } - - private fun sonosCache(session: IHTTPSession): Response { - if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) { - throw WebException(NOT_FOUND) - } - - if (session.method != Method.GET) { - throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed") - } - - val filename = Uri.parse(session.uri).lastPathSegment ?: throw WebException(BAD_REQUEST) - val file = File(context.cacheDir, filename) - - if (!file.exists()) { - throw WebException(NOT_FOUND) - } - - val stream = BufferedInputStream(FileInputStream(file)) - val size = file.length() - return newFixedLengthResponse(OK, MimeType.forFile(file).mimeType, stream, size) - } - - private fun gong(session: IHTTPSession): Response { - if (!preferences.getBoolean(PreferenceKey.ENABLE_GONG, false)) { - throw WebException(NOT_FOUND) - } - - if (session.method != Method.GET) { - throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed") - } - - val uri = Uri.parse( - preferences.getString(PreferenceKey.GONG, null) ?: throw TTSException() - ) - - val size = context.contentResolver.openFileDescriptor(uri, "r")?.statSize - ?: throw TTSException() - - val stream = BufferedInputStream( - context.contentResolver.openInputStream(uri) ?: throw TTSException() - ) - - return newFixedLengthResponse(OK, MimeType.WAV.mimeType, stream, size) - } +// private fun file(session: IHTTPSession, audioFormat: AudioFormat): Response { +// if (!preferences.getBoolean(PreferenceKey.ENABLE_FILE_ENDPOINTS, true)) { +// throw WebException(NOT_FOUND) +// } +// +// if (session.method != Method.POST) { +// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed") +// } +// +// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { +// throw WebException(BAD_REQUEST, "Only JSON data is accepted") +// } +// +// val dto = extractBody(session) { BaseDTO(it) } +// +// val (stream, size) = tts.fetchTTSStream(dto.text, dto.language, audioFormat) +// return newFixedLengthResponse(OK, MimeType.forAudioFormat(audioFormat).mimeType, stream, size) +// } +// +// private fun sonos(session: IHTTPSession): Response { +// if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) { +// throw WebException(NOT_FOUND) +// } +// +// if (session.method != Method.POST) { +// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed") +// } +// +// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { +// throw WebException(BAD_REQUEST, "Only JSON data is accepted") +// } +// +// if (sonosSilenceSchedulerEnabled && sonosSilenceSchedule.inRange(Calendar.getInstance())) { +// return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "") +// } +// +// val dto = extractBody(session) { SonosDTO(it) } +// +// sonos.push(dto) +// +// return newFixedLengthResponse(ACCEPTED, MIME_JSON, QUEUED_RESPONSE) +// } +// +// private fun sonosCache(session: IHTTPSession): Response { +// if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) { +// throw WebException(NOT_FOUND) +// } +// +// if (session.method != Method.GET) { +// throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed") +// } +// +// val filename = Uri.parse(session.uri).lastPathSegment ?: throw WebException(BAD_REQUEST) +// val file = File(context.cacheDir, filename) +// +// if (!file.exists()) { +// throw WebException(NOT_FOUND) +// } +// +// val stream = BufferedInputStream(FileInputStream(file)) +// val size = file.length() +// return newFixedLengthResponse(OK, MimeType.forFile(file).mimeType, stream, size) +// } +// +// private fun gong(session: IHTTPSession): Response { +// if (!preferences.getBoolean(PreferenceKey.ENABLE_GONG, false)) { +// throw WebException(NOT_FOUND) +// } +// +// if (session.method != Method.GET) { +// throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed") +// } +// +// val uri = Uri.parse( +// preferences.getString(PreferenceKey.GONG, null) ?: throw TTSException() +// ) +// +// val size = context.contentResolver.openFileDescriptor(uri, "r")?.statSize +// ?: throw TTSException() +// +// val stream = BufferedInputStream( +// context.contentResolver.openInputStream(uri) ?: throw TTSException() +// ) +// +// return newFixedLengthResponse(OK, MimeType.WAV.mimeType, stream, size) +// } override fun start() { super.start() diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServerFactory.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServerFactory.kt index 055cb3b..43c9cfb 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServerFactory.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServerFactory.kt @@ -2,6 +2,7 @@ package com.bartlomiejpluta.ttsserver.core.web.server import android.content.Context import android.content.SharedPreferences +import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey @@ -10,13 +11,15 @@ class WebServerFactory( private val preferences: SharedPreferences, private val context: Context, private val tts: TTSEngine, - private val sonos: SonosQueue + private val sonos: SonosQueue, + private val endpointLoader: EndpointLoader ) { - fun createWebServer() = - WebServer( - preferences.getInt( - PreferenceKey.PORT, - 8080 - ), context, preferences, tts, sonos - ) + fun createWebServer() = WebServer( + preferences.getInt(PreferenceKey.PORT, 8080), + context, + preferences, + tts, + sonos, + endpointLoader.loadEndpoints() + ) } \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/uri/UriTemplate.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/uri/UriTemplate.kt index 074d907..4d7de12 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/uri/UriTemplate.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/uri/UriTemplate.kt @@ -28,10 +28,9 @@ class UriTemplate private constructor(uri: String) { variables.add(variableBuilder.toString()) } - isVariable -> char.takeIf { it.isLetter() } ?: error( - "Only letters are allowed as template", - index + 1 - ).let { variableBuilder?.append(it) } + isVariable -> char.takeIf { it.isLetter() } + ?.let { variableBuilder?.append(it) } + ?: error("Only letters are allowed as template", index + 1) else -> patternBuilder.append(char) } diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/component/AppComponent.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/component/AppComponent.kt index b0156bb..ce09872 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/component/AppComponent.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/component/AppComponent.kt @@ -3,6 +3,7 @@ package com.bartlomiejpluta.ttsserver.di.component import android.content.Context import com.bartlomiejpluta.ttsserver.TTSApplication import com.bartlomiejpluta.ttsserver.di.module.AndroidModule +import com.bartlomiejpluta.ttsserver.di.module.LuaModule import com.bartlomiejpluta.ttsserver.di.module.TTSModule import dagger.BindsInstance import dagger.Component @@ -11,7 +12,7 @@ import dagger.android.support.AndroidSupportInjectionModule import javax.inject.Singleton @Singleton -@Component(modules = [AndroidSupportInjectionModule::class, AndroidModule::class, TTSModule::class]) +@Component(modules = [AndroidSupportInjectionModule::class, AndroidModule::class, TTSModule::class, LuaModule::class]) interface AppComponent : AndroidInjector { @Component.Builder diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/LuaModule.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/LuaModule.kt new file mode 100644 index 0000000..2824599 --- /dev/null +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/LuaModule.kt @@ -0,0 +1,33 @@ +package com.bartlomiejpluta.ttsserver.di.module + +import android.content.Context +import com.bartlomiejpluta.ttsserver.core.lua.lib.HTTPLibrary +import com.bartlomiejpluta.ttsserver.core.lua.lib.TTSLibrary +import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader +import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory +import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class LuaModule { + + @Provides + @Singleton + fun endpointLoader(context: Context, sandboxFactory: SandboxFactory) = + EndpointLoader(context, sandboxFactory) + + @Provides + @Singleton + fun sandboxFactory(httpLibrary: HTTPLibrary, ttsLibrary: TTSLibrary) = + SandboxFactory(httpLibrary, ttsLibrary) + + @Provides + @Singleton + fun httpLibrary() = HTTPLibrary() + + @Provides + @Singleton + fun ttsLibrary(ttsEngine: TTSEngine) = TTSLibrary(ttsEngine) +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt index 4a4f9b4..23d4549 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.SharedPreferences import android.speech.tts.TextToSpeech import androidx.preference.PreferenceManager +import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder @@ -43,12 +44,14 @@ class TTSModule { preferences: SharedPreferences, context: Context, tts: TTSEngine, - sonos: SonosQueue + sonos: SonosQueue, + endpointLoader: EndpointLoader ) = WebServerFactory( preferences, context, tts, - sonos + sonos, + endpointLoader ) @Provides