Skip to content

Commit

Permalink
Allow sending simple HTTP requests from Scripting #422
Browse files Browse the repository at this point in the history
  • Loading branch information
Waboodoo committed Dec 18, 2024
1 parent 931e010 commit f579092
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 7 deletions.
15 changes: 15 additions & 0 deletions HTTPShortcuts/app/src/main/assets/docs/scripting.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,21 @@
</code></pre><p>For this function to work, location services need to be enabled and the app needs to be granted the permission to access the device's location. This is a technical limitation imposed by the Android OS. See also the <a href="permissions.md">Permissions</a> page for details.</p><p><a name="wol"></a></p><h3>Wake-on-LAN</h3><p>You can use the <code>wakeOnLan</code> function to send a magic packet to turn on another device on your network. The first parameter has to be the MAC-address of the device. As the optional second parameter, you can pass the network/broadcast address to be used, and as the third parameter you can define the port.</p><pre><code class="language-js">wakeOnLan('01-23-45-67-89-ab');

wakeOnLan('01-23-45-67-89-ab', '255.255.255.255', 9);
</code></pre><p><a name="send-http-request"></a></p><h3>Send HTTP request</h3><p>The <code>sendHttpRequest</code> function allows you to send a simple HTTP request. The first parameter is the URL, the second (optional) parameter provides additional options. Currently, the only supported is &quot;method&quot;, which is used to set the HTTP method.
The function returns an object which includes a <code>status</code> field, which has the value &quot;success&quot;, &quot;httpError&quot; or &quot;networkError&quot;. If it is &quot;networkError&quot;, you can check the field <code>networkError</code> for details. Otherwise, you can check the <code>response</code> field for the HTTP response.</p><blockquote><p>If you need more options, consider creating a dedicated HTTP Shortcut for your request and invoking it using <a href="enqueueShortcut">#trigger-shortcut</a> or <a href="executeShortcut">#execute-shortcut</a> instead.</p></blockquote><pre><code class="language-js">const result = sendHttpRequest(
&quot;https://example.com&quot;,
{
method: &quot;POST&quot;,
}
);

if (result.status == &quot;success&quot;) {
alert(result.response.body);
} else if (result.status == &quot;httpError&quot;) {
alert(&quot;Failed with status code &quot; + result.response.statusCode);
} else {
alert(&quot;Failed with network error: &quot; + result.networkError;
}
</code></pre><p><a name="send-mqtt-message"></a></p><h3>Send MQTT message</h3><p>The <code>sendMqttMessages</code> function allows you to connect to an MQTT broker, send (i.e. publish) one or more messages to it, and then disconnect again. The first parameter is the URI of the server/broker, the second (optional) parameter provides options for the connection (e.g. username and password) and the third parameter is a list of all the messages that should be sent.</p><pre><code class="language-js">sendMQTTMessages(
&quot;tcp://192.168.0.42:1234&quot;,
{&quot;username&quot;: &quot;admin&quot;, &quot;password&quot;: &quot;1234&quot;},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,16 @@ constructor(
) {
insertText("wakeOnLan(\"", "\");\n")
}
item(
R.string.action_type_send_http_request,
docRef = "send-http-request",
keywords = setOf("network", "fetch"),
) {
insertText(
"sendHttpRequest(\"https://",
"\", {\"method\": \"GET\"});\n",
)
}
item(
R.string.action_type_send_mqtt_message,
docRef = "send-mqtt-message",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,7 @@ constructor(
.use { okHttpResponse ->
logInfo("HTTP request completed")
val contentFile = if (shortcut.usesResponseBody) {
responseFileStorage.store(
okHttpResponse,
finishNormallyOnTimeout = okHttpResponse.isStreaming() || okHttpResponse.isUnknownLength(),
)
responseFileStorage.store(okHttpResponse)
} else null

val isSuccess = okHttpResponse.code in 200..399
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import ch.rmy.android.framework.extensions.takeUnlessEmpty
import ch.rmy.android.http_shortcuts.http.HttpRequester.Companion.isStreaming
import ch.rmy.android.http_shortcuts.http.HttpRequester.Companion.isUnknownLength
import okhttp3.Response
import java.io.File
import java.io.InputStream
Expand All @@ -15,7 +17,10 @@ class ResponseFileStorage(
private val storeDirectoryUri: Uri?,
) {

fun store(response: Response, finishNormallyOnTimeout: Boolean): DocumentFile {
fun store(
response: Response,
finishNormallyOnTimeout: Boolean = response.isStreaming() || response.isUnknownLength(),
): DocumentFile {
val fileName = "response_$sessionId"

val documentFile = storeDirectoryUri
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ class ResponseFileStorageFactory
constructor(
private val context: Context,
) {
fun create(sessionId: String, storeDirectoryUri: Uri?): ResponseFileStorage =
fun create(sessionId: String, storeDirectoryUri: Uri? = null): ResponseFileStorage =
ResponseFileStorage(context, sessionId, storeDirectoryUri)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ShortcutResponse internal constructor(
val statusCode: Int,
val contentFile: DocumentFile?,
val timing: Duration,
private val charsetOverride: Charset?,
private val charsetOverride: Charset? = null,
) {

val contentType: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import ch.rmy.android.http_shortcuts.scripting.actions.types.PromptTimeActionTyp
import ch.rmy.android.http_shortcuts.scripting.actions.types.RenameShortcutActionType
import ch.rmy.android.http_shortcuts.scripting.actions.types.ScanBarcodeActionType
import ch.rmy.android.http_shortcuts.scripting.actions.types.SelectionActionType
import ch.rmy.android.http_shortcuts.scripting.actions.types.SendHttpRequestActionType
import ch.rmy.android.http_shortcuts.scripting.actions.types.SendIntentActionType
import ch.rmy.android.http_shortcuts.scripting.actions.types.SendMQTTMessagesActionType
import ch.rmy.android.http_shortcuts.scripting.actions.types.SendTCPPacketActionType
Expand Down Expand Up @@ -90,6 +91,7 @@ constructor(
renameShortcutActionType: RenameShortcutActionType,
scanBarcodeActionType: ScanBarcodeActionType,
selectionActionType: SelectionActionType,
sendHttpRequestActionType: SendHttpRequestActionType,
sendIntentActionType: SendIntentActionType,
sendMQTTMessagesActionType: SendMQTTMessagesActionType,
sendTCPPacketActionType: SendTCPPacketActionType,
Expand Down Expand Up @@ -153,6 +155,7 @@ constructor(
renameShortcutActionType,
scanBarcodeActionType,
selectionActionType,
sendHttpRequestActionType,
sendIntentActionType,
sendMQTTMessagesActionType,
sendTCPPacketActionType,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package ch.rmy.android.http_shortcuts.scripting.actions.types

import android.content.Context
import ch.rmy.android.framework.extensions.tryOrLog
import ch.rmy.android.framework.utils.UUIDUtils.newUUID
import ch.rmy.android.http_shortcuts.exceptions.ResponseTooLargeException
import ch.rmy.android.http_shortcuts.http.HttpClientFactory
import ch.rmy.android.http_shortcuts.http.HttpHeaders
import ch.rmy.android.http_shortcuts.http.RequestBuilder
import ch.rmy.android.http_shortcuts.http.ResponseFileStorageFactory
import ch.rmy.android.http_shortcuts.http.ShortcutResponse
import ch.rmy.android.http_shortcuts.scripting.ExecutionContext
import ch.rmy.android.http_shortcuts.utils.UserAgentProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.IOException
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds

class SendHttpRequestAction
@Inject
constructor(
@ApplicationContext
private val context: Context,
private val httpClientFactory: HttpClientFactory,
private val responseFileStorageFactory: ResponseFileStorageFactory,
) : Action<SendHttpRequestAction.Params> {
override suspend fun Params.execute(executionContext: ExecutionContext): JSONObject =
withContext(Dispatchers.IO) {
val client = httpClientFactory.getClient(context)
val storage = responseFileStorageFactory.create(
sessionId = "${executionContext.shortcutId}_${newUUID()}"
)

try {
val response = client.newCall(
RequestBuilder(method, url)
.header(HttpHeaders.CONNECTION, "close")
.userAgent(UserAgentProvider.getUserAgent(context))
.build()
)
.execute()

val contentFile = storage.store(response)

val shortcutResponse = ShortcutResponse(
url = url,
headers = HttpHeaders.parse(response.headers),
statusCode = response.code,
contentFile = contentFile,
timing = (response.receivedResponseAtMillis - response.sentRequestAtMillis).milliseconds,
)

JSONObject(
mapOf(
"status" to if (response.isSuccessful) "success" else "httpError",
"response" to
mapOf(
"body" to try {
shortcutResponse.getContentAsString(context)
} catch (_: ResponseTooLargeException) {
""
},
"headers" to tryOrLog { shortcutResponse.headersAsMultiMap },
"cookies" to tryOrLog { shortcutResponse.cookiesAsMultiMap },
"statusCode" to shortcutResponse.statusCode,
)
)
)
} catch (e: IOException) {
JSONObject(
mapOf(
"status" to "networkError",
"networkError" to e.message,
"response" to null,
)
)
}
}

data class Params(
val url: String,
val method: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ch.rmy.android.http_shortcuts.scripting.actions.types

import ch.rmy.android.http_shortcuts.scripting.ActionAlias
import ch.rmy.android.http_shortcuts.scripting.actions.ActionData
import ch.rmy.android.http_shortcuts.scripting.actions.ActionRunnable
import javax.inject.Inject

class SendHttpRequestActionType
@Inject
constructor(
private val sendHttpRequestAction: SendHttpRequestAction,
) : ActionType {
override val type = TYPE

override fun getActionRunnable(actionDTO: ActionData): ActionRunnable<*> {
val options = actionDTO.getObject(1)
return ActionRunnable(
action = sendHttpRequestAction,
params = SendHttpRequestAction.Params(
url = actionDTO.getString(0) ?: "",
method = (options?.get("method") as? String)?.uppercase() ?: "GET",
),
)
}

override fun getAlias() = ActionAlias(
functionName = FUNCTION_NAME,
functionNameAliases = setOf("sendHTTPRequest"),
parameters = 2,
)

companion object {
private const val TYPE = "send_http_request"
private const val FUNCTION_NAME = "sendHttpRequest"
}
}
2 changes: 2 additions & 0 deletions HTTPShortcuts/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,8 @@
<string name="action_type_send_udp_packet">Send UDP Packet</string>
<!-- Shortcut Action Title: Send a TCP packet to another device on the network -->
<string name="action_type_send_tcp_packet">Send TCP Packet</string>
<!-- Shortcut Action Title: Send an HTTP request -->
<string name="action_type_send_http_request">Send HTTP Request</string>
<!-- Shortcut Action Title: Send an MQTT message to a broker -->
<string name="action_type_send_mqtt_message">Send MQTT Message</string>
<!-- Shortcut Action Title: Abort the execution of the shortcut -->
Expand Down
26 changes: 26 additions & 0 deletions docs/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,32 @@ wakeOnLan('01-23-45-67-89-ab');
wakeOnLan('01-23-45-67-89-ab', '255.255.255.255', 9);
```

<a name="send-http-request"></a>
### Send HTTP request

The `sendHttpRequest` function allows you to send a simple HTTP request. The first parameter is the URL, the second (optional) parameter provides additional options. Currently, the only supported option is "method", which is used to set the HTTP method.

The function returns an object which includes a `status` field, which has the value "success", "httpError" or "networkError". If it is "networkError", you can check the field `networkError` for details. Otherwise, you can check the `response` field for the HTTP response object. It includes fields `body`, `headers`, `cookies` and `statusCode`.

> If you need more options, consider creating a dedicated HTTP Shortcut for your request and invoking it using [enqueueShortcut](#trigger-shortcut) or [executeShortcut](#execute-shortcut) instead.
```js
const result = sendHttpRequest(
"https://example.com",
{
method: "POST",
}
);

if (result.status == "success") {
alert(result.response.body);
} else if (result.status == "httpError") {
alert("Failed with status code " + result.response.statusCode);
} else {
alert("Failed with network error: " + result.networkError);
}
```

<a name="send-mqtt-message"></a>
### Send MQTT message

Expand Down

0 comments on commit f579092

Please sign in to comment.