Skip to content

Commit

Permalink
Merge pull request #393 from anoop-b/webhook
Browse files Browse the repository at this point in the history
Added webhook module for URL forwarding
  • Loading branch information
TrianguloY authored Nov 19, 2024
2 parents 0b44c82 + 4025417 commit 8d41228
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public JSONObject buildBuiltIn(Context cntx) throws JSONException {
.put("action", "unshort")
.put("enabled", false)
)
.put("Send everything to webhook", new JSONObject()
.put("regex", ".*")
.put("action", "webhook")
.put("enabled", false)
)
;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.trianguloy.urlchecker.modules.list.UnshortenModule;
import com.trianguloy.urlchecker.modules.list.UriPartsModule;
import com.trianguloy.urlchecker.modules.list.VirusTotalModule;
import com.trianguloy.urlchecker.modules.list.WebhookModule;
import com.trianguloy.urlchecker.utilities.generics.GenericPref;

import java.util.ArrayList;
Expand Down Expand Up @@ -47,6 +48,7 @@ public class ModuleManager {
modules.add(new UriPartsModule());
modules.add(new PatternModule());
modules.add(new HostsModule());
modules.add(new WebhookModule());
// new modules should preferably be added directly above this line
modules.add(new FlagsModule());
modules.add(new DebugModule());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package com.trianguloy.urlchecker.modules.list;

import android.app.AlertDialog;
import android.content.Context;
import android.text.Editable;
import android.util.Pair;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.trianguloy.urlchecker.R;
import com.trianguloy.urlchecker.activities.ModulesActivity;
import com.trianguloy.urlchecker.dialogs.MainDialog;
import com.trianguloy.urlchecker.modules.AModuleConfig;
import com.trianguloy.urlchecker.modules.AModuleData;
import com.trianguloy.urlchecker.modules.AModuleDialog;
import com.trianguloy.urlchecker.modules.AutomationRules;
import com.trianguloy.urlchecker.url.UrlData;
import com.trianguloy.urlchecker.utilities.generics.GenericPref;
import com.trianguloy.urlchecker.utilities.methods.AndroidUtils;
import com.trianguloy.urlchecker.utilities.methods.HttpUtils;
import com.trianguloy.urlchecker.utilities.methods.JavaUtils;
import com.trianguloy.urlchecker.utilities.wrappers.DefaultTextWatcher;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/** This module sends the current url to a custom webhook */
public class WebhookModule extends AModuleData {

public static GenericPref.Str WEBHOOK_URL_PREF(Context cntx) {
return new GenericPref.Str("webhook_url", "", cntx);
}

public static GenericPref.Str WEBHOOK_BODY_PREF(Context cntx) {
return new GenericPref.Str("webhook_body", WebhookConfig.DEFAULT, cntx);
}

@Override
public String getId() {
return "webhook";
}

@Override
public int getName() {
return R.string.mWebhook_name;
}

@Override
public boolean isEnabledByDefault() {
return false;
}

@Override
public AModuleDialog getDialog(MainDialog cntx) {
return new WebhookDialog(cntx);
}

@Override
public AModuleConfig getConfig(ModulesActivity cntx) {
return new WebhookConfig(cntx);
}

@Override
public List<AutomationRules.Automation<AModuleDialog>> getAutomations() {
return (List<AutomationRules.Automation<AModuleDialog>>) (List<?>) WebhookDialog.AUTOMATIONS;
}
}

class WebhookDialog extends AModuleDialog {

private static final Executor executor = Executors.newSingleThreadExecutor();

private final GenericPref.Str webhookUrl;
private final GenericPref.Str webhookBody;
private TextView statusText;
private Button statusButton;

static final List<AutomationRules.Automation<WebhookDialog>> AUTOMATIONS = List.of(
new AutomationRules.Automation<>(
"webhook",
R.string.mWebhook_auto_send,
WebhookDialog::sendToWebhook
)
);

public WebhookDialog(MainDialog dialog) {
super(dialog);
webhookUrl = WebhookModule.WEBHOOK_URL_PREF(dialog);
webhookBody = WebhookModule.WEBHOOK_BODY_PREF(dialog);
}

@Override
public int getLayoutId() {
return R.layout.button_text;
}

@Override
public void onInitialize(View views) {
statusText = views.findViewById(R.id.text);
statusButton = views.findViewById(R.id.button);

statusButton.setText(R.string.mWebhook_send);
statusButton.setOnClickListener(v -> sendToWebhook());
}

@Override
public void onDisplayUrl(UrlData urlData) {
statusText.setText("");
AndroidUtils.clearRoundedColor(statusText);
}

private void sendToWebhook() {
statusText.setText(R.string.mWebhook_sending);
statusButton.setEnabled(false);

executor.execute(() -> {
var sent = send(webhookUrl.get(), getUrl(), webhookBody.get());
getActivity().runOnUiThread(() -> {
statusText.setText(sent ? R.string.mWebhook_success : R.string.mWebhook_error);
if (!sent) {
AndroidUtils.setRoundedColor(R.color.bad, statusText);
}
statusButton.setEnabled(true);
});
});
}

/** Performs the send action */
static boolean send(String webhook, String url, String body) {
try {
var json = body
.replace("$URL$", url)
.replace("$TIMESTAMP$", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).format(new Date()));

var responseCode = HttpUtils.performPOSTJSON(webhook, json);
return responseCode >= 200 && responseCode < 300;
} catch (IOException e) {
AndroidUtils.assertError("Failed to send to webhook", e);
return false;
}

}
}

class WebhookConfig extends AModuleConfig {

public static final String DEFAULT = "{\"url\":\"$URL$\",\"timestamp\":\"$TIMESTAMP$\"}";

private static final List<Pair<String, String>> TEMPLATES = List.of(
Pair.create("custom", DEFAULT),
Pair.create("Discord", "{\"content\":\"$URL$ @ $TIMESTAMP$\"}"),
Pair.create("Slack", "{\"text\":\"$URL$ @ $TIMESTAMP$\"}"),
Pair.create("Teams", "{\"text\":\"$URL$ @ $TIMESTAMP$\"}")
);

private final GenericPref.Str webhookUrl;
private final GenericPref.Str webhookBody;

public WebhookConfig(ModulesActivity activity) {
super(activity);
webhookUrl = WebhookModule.WEBHOOK_URL_PREF(activity);
webhookBody = WebhookModule.WEBHOOK_BODY_PREF(activity);
}

@Override
public int cannotEnableErrorId() {
return webhookUrl.get().isEmpty() || webhookBody.get().isEmpty() ? R.string.mWebhook_missing_config : -1;
}

@Override
public int getLayoutId() {
return R.layout.config_webhook;
}

@Override
public void onInitialize(View views) {
var url = views.<EditText>findViewById(R.id.webhook_url);
var body = views.<EditText>findViewById(R.id.webhook_body);
var test = views.findViewById(R.id.webhook_test);

// configs
webhookUrl.attachToEditText(url);
webhookBody.attachToEditText(body);

// check disable
var nonEmpty = new DefaultTextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (s.length() == 0) {
disable();
test.setEnabled(false);
} else {
test.setEnabled(true);
}
}
};
url.addTextChangedListener(nonEmpty);
body.addTextChangedListener(nonEmpty);

test.setEnabled(cannotEnableErrorId() == -1);

// click template
views.findViewById(R.id.webhook_templates).setOnClickListener(v ->
new AlertDialog.Builder(v.getContext())
.setTitle(R.string.mWebhook_templates)
.setItems(JavaUtils.mapEach(TEMPLATES, e -> e.first).toArray(new String[0]), (dialog, which) ->
body.setText(TEMPLATES.get(which).second))
.show());

// click test
test.setOnClickListener(v -> {
test.setEnabled(false);
new Thread(() -> {
var ok = WebhookDialog.send(webhookUrl.get(), webhookUrl.get(), webhookBody.get());
getActivity().runOnUiThread(() -> {
test.setEnabled(true);
Toast.makeText(v.getContext(), ok ? R.string.mWebhook_success : R.string.mWebhook_error, Toast.LENGTH_SHORT).show();
});
}).start();
});
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.trianguloy.urlchecker.utilities.methods;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import javax.net.ssl.HttpsURLConnection;

Expand Down Expand Up @@ -42,4 +45,22 @@ public static String performPOST(String url, String body) throws IOException {
: conn.getErrorStream()
);
}

/**
* Same as performPOST, but for a different module.
* TODO: use a unique method for all connections (merge with the virusTotal v3 branch issue)
*/
public static int performPOSTJSON(String url, String body) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);

try (OutputStream os = conn.getOutputStream()) {
byte[] input = body.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}

return conn.getResponseCode();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ static <T> List<T> toList(Iterator<T> iterator) {
return list;
}

/** returns list.map{ mapper(it) } but using <24 api android java */
static <I, O> List<O> mapEach(List<I> list, Function<I, O> mapper) {
var result = new ArrayList<O>();
for (var i : list) {
result.add(mapper.apply(i));
}
return result;
}

/**
* Converts a string into a json object, returns empty on failure
*/
Expand Down
52 changes: 52 additions & 0 deletions app/src/main/res/layout/config_webhook.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mWebhook_desc" />

<EditText
android:id="@+id/webhook_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/mWebhook_url_hint"
android:inputType="textUri" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">

<EditText
android:id="@+id/webhook_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:gravity="start|top"
android:hint="@string/mWebhook_body_hint"
android:inputType="textMultiLine" />

<Button
android:id="@+id/webhook_templates"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minHeight="0dp"
android:text="@string/mWebhook_templates" />

</LinearLayout>

<Button
android:id="@+id/webhook_test"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mWebhook_test" />

</LinearLayout>
15 changes: 15 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,20 @@ This is an advanced version of the 'Queries Remover' module."</string>
<string name="mParts_empty">(empty)</string>
<string name="mParts_copy">Part copied to clipboard</string>
<!-- -->
<string name="mWebhook_name">Webhook</string>
<string name="mWebhook_desc">This module sends URLs to a configured webhook endpoint. Each URL is sent with a timestamp, allowing you to collect and process URLs on your own server.\n\nConfigure your webhook URL and body below to start sending URLs.\nFor the body, you can use the placeholders $URL$ and $TIMESTAMP$.</string>
<string name="mWebhook_url_hint">Webhook URL</string>
<string name="mWebhook_body_hint">Webhook body</string>
<string name="mWebhook_send">Send to Webhook</string>
<string name="mWebhook_auto">Automatically send URLs</string>
<string name="mWebhook_auto_send">Send to webhook</string>
<string name="mWebhook_missing_config">Missing webhook url or body</string>
<string name="mWebhook_sending">Sending to webhook…</string>
<string name="mWebhook_success">Successfully sent to webhook</string>
<string name="mWebhook_error">Failed to send to webhook</string>
<string name="mWebhook_templates">Templates</string>
<string name="mWebhook_test">Test webhook</string>
<!-- -->
<string name="mDrawer_name">Drawer</string>
<string name="mDrawer_desc">All modules below this will be hidden in a drawer. The modules will still work, but will be hidden until you open the drawer. Here you can place modules you rarely use, to avoid clutter.</string>
<!-- -->
Expand All @@ -407,4 +421,5 @@ This is an advanced version of the 'Queries Remover' module."</string>
<string name="mChg_updated">The app has been updated.</string>
<string name="mChg_current">Current version: %s</string>
<string name="mChg_view">View changes</string>

</resources>

0 comments on commit 8d41228

Please sign in to comment.