From 43278562df752f19fb98d28471251800212eabed Mon Sep 17 00:00:00 2001 From: Eyzer <55920527+Eyzer@users.noreply.github.com> Date: Mon, 14 Sep 2026 03:33:56 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E6=AD=A3=E5=88=99=E4=B8=AD=E5=8C=85=E5=90=AB"=3D"?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=EF=BC=88=E5=A6=82(=3F=3D...)=E5=85=88?= =?UTF-8?q?=E8=A1=8C=E6=96=AD=E8=A8=80=EF=BC=89=E6=97=B6=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/kotlin/cn/ppps/forwarder/entity/MsgInfo.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/cn/ppps/forwarder/entity/MsgInfo.kt b/app/src/main/kotlin/cn/ppps/forwarder/entity/MsgInfo.kt index ad60183b3b..feca4a2549 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/entity/MsgInfo.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/entity/MsgInfo.kt @@ -167,7 +167,8 @@ data class MsgInfo( } val tagName = tag.removePrefix("{{").removeSuffix("}}") - val tagRegex = "\\{\\{${tagName}###([^=]+)===(.*?)\\}\\}".toRegex() + //使用 (.+?) 而非 ([^=]+):正则部分可能包含 "=" 字符(如 (?=...) 正向先行断言),([^=]+) 会提前截断导致标签解析失败 + val tagRegex = "\\{\\{${tagName}###(.+?)===(.*?)\\}\\}".toRegex() tagRegex.findAll(result).forEach { try { Log.d("MsgInfo", "tagRegex: ${it.value}, ${it.groupValues}") From c72f70067e8c4fdc33c697c758dd3d6fee96a78f Mon Sep 17 00:00:00 2001 From: Eyzer <55920527+Eyzer@users.noreply.github.com> Date: Mon, 14 Sep 2026 03:33:56 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9AWxPusher?= =?UTF-8?q?=E8=BD=AC=E5=8F=91=E9=80=9A=E9=81=93=EF=BC=88=E6=A0=87=E5=87=86?= =?UTF-8?q?/=E6=9E=81=E7=AE=80=E6=8E=A8=E9=80=81=EF=BC=89=EF=BC=9BWebhook/?= =?UTF-8?q?WxPusher=E9=80=9A=E9=81=93=E7=BA=A7=E6=AD=A3=E5=88=99=E6=8F=90?= =?UTF-8?q?=E5=8F=96=EF=BC=9B=E4=BF=AE=E5=A4=8DWebhook=20GET=20URL?= =?UTF-8?q?=E6=8B=BC=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../forwarder/database/entity/LogsDetail.kt | 2 + .../ppps/forwarder/database/entity/Sender.kt | 2 + .../cn/ppps/forwarder/entity/MsgInfo.kt | 9 +- .../forwarder/entity/result/WxpusherResult.kt | 8 + .../entity/setting/WebhookSetting.kt | 1 + .../entity/setting/WxpusherSetting.kt | 30 ++ .../forwarder/fragment/SendersFragment.kt | 10 + .../fragment/senders/WebhookFragment.kt | 4 +- .../fragment/senders/WxpusherFragment.kt | 262 +++++++++++++ .../cn/ppps/forwarder/utils/Constants.kt | 1 + .../cn/ppps/forwarder/utils/SendUtils.kt | 7 + .../forwarder/utils/sender/WebhookUtils.kt | 10 +- .../forwarder/utils/sender/WxpusherUtils.kt | 110 ++++++ app/src/main/res/drawable/icon_wxpusher.xml | 18 + .../res/layout/fragment_senders_webhook.xml | 33 ++ .../res/layout/fragment_senders_wxpusher.xml | 363 ++++++++++++++++++ app/src/main/res/values-en/strings.xml | 28 ++ app/src/main/res/values-zh-rCN/strings.xml | 28 ++ app/src/main/res/values-zh-rTW/strings.xml | 28 ++ app/src/main/res/values/strings.xml | 28 ++ 20 files changed, 977 insertions(+), 5 deletions(-) create mode 100644 app/src/main/kotlin/cn/ppps/forwarder/entity/result/WxpusherResult.kt create mode 100644 app/src/main/kotlin/cn/ppps/forwarder/entity/setting/WxpusherSetting.kt create mode 100644 app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/WxpusherFragment.kt create mode 100644 app/src/main/kotlin/cn/ppps/forwarder/utils/sender/WxpusherUtils.kt create mode 100644 app/src/main/res/drawable/icon_wxpusher.xml create mode 100644 app/src/main/res/layout/fragment_senders_wxpusher.xml diff --git a/app/src/main/kotlin/cn/ppps/forwarder/database/entity/LogsDetail.kt b/app/src/main/kotlin/cn/ppps/forwarder/database/entity/LogsDetail.kt index aa6b2d0417..26c17b99e9 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/database/entity/LogsDetail.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/database/entity/LogsDetail.kt @@ -20,6 +20,7 @@ import cn.ppps.forwarder.utils.TYPE_URL_SCHEME import cn.ppps.forwarder.utils.TYPE_WEBHOOK import cn.ppps.forwarder.utils.TYPE_WEWORK_AGENT import cn.ppps.forwarder.utils.TYPE_WEWORK_ROBOT +import cn.ppps.forwarder.utils.TYPE_WXPUSHER import kotlinx.parcelize.Parcelize import java.util.Date @@ -70,6 +71,7 @@ data class LogsDetail( TYPE_FEISHU_APP -> R.drawable.icon_feishu_app TYPE_URL_SCHEME -> R.drawable.icon_url_scheme TYPE_SOCKET -> R.drawable.icon_socket + TYPE_WXPUSHER -> R.drawable.icon_wxpusher else -> R.drawable.icon_sms } } diff --git a/app/src/main/kotlin/cn/ppps/forwarder/database/entity/Sender.kt b/app/src/main/kotlin/cn/ppps/forwarder/database/entity/Sender.kt index 09a3401ad9..d92b495e33 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/database/entity/Sender.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/database/entity/Sender.kt @@ -22,6 +22,7 @@ import cn.ppps.forwarder.utils.TYPE_URL_SCHEME import cn.ppps.forwarder.utils.TYPE_WEBHOOK import cn.ppps.forwarder.utils.TYPE_WEWORK_AGENT import cn.ppps.forwarder.utils.TYPE_WEWORK_ROBOT +import cn.ppps.forwarder.utils.TYPE_WXPUSHER import kotlinx.parcelize.Parcelize import java.util.Date @@ -55,6 +56,7 @@ data class Sender( TYPE_FEISHU_APP -> R.drawable.icon_feishu_app TYPE_URL_SCHEME -> R.drawable.icon_url_scheme TYPE_SOCKET -> R.drawable.icon_socket + TYPE_WXPUSHER -> R.drawable.icon_wxpusher else -> R.drawable.icon_sms } diff --git a/app/src/main/kotlin/cn/ppps/forwarder/entity/MsgInfo.kt b/app/src/main/kotlin/cn/ppps/forwarder/entity/MsgInfo.kt index feca4a2549..6e76a96b21 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/entity/MsgInfo.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/entity/MsgInfo.kt @@ -142,7 +142,7 @@ data class MsgInfo( var newContent = this val lineArray = regexReplace.split("\\n".toRegex()).toTypedArray() for (line in lineArray) { - val lineSplit = line.split("===".toRegex()).toTypedArray() + val lineSplit = line.split("===").toTypedArray() if (lineSplit.isNotEmpty()) { val regex = lineSplit[0] val replacement = @@ -158,6 +158,11 @@ data class MsgInfo( } } + //对外提供正则替换能力:供发送通道对已渲染内容执行 正则===替换 规则(每行一条),如提取验证码 + fun applyRegexReplace(content: String, regexReplace: String): String { + return content.regexReplace(regexReplace) + } + //替换标签(支持正则替换) private fun String.replaceTag(tag: String, info: String, encoderName: String = "", ignoreCase: Boolean = true): String { var result = when (encoderName) { @@ -167,7 +172,7 @@ data class MsgInfo( } val tagName = tag.removePrefix("{{").removeSuffix("}}") - //使用 (.+?) 而非 ([^=]+):正则部分可能包含 "=" 字符(如 (?=...) 正向先行断言),([^=]+) 会提前截断导致标签解析失败 + //使用 (.+?) 而非 ([^=]+):正则部分可能包含 "=" 字符(如 (?=...) 正向前瞻),([^=]+) 会提前截断导致标签解析失败 val tagRegex = "\\{\\{${tagName}###(.+?)===(.*?)\\}\\}".toRegex() tagRegex.findAll(result).forEach { try { diff --git a/app/src/main/kotlin/cn/ppps/forwarder/entity/result/WxpusherResult.kt b/app/src/main/kotlin/cn/ppps/forwarder/entity/result/WxpusherResult.kt new file mode 100644 index 0000000000..44abe83e43 --- /dev/null +++ b/app/src/main/kotlin/cn/ppps/forwarder/entity/result/WxpusherResult.kt @@ -0,0 +1,8 @@ +package cn.ppps.forwarder.entity.result + +data class WxpusherResult( + var code: Long = 0, + var msg: String = "", + var data: Any? = null, + var success: Boolean = false, +) diff --git a/app/src/main/kotlin/cn/ppps/forwarder/entity/setting/WebhookSetting.kt b/app/src/main/kotlin/cn/ppps/forwarder/entity/setting/WebhookSetting.kt index 2cd010ef13..f030340614 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/entity/setting/WebhookSetting.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/entity/setting/WebhookSetting.kt @@ -17,6 +17,7 @@ data class WebhookSetting( val proxyAuthenticator: Boolean = false, val proxyUsername: String = "", val proxyPassword: String = "", + val regexReplace: String = "", ) : Serializable { fun getMethodCheckId(): Int { return when (method) { diff --git a/app/src/main/kotlin/cn/ppps/forwarder/entity/setting/WxpusherSetting.kt b/app/src/main/kotlin/cn/ppps/forwarder/entity/setting/WxpusherSetting.kt new file mode 100644 index 0000000000..d31bcc39f2 --- /dev/null +++ b/app/src/main/kotlin/cn/ppps/forwarder/entity/setting/WxpusherSetting.kt @@ -0,0 +1,30 @@ +package cn.ppps.forwarder.entity.setting + +import java.io.Serializable + +data class WxpusherSetting( + val appToken: String = "", + val uids: String = "", + val topicIds: String = "", + val spt: String = "", + val contentType: Int = 1, + val summaryTemplate: String = "", + val regexReplace: String = "", +) : Serializable { + + //SPT极简推送模式:自己发给自己,无需创建应用 + val isSimplePush: Boolean + get() = spt.isNotBlank() && appToken.isBlank() + + fun getPushTypeCheckId(): Int { + return if (isSimplePush) cn.ppps.forwarder.R.id.rb_simple_push else cn.ppps.forwarder.R.id.rb_standard_push + } + + fun getContentTypeCheckId(): Int { + return when (contentType) { + 2 -> cn.ppps.forwarder.R.id.rb_content_html + 3 -> cn.ppps.forwarder.R.id.rb_content_markdown + else -> cn.ppps.forwarder.R.id.rb_content_text + } + } +} diff --git a/app/src/main/kotlin/cn/ppps/forwarder/fragment/SendersFragment.kt b/app/src/main/kotlin/cn/ppps/forwarder/fragment/SendersFragment.kt index 9b8e8cacbb..ab7c6515dc 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/fragment/SendersFragment.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/fragment/SendersFragment.kt @@ -35,6 +35,7 @@ import cn.ppps.forwarder.fragment.senders.UrlSchemeFragment import cn.ppps.forwarder.fragment.senders.WebhookFragment import cn.ppps.forwarder.fragment.senders.WeworkAgentFragment import cn.ppps.forwarder.fragment.senders.WeworkRobotFragment +import cn.ppps.forwarder.fragment.senders.WxpusherFragment import cn.ppps.forwarder.utils.KEY_SENDER_CLONE import cn.ppps.forwarder.utils.KEY_SENDER_ID import cn.ppps.forwarder.utils.KEY_SENDER_TYPE @@ -55,6 +56,7 @@ import cn.ppps.forwarder.utils.TYPE_URL_SCHEME import cn.ppps.forwarder.utils.TYPE_WEBHOOK import cn.ppps.forwarder.utils.TYPE_WEWORK_AGENT import cn.ppps.forwarder.utils.TYPE_WEWORK_ROBOT +import cn.ppps.forwarder.utils.TYPE_WXPUSHER import cn.ppps.forwarder.utils.XToastUtils import com.scwang.smartrefresh.layout.api.RefreshLayout import com.xuexiang.xaop.annotation.SingleClick @@ -200,6 +202,13 @@ class SendersFragment : BaseFragment(), CoreAnim.slide, R.drawable.icon_socket ), + PageInfo( + getString(R.string.wxpusher), + "cn.ppps.forwarder.fragment.senders.WxpusherFragment", + "{\"\":\"\"}", + CoreAnim.slide, + R.drawable.icon_wxpusher + ), ) override fun viewBindingInflate( @@ -354,6 +363,7 @@ class SendersFragment : BaseFragment(), TYPE_FEISHU_APP -> FeishuAppFragment::class.java TYPE_URL_SCHEME -> UrlSchemeFragment::class.java TYPE_SOCKET -> SocketFragment::class.java + TYPE_WXPUSHER -> WxpusherFragment::class.java else -> DingtalkGroupRobotFragment::class.java } } diff --git a/app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/WebhookFragment.kt b/app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/WebhookFragment.kt index d9e6253543..7d4cadf68f 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/WebhookFragment.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/WebhookFragment.kt @@ -134,6 +134,7 @@ class WebhookFragment : BaseFragment(), View.OnC binding!!.etSecret.setText(settingVo.secret) binding!!.etResponse.setText(settingVo.response) binding!!.etWebParams.setText(settingVo.webParams) + binding!!.etRegexReplace.setText(settingVo.regexReplace) //set header for ((key, value) in settingVo.headers) { addHeaderItemLinearLayout( @@ -255,6 +256,7 @@ class WebhookFragment : BaseFragment(), View.OnC val secret = binding!!.etSecret.text.toString().trim() val response = binding!!.etResponse.text.toString().trim() val webParams = binding!!.etWebParams.text.toString().trim() + val regexReplace = binding!!.etRegexReplace.text.toString().trim() val headers = getHeadersFromHeaderItemMap(headerItemMap) val proxyType: Proxy.Type = when (binding!!.rgProxyType.checkedRadioButtonId) { @@ -276,7 +278,7 @@ class WebhookFragment : BaseFragment(), View.OnC throw Exception(getString(R.string.invalid_username_or_password)) } - return WebhookSetting(method, webServer, secret, response, webParams, headers, proxyType, proxyHost, proxyPort, proxyAuthenticator, proxyUsername, proxyPassword) + return WebhookSetting(method, webServer, secret, response, webParams, headers, proxyType, proxyHost, proxyPort, proxyAuthenticator, proxyUsername, proxyPassword, regexReplace) } diff --git a/app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/WxpusherFragment.kt b/app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/WxpusherFragment.kt new file mode 100644 index 0000000000..03ac2271e9 --- /dev/null +++ b/app/src/main/kotlin/cn/ppps/forwarder/fragment/senders/WxpusherFragment.kt @@ -0,0 +1,262 @@ +package cn.ppps.forwarder.fragment.senders + +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.RadioGroup +import androidx.fragment.app.viewModels +import com.google.gson.Gson +import cn.ppps.forwarder.R +import cn.ppps.forwarder.core.BaseFragment +import cn.ppps.forwarder.core.Core +import cn.ppps.forwarder.database.entity.Sender +import cn.ppps.forwarder.database.viewmodel.BaseViewModelFactory +import cn.ppps.forwarder.database.viewmodel.SenderViewModel +import cn.ppps.forwarder.databinding.FragmentSendersWxpusherBinding +import cn.ppps.forwarder.entity.MsgInfo +import cn.ppps.forwarder.entity.setting.WxpusherSetting +import cn.ppps.forwarder.utils.CommonUtils +import cn.ppps.forwarder.utils.EVENT_TOAST_ERROR +import cn.ppps.forwarder.utils.KEY_SENDER_CLONE +import cn.ppps.forwarder.utils.KEY_SENDER_ID +import cn.ppps.forwarder.utils.KEY_SENDER_TEST +import cn.ppps.forwarder.utils.KEY_SENDER_TYPE +import cn.ppps.forwarder.utils.Log +import cn.ppps.forwarder.utils.SettingUtils +import cn.ppps.forwarder.utils.XToastUtils +import cn.ppps.forwarder.utils.sender.WxpusherUtils +import com.jeremyliao.liveeventbus.LiveEventBus +import com.xuexiang.xaop.annotation.SingleClick +import com.xuexiang.xpage.annotation.Page +import com.xuexiang.xrouter.annotation.AutoWired +import com.xuexiang.xrouter.launcher.XRouter +import com.xuexiang.xui.utils.CountDownButtonHelper +import com.xuexiang.xui.widget.actionbar.TitleBar +import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction +import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog +import io.reactivex.SingleObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import java.util.Date + +@Page(name = "WxPusher") +@Suppress("PrivatePropertyName") +class WxpusherFragment : BaseFragment(), View.OnClickListener { + + private val TAG: String = WxpusherFragment::class.java.simpleName + private var titleBar: TitleBar? = null + private val viewModel by viewModels { BaseViewModelFactory(context) } + private var mCountDownHelper: CountDownButtonHelper? = null + + @JvmField + @AutoWired(name = KEY_SENDER_ID) + var senderId: Long = 0 + + @JvmField + @AutoWired(name = KEY_SENDER_TYPE) + var senderType: Int = 0 + + @JvmField + @AutoWired(name = KEY_SENDER_CLONE) + var isClone: Boolean = false + + override fun initArgs() { + XRouter.getInstance().inject(this) + } + + override fun viewBindingInflate( + inflater: LayoutInflater, + container: ViewGroup, + ): FragmentSendersWxpusherBinding { + return FragmentSendersWxpusherBinding.inflate(inflater, container, false) + } + + override fun initTitle(): TitleBar? { + titleBar = super.initTitle()!!.setImmersive(false).setTitle(R.string.wxpusher) + return titleBar + } + + /** + * 初始化控件 + */ + override fun initViews() { + //测试按钮增加倒计时,避免重复点击 + mCountDownHelper = CountDownButtonHelper(binding!!.btnTest, SettingUtils.requestTimeout) + mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener { + override fun onCountDown(time: Int) { + binding!!.btnTest.text = String.format(getString(R.string.seconds_n), time) + } + + override fun onFinished() { + binding!!.btnTest.text = getString(R.string.test) + } + }) + + //创建标签按钮 + CommonUtils.createTagButtons(requireContext(), binding!!.glSummaryTemplate, binding!!.etSummaryTemplate) + + //新增 + if (senderId <= 0) { + titleBar?.setSubTitle(getString(R.string.add_sender)) + binding!!.btnDel.setText(R.string.discard) + return + } + + //编辑 + binding!!.btnDel.setText(R.string.del) + Core.sender.get(senderId).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : SingleObserver { + override fun onSubscribe(d: Disposable) {} + + override fun onError(e: Throwable) { + e.printStackTrace() + Log.e(TAG, "onError:$e") + } + + override fun onSuccess(sender: Sender) { + if (isClone) { + titleBar?.setSubTitle(getString(R.string.clone_sender) + ": " + sender.name) + binding!!.btnDel.setText(R.string.discard) + } else { + titleBar?.setSubTitle(getString(R.string.edit_sender) + ": " + sender.name) + } + binding!!.etName.setText(sender.name) + binding!!.sbEnable.isChecked = sender.status == 1 + val settingVo = Gson().fromJson(sender.jsonSetting, WxpusherSetting::class.java) + Log.d(TAG, settingVo.toString()) + if (settingVo != null) { + binding!!.rgPushType.check(settingVo.getPushTypeCheckId()) + binding!!.etAppToken.setText(settingVo.appToken) + binding!!.etUids.setText(settingVo.uids) + binding!!.etTopicIds.setText(settingVo.topicIds) + binding!!.etSpt.setText(settingVo.spt) + binding!!.rgContentType.check(settingVo.getContentTypeCheckId()) + binding!!.etSummaryTemplate.setText(settingVo.summaryTemplate) + binding!!.etRegexReplace.setText(settingVo.regexReplace) + } + togglePushTypeLayouts() + } + }) + } + + override fun initListeners() { + binding!!.btnTest.setOnClickListener(this) + binding!!.btnDel.setOnClickListener(this) + binding!!.btnSave.setOnClickListener(this) + binding!!.rgPushType.setOnCheckedChangeListener { _: RadioGroup?, _: Int -> togglePushTypeLayouts() } + LiveEventBus.get(KEY_SENDER_TEST, String::class.java).observe(this) { mCountDownHelper?.finish() } + } + + //切换标准推送与SPT极简推送的显示 + private fun togglePushTypeLayouts() { + if (binding!!.rgPushType.checkedRadioButtonId == R.id.rb_simple_push) { + binding!!.layoutStandardPush.visibility = View.GONE + binding!!.layoutSimplePush.visibility = View.VISIBLE + } else { + binding!!.layoutStandardPush.visibility = View.VISIBLE + binding!!.layoutSimplePush.visibility = View.GONE + } + } + + @SingleClick + override fun onClick(v: View) { + try { + when (v.id) { + + R.id.btn_test -> { + mCountDownHelper?.start() + Thread { + try { + val settingVo = checkSetting() + Log.d(TAG, settingVo.toString()) + val name = binding!!.etName.text.toString().trim().takeIf { it.isNotEmpty() } ?: getString(R.string.test_sender_name) + val msgInfo = MsgInfo("sms", getString(R.string.test_phone_num), String.format(getString(R.string.test_sender_sms), name), Date(), getString(R.string.test_sim_info)) + WxpusherUtils.sendMsg(settingVo, msgInfo) + } catch (e: Exception) { + e.printStackTrace() + Log.e(TAG, "onClick error:$e") + LiveEventBus.get(EVENT_TOAST_ERROR, String::class.java).post(e.message.toString()) + } + LiveEventBus.get(KEY_SENDER_TEST, String::class.java).post("finish") + }.start() + return + } + + R.id.btn_del -> { + if (senderId <= 0 || isClone) { + popToBack() + return + } + + MaterialDialog.Builder(requireContext()).title(R.string.delete_sender_title).content(R.string.delete_sender_tips).positiveText(R.string.lab_yes).negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? -> + viewModel.delete(senderId) + XToastUtils.success(R.string.delete_sender_toast) + popToBack() + }.show() + return + } + + R.id.btn_save -> { + val name = binding!!.etName.text.toString().trim() + if (TextUtils.isEmpty(name)) { + throw Exception(getString(R.string.invalid_name)) + } + + val status = if (binding!!.sbEnable.isChecked) 1 else 0 + val settingVo = checkSetting() + if (isClone) senderId = 0 + val senderNew = Sender(senderId, senderType, name, Gson().toJson(settingVo), status) + Log.d(TAG, senderNew.toString()) + + viewModel.insertOrUpdate(senderNew) + XToastUtils.success(R.string.tipSaveSuccess) + popToBack() + return + } + } + } catch (e: Exception) { + XToastUtils.error(e.message.toString()) + e.printStackTrace() + Log.e(TAG, "onClick error:$e") + } + } + + private fun checkSetting(): WxpusherSetting { + val appToken = binding!!.etAppToken.text.toString().trim() + val uids = binding!!.etUids.text.toString().trim() + val topicIds = binding!!.etTopicIds.text.toString().trim() + val spt = binding!!.etSpt.text.toString().trim() + val summary = binding!!.etSummaryTemplate.text.toString().trim() + val regexReplace = binding!!.etRegexReplace.text.toString().trim() + + if (binding!!.rgPushType.checkedRadioButtonId == R.id.rb_simple_push) { + //SPT极简推送 + if (TextUtils.isEmpty(spt)) { + throw Exception(getString(R.string.invalid_wxpusher_spt)) + } + } else { + //标准推送 + if (TextUtils.isEmpty(appToken)) { + throw Exception(getString(R.string.invalid_wxpusher_app_token)) + } + if (TextUtils.isEmpty(uids) && TextUtils.isEmpty(topicIds)) { + throw Exception(getString(R.string.invalid_wxpusher_uids)) + } + } + + val contentType = when (binding!!.rgContentType.checkedRadioButtonId) { + R.id.rb_content_html -> 2 + R.id.rb_content_markdown -> 3 + else -> 1 + } + + return WxpusherSetting(appToken, uids, topicIds, spt, contentType, summary, regexReplace) + } + + override fun onDestroyView() { + if (mCountDownHelper != null) mCountDownHelper!!.recycle() + super.onDestroyView() + } + +} diff --git a/app/src/main/kotlin/cn/ppps/forwarder/utils/Constants.kt b/app/src/main/kotlin/cn/ppps/forwarder/utils/Constants.kt index 37f3bba72e..3fdf2b1e22 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/utils/Constants.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/utils/Constants.kt @@ -150,6 +150,7 @@ const val TYPE_DINGTALK_INNER_ROBOT = 12 const val TYPE_FEISHU_APP = 13 const val TYPE_URL_SCHEME = 14 const val TYPE_SOCKET = 15 +const val TYPE_WXPUSHER = 16 //前台服务 const val FRONT_NOTIFY_ID = 0x1010 diff --git a/app/src/main/kotlin/cn/ppps/forwarder/utils/SendUtils.kt b/app/src/main/kotlin/cn/ppps/forwarder/utils/SendUtils.kt index 228b991cbc..917e91bc36 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/utils/SendUtils.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/utils/SendUtils.kt @@ -26,6 +26,7 @@ import cn.ppps.forwarder.entity.setting.UrlSchemeSetting import cn.ppps.forwarder.entity.setting.WebhookSetting import cn.ppps.forwarder.entity.setting.WeworkAgentSetting import cn.ppps.forwarder.entity.setting.WeworkRobotSetting +import cn.ppps.forwarder.entity.setting.WxpusherSetting import cn.ppps.forwarder.utils.sender.BarkUtils import cn.ppps.forwarder.utils.sender.DingtalkGroupRobotUtils import cn.ppps.forwarder.utils.sender.DingtalkInnerRobotUtils @@ -42,6 +43,7 @@ import cn.ppps.forwarder.utils.sender.UrlSchemeUtils import cn.ppps.forwarder.utils.sender.WebhookUtils import cn.ppps.forwarder.utils.sender.WeworkAgentUtils import cn.ppps.forwarder.utils.sender.WeworkRobotUtils +import cn.ppps.forwarder.utils.sender.WxpusherUtils import cn.ppps.forwarder.workers.SendLogicWorker import cn.ppps.forwarder.workers.SendWorker import cn.ppps.forwarder.workers.UpdateLogsWorker @@ -197,6 +199,11 @@ object SendUtils { SocketUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId) } + TYPE_WXPUSHER -> { + val settingVo = Gson().fromJson(sender.jsonSetting, WxpusherSetting::class.java) + WxpusherUtils.sendMsg(settingVo, msgInfo, rule, senderIndex, logId, msgId) + } + else -> { updateLogs(logId, 0, getString(R.string.unknown_sender)) senderLogic(0, msgInfo, rule, senderIndex, msgId) diff --git a/app/src/main/kotlin/cn/ppps/forwarder/utils/sender/WebhookUtils.kt b/app/src/main/kotlin/cn/ppps/forwarder/utils/sender/WebhookUtils.kt index 38a895eeb0..62a11e3239 100644 --- a/app/src/main/kotlin/cn/ppps/forwarder/utils/sender/WebhookUtils.kt +++ b/app/src/main/kotlin/cn/ppps/forwarder/utils/sender/WebhookUtils.kt @@ -47,12 +47,17 @@ class WebhookUtils { msgId: Long = 0L ) { val from: String = msgInfo.from - val content: String = if (rule != null) { + var content: String = if (rule != null) { msgInfo.getContentForSend(rule.smsTemplate, rule.regexReplace, rule.title) } else { msgInfo.getContentForSend(SettingUtils.smsTemplate) } + //通道级正则替换:无需配置转发规则即可在Webhook通道直接提取验证码等 + if (!TextUtils.isEmpty(setting.regexReplace)) { + content = msgInfo.applyRegexReplace(content, setting.regexReplace) + } + var requestUrl: String = setting.webServer //推送地址 Log.i(TAG, "requestUrl:$requestUrl") @@ -110,7 +115,8 @@ class WebhookUtils { } val request = if (setting.method == "GET" && TextUtils.isEmpty(webParams)) { - setting.webServer += (if (setting.webServer.contains("?")) "&" else "?") + "from=" + URLEncoder.encode( + //修复:原来 from 参数误拼到 setting.webServer 上导致丢失,且 content 前固定拼接 & 导致无 ? 时 URL 不合法 + requestUrl += (if (requestUrl.contains("?")) "&" else "?") + "from=" + URLEncoder.encode( from, "UTF-8" ) diff --git a/app/src/main/kotlin/cn/ppps/forwarder/utils/sender/WxpusherUtils.kt b/app/src/main/kotlin/cn/ppps/forwarder/utils/sender/WxpusherUtils.kt new file mode 100644 index 0000000000..6286c232c9 --- /dev/null +++ b/app/src/main/kotlin/cn/ppps/forwarder/utils/sender/WxpusherUtils.kt @@ -0,0 +1,110 @@ +package cn.ppps.forwarder.utils.sender + +import android.text.TextUtils +import cn.ppps.forwarder.database.entity.Rule +import cn.ppps.forwarder.entity.MsgInfo +import cn.ppps.forwarder.entity.result.WxpusherResult +import cn.ppps.forwarder.entity.setting.WxpusherSetting +import cn.ppps.forwarder.utils.Log +import cn.ppps.forwarder.utils.SendUtils +import cn.ppps.forwarder.utils.SettingUtils +import cn.ppps.forwarder.utils.interceptor.LoggingInterceptor +import com.google.gson.Gson +import com.xuexiang.xhttp2.XHttp +import com.xuexiang.xhttp2.callback.SimpleCallBack +import com.xuexiang.xhttp2.exception.ApiException + +//WxPusher官方文档:https://wxpusher.zjiecode.com/docs/ +class WxpusherUtils private constructor() { + companion object { + + private val TAG: String = WxpusherUtils::class.java.simpleName + + //WxPusher返回码:1000=成功 + private const val WXPUSHER_CODE_SUCCESS = 1000L + + fun sendMsg( + setting: WxpusherSetting, + msgInfo: MsgInfo, + rule: Rule? = null, + senderIndex: Int = 0, + logId: Long = 0L, + msgId: Long = 0L + ) { + var summary: String = if (rule != null) { + msgInfo.getTitleForSend(setting.summaryTemplate, rule.regexReplace, rule.title) + } else { + msgInfo.getTitleForSend(setting.summaryTemplate) + } + var content: String = if (rule != null) { + msgInfo.getContentForSend(rule.smsTemplate, rule.regexReplace, rule.title) + } else { + msgInfo.getContentForSend(SettingUtils.smsTemplate) + } + + //通道级正则替换:无需配置转发规则即可在WxPusher通道直接提取验证码等,同时作用于摘要和内容 + if (!TextUtils.isEmpty(setting.regexReplace)) { + summary = msgInfo.applyRegexReplace(summary, setting.regexReplace) + content = msgInfo.applyRegexReplace(content, setting.regexReplace) + } + + val msgMap: MutableMap = mutableMapOf() + msgMap["content"] = content + msgMap["contentType"] = setting.contentType + if (summary.isNotEmpty()) msgMap["summary"] = summary + + //两种发送方式:SPT极简推送(发送者和接收者是同一人) 和 标准推送(appToken+UID/主题ID群发) + val requestUrl: String = if (setting.isSimplePush) { + val sptList = setting.spt.split(",").map { it.trim() }.filter { it.isNotEmpty() } + if (sptList.isEmpty()) throw IllegalArgumentException("invalid spt") + if (sptList.size == 1) msgMap["spt"] = sptList[0] else msgMap["sptList"] = sptList + "https://wxpusher.zjiecode.com/api/send/message/simple-push" + } else { + if (setting.appToken.isBlank()) throw IllegalArgumentException("invalid appToken") + msgMap["appToken"] = setting.appToken + val uids = setting.uids.split(",").map { it.trim() }.filter { it.isNotEmpty() } + if (uids.isNotEmpty()) msgMap["uids"] = uids + val topicIds = setting.topicIds.split(",").mapNotNull { it.trim().toIntOrNull() } + if (topicIds.isNotEmpty()) msgMap["topicIds"] = topicIds + "https://wxpusher.zjiecode.com/api/send/message" + } + + val requestMsg: String = Gson().toJson(msgMap) + Log.i(TAG, "requestUrl:$requestUrl") + Log.i(TAG, "requestMsg:$requestMsg") + + XHttp.post(requestUrl) + .upJson(requestMsg) + .keepJson(true) + .retryCount(SettingUtils.requestRetryTimes) //超时重试的次数 + .retryDelay(SettingUtils.requestDelayTime * 1000) //超时重试的延迟时间 + .retryIncreaseDelay(SettingUtils.requestDelayTime * 1000) //超时重试叠加延时 + .timeStamp(true) //url自动追加时间戳,避免缓存 + .addInterceptor(LoggingInterceptor(logId)) //增加一个log拦截器, 记录请求日志 + .execute(object : SimpleCallBack() { + + override fun onError(e: ApiException) { + Log.e(TAG, e.detailMessage) + val status = 0 + SendUtils.updateLogs(logId, status, e.displayMessage) + SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) + } + + override fun onSuccess(response: String) { + Log.i(TAG, response) + val resp = try { + Gson().fromJson(response, WxpusherResult::class.java) + } catch (e: Exception) { + null + } + val status = if (resp?.code == WXPUSHER_CODE_SUCCESS) 2 else 0 + SendUtils.updateLogs(logId, status, response) + SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) + } + + }) + + } + + } +} diff --git a/app/src/main/res/drawable/icon_wxpusher.xml b/app/src/main/res/drawable/icon_wxpusher.xml new file mode 100644 index 0000000000..bf8b358349 --- /dev/null +++ b/app/src/main/res/drawable/icon_wxpusher.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_senders_webhook.xml b/app/src/main/res/layout/fragment_senders_webhook.xml index 450a68906f..0e4ecfcb66 100644 --- a/app/src/main/res/layout/fragment_senders_webhook.xml +++ b/app/src/main/res/layout/fragment_senders_webhook.xml @@ -169,6 +169,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index e5b88210fb..da66c1a914 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -649,6 +649,30 @@ Socket + WxPusher + Push Type + Standard push requires an app; Simple push (SPT) sends to yourself + Standard + Simple (SPT) + appToken + Get it from the WxPusher admin console after creating an app + AT_xxx + Receiver UIDs + Separate multiple UIDs with commas + Topic IDs + SPT + Scan the QR code to get it, up to 10, separate with commas + SPT_xxx + Content Type + Text + HTML + Markdown + Summary + Optional, up to 100 characters + Please enter the appToken + Please enter at least one UID or Topic ID + Please enter the SPT + Webhook Server For example: https://a.b.com/msg?token=xyz Params @@ -656,6 +680,10 @@ Secret: If left empty, the sign will not be calculated Successful Response Keyword:If left empty, HTTP status 200 represents success Successful Response Keyword:Leaving it blank means sending is considered as successful. + Regex Replacement + Optional. Format: regex===replacement, one rule per line, applied to the sent content.\ne.g. .*?(\\d{6}).*?===$1 to extract a 6-digit verification code + Regex Replacement + Optional. Format: regex===replacement, one rule per line, applied to both summary and content.\ne.g. .*?(\\d{6}).*?===$1 to extract a 6-digit verification code Headers Key Value diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2409036b33..2c18169cd6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -650,6 +650,30 @@ Socket + WxPusher + 推送方式 + 标准推送需在后台创建应用;极简推送(SPT)自己发给自己 + 标准推送 + 极简推送(SPT) + appToken + 在WxPusher后台创建应用后获取 + AT_xxx + 接收UID + 多个以英文逗号分隔 + 主题ID(Topic) + SPT + 扫码获取,最多10个,多个以英文逗号分隔 + SPT_xxx + 内容类型 + 文字 + HTML + Markdown + 消息摘要 + 可选,最长100字 + 请填写appToken + 请至少填写一个UID或主题ID + 请填写SPT + Webhook Server 例如:https://a.b.com/msg?token=xyz 消息模板 @@ -657,6 +681,10 @@ Secret:置空则不计算sign 成功应答关键字:置空则http状态200即为成功 成功应答关键字:置空则发出即成功 + 正则替换内容 + 可选。格式:正则表达式===替换结果,每行一个规则,对发送内容执行。\n例如:.*?(\\d{6}).*?===$1 可提取6位验证码 + 正则替换内容 + 可选。格式:正则表达式===替换结果,每行一个规则,同时作用于摘要和发送内容。\n例如:.*?(\\d{6}).*?===$1 可提取6位验证码 Headers Key Value diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ee6250f96b..6356095d0b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -650,6 +650,30 @@ Socket + WxPusher + 推送方式 + 標準推送需在後台創建應用;極簡推送(SPT)自己發給自己 + 標準推送 + 極簡推送(SPT) + appToken + 在WxPusher後台創建應用後獲取 + AT_xxx + 接收UID + 多個以英文逗號分隔 + 主題ID(Topic) + SPT + 掃碼獲取,最多10個,多個以英文逗號分隔 + SPT_xxx + 內容類型 + 文字 + HTML + Markdown + 消息摘要 + 可選,最長100字 + 請填寫appToken + 請至少填寫一個UID或主題ID + 請填寫SPT + Webhook Server 例如:https://a.b.com/msg?token=xyz 消息模板 @@ -657,6 +681,10 @@ Secret:置空則不計算sign 成功應答關鍵字:置空則http狀態200即為成功 成功應答關鍵字:置空則發出即成功 + 正則替換內容 + 可選。格式:正則表達式===替換結果,每行一個規則,對發送內容執行。\n例如:.*?(\\d{6}).*?===$1 可提取6位驗證碼 + 正則替換內容 + 可選。格式:正則表達式===替換結果,每行一個規則,同時作用於摘要和發送內容。\n例如:.*?(\\d{6}).*?===$1 可提取6位驗證碼 Headers Key Value diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d0e1239a1..73fdf9a657 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -680,6 +680,30 @@ Socket + WxPusher + 推送方式 + 标准推送需在后台创建应用;极简推送(SPT)自己发给自己 + 标准推送 + 极简推送(SPT) + appToken + 在WxPusher后台创建应用后获取 + AT_xxx + 接收UID + 多个以英文逗号分隔 + 主题ID(Topic) + SPT + 扫码获取,最多10个,多个以英文逗号分隔 + SPT_xxx + 内容类型 + 文字 + HTML + Markdown + 消息摘要 + 可选,最长100字 + 请填写appToken + 请至少填写一个UID或主题ID + 请填写SPT + Webhook Server 例如:https://a.b.com/msg?token=xyz 消息模板 @@ -687,6 +711,10 @@ Secret:置空则不计算sign 成功应答关键字:置空则http状态200即为成功 成功应答关键字:置空则发出即成功 + 正则替换内容 + 可选。格式:正则表达式===替换结果,每行一个规则,对发送内容执行。\n例如:.*?(\\d{6}).*?===$1 可提取6位验证码 + 正则替换内容 + 可选。格式:正则表达式===替换结果,每行一个规则,同时作用于摘要和发送内容。\n例如:.*?(\\d{6}).*?===$1 可提取6位验证码 Headers Key Value