diff --git a/app/src/main/java/cc/microblock/hook/Pangu.kt b/app/src/main/java/cc/microblock/hook/Pangu.kt new file mode 100644 index 0000000000..1c42545643 --- /dev/null +++ b/app/src/main/java/cc/microblock/hook/Pangu.kt @@ -0,0 +1,186 @@ +/* + * QAuxiliary - An Xposed module for QQ/TIM + * Copyright (C) 2019-2023 QAuxiliary developers + * https://github.com/cinit/QAuxiliary + * + * This software is non-free but opensource software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation; either + * version 3 of the License, or any later version and our eula as published + * by QAuxiliary contributors. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * and eula along with this software. If not, see + * + * . + */ + +package cc.microblock.hook + +import com.github.kyuubiran.ezxhelper.utils.hookBefore +import io.github.qauxv.base.annotation.FunctionHookEntry +import io.github.qauxv.base.annotation.UiItemAgentEntry +import io.github.qauxv.dsl.FunctionEntryRouter +import io.github.qauxv.hook.CommonSwitchFunctionHook +import io.github.qauxv.util.dexkit.AIOTextElementCtor +import io.github.qauxv.util.dexkit.DexKit +import xyz.nextalone.util.get +import xyz.nextalone.util.set +import java.util.regex.Pattern + +val CJK = "\u2e80-\u2eff\u2f00-\u2fdf\u3040-\u309f\u30a0-\u30fa\u30fc-\u30ff\u3100-\u312f\u3200-\u32ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff" + +val ANY_CJK = Pattern.compile("[$CJK]") + +val CONVERT_TO_FULLWIDTH_CJK_SYMBOLS_CJK = Pattern.compile("([$CJK])[ ]*(\\:+|\\.)[ ]*([$CJK])") +val CONVERT_TO_FULLWIDTH_CJK_SYMBOLS = Pattern.compile("([$CJK])[ ]*([~\\!;,\\?]+)[ ]*") +val DOTS_CJK = Pattern.compile("([\\.]{2,}|\\u2026)([$CJK])") +val FIX_CJK_COLON_ANS = Pattern.compile("([$CJK])\\:([A-Z0-9\\(\\)])") + +val CJK_QUOTE = Pattern.compile("([$CJK])([\\`\"\\u05f4])") +val QUOTE_CJK = Pattern.compile("([\\`\"\\u05f4])([$CJK])") +val FIX_QUOTE_ANY_QUOTE = Pattern.compile("([`\"\\u05f4]+)[ ]*(.+?)[ ]*([`\"\\u05f4]+)") + +val CJK_SINGLE_QUOTE_BUT_POSSESSIVE = Pattern.compile("([$CJK])('[^s])") +val SINGLE_QUOTE_CJK = Pattern.compile("(')([$CJK])") +val FIX_POSSESSIVE_SINGLE_QUOTE = Pattern.compile("([A-Za-z0-9${CJK}])( )('s)") + +val HASH_ANS_CJK_HASH = Pattern.compile("([$CJK])(#)([$CJK]+)(#)([$CJK])") +val CJK_HASH = Pattern.compile("([$CJK])(#([^ ]))") +val HASH_CJK = Pattern.compile("(([^ ])#)([$CJK])") + +val CJK_OPERATOR_ANS = Pattern.compile("([$CJK])([\\+\\-\\*\\/=&\\|<>])([A-Za-z0-9])") +val ANS_OPERATOR_CJK = Pattern.compile("([A-Za-z0-9])([\\+\\-\\*\\/=&\\|<>])([$CJK])") + +val FIX_SLASH_AS = Pattern.compile("([/]) ([a-z\\-\\_\\./]+)") +val FIX_SLASH_AS_SLASH = Pattern.compile("([/\\.])([A-Za-z\\-\\_\\./]+) ([/])") + +val CJK_LEFT_BRACKET = Pattern.compile("([$CJK])([\\(\\[\\{<>\\u201c])") +val RIGHT_BRACKET_CJK = Pattern.compile("([\\)\\]\\}>\\u201d])([$CJK])") +val FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET = Pattern.compile("([\\(\\[\\{<\\u201c]+)[ ]*(.+?)[ ]*([\\)\\]\\}>\u201d]+)") +val ANS_CJK_LEFT_BRACKET_ANY_RIGHT_BRACKET = Pattern.compile("([A-Za-z0-9${CJK}])[ ]*([\\u201c])([A-Za-z0-9${CJK}\\-_ ]+)([\\u201d])") +val LEFT_BRACKET_ANY_RIGHT_BRACKET_ANS_CJK = Pattern.compile("([\\u201c])([A-Za-z0-9${CJK}\\-_ ]+)([\\u201d])[ ]*([A-Za-z0-9${CJK}])") + +val AN_LEFT_BRACKET = Pattern.compile("([A-Za-z0-9])([\\(\\[\\{])") +val RIGHT_BRACKET_AN = Pattern.compile("([\\)\\]\\}])([A-Za-z0-9])") + +val CJK_ANS = Pattern.compile("([$CJK])([A-Za-z\\u0370-\\u03ff0-9@\$%\\^&\\*\\-\\+\\\\=\\|/\\u00a1-\\u00ff\\u2150-\\u218f\\u2700—\\u27bf])") +val ANS_CJK = Pattern.compile("([A-Za-z\\u0370-\\u03ff0-9~\\\$%\\^&\\*\\-\\+\\\\=\\|/!;:,\\.\\?\\u00a1-\\u00ff\\u2150-\\u218f\\u2700—\\u27bf])([$CJK])") + +val S_A = Pattern.compile("(%)([A-Za-z])") + +val MIDDLE_DOT = Pattern.compile("([ ]*)([\\u00b7\\u2022\\u2027])([ ]*)") + +fun convertToFullwidth(symbols: String): String { + return symbols + .replace("~", "~") + .replace("!", "!") + .replace(";", ";") + .replace(":", ":") + .replace(",", ",") + .replace(".", "。") + .replace("?", "?") +} + +// This is an incomplete implementation of https://github.com/vinta/pangu.js/blob/master/src/shared/core.js +// https://github.com/vinta/pangu.js/blob/6107055384b99e6f30a49f5d1b85aa0b78251dc2/src/shared/core.js#L118-L126 +// These lines are implemented, work in the playground, but are commented in the actual code, as they are causing compile errors +// As I'm not so familiar with Kotlin, if someone is able to figure it out and make it a complete impl, I'll appreciate it. +fun pangu_spacing(text: String): String { + if (text.length <= 1 || !ANY_CJK.matcher(text).find()) { + return text + } + + var newText = text + +/* + +// Corresponding lines: https://github.com/vinta/pangu.js/blob/6107055384b99e6f30a49f5d1b85aa0b78251dc2/src/shared/core.js#L118-L126 + + newText = CONVERT_TO_FULLWIDTH_CJK_SYMBOLS_CJK.matcher(newText).replaceAll { matchResult -> + val leftCjk = matchResult.group(1) + val symbols = matchResult.group(2) + val rightCjk = matchResult.group(3) + val fullwidthSymbols = convertToFullwidth(symbols) + "$leftCjk$fullwidthSymbols$rightCjk" + } + + newText = CONVERT_TO_FULLWIDTH_CJK_SYMBOLS.matcher(newText).replaceAll { matchResult -> + val cjk = matchResult.group(1) + val symbols = matchResult.group(2) + val fullwidthSymbols = convertToFullwidth(symbols) + "$cjk$fullwidthSymbols" + } +*/ + + newText = DOTS_CJK.matcher(newText).replaceAll("$1 $2") + newText = FIX_CJK_COLON_ANS.matcher(newText).replaceAll("$1:$2") + + newText = CJK_QUOTE.matcher(newText).replaceAll("$1 $2") + newText = QUOTE_CJK.matcher(newText).replaceAll("$1 $2") + newText = FIX_QUOTE_ANY_QUOTE.matcher(newText).replaceAll("$1$2$3") + + newText = CJK_SINGLE_QUOTE_BUT_POSSESSIVE.matcher(newText).replaceAll("$1 $2") + newText = SINGLE_QUOTE_CJK.matcher(newText).replaceAll("$1 $2") + newText = FIX_POSSESSIVE_SINGLE_QUOTE.matcher(newText).replaceAll("$1's") + + newText = HASH_ANS_CJK_HASH.matcher(newText).replaceAll("$1 $2$3$4 $5") + newText = CJK_HASH.matcher(newText).replaceAll("$1 $2") + newText = HASH_CJK.matcher(newText).replaceAll("$1 $3") + + newText = CJK_OPERATOR_ANS.matcher(newText).replaceAll("$1 $2 $3") + newText = ANS_OPERATOR_CJK.matcher(newText).replaceAll("$1 $2 $3") + + newText = FIX_SLASH_AS.matcher(newText).replaceAll("$1$2") + newText = FIX_SLASH_AS_SLASH.matcher(newText).replaceAll("$1$2$3") + + newText = CJK_LEFT_BRACKET.matcher(newText).replaceAll("$1 $2") + newText = RIGHT_BRACKET_CJK.matcher(newText).replaceAll("$1 $2") + newText = FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET.matcher(newText).replaceAll("$1$2$3") + newText = ANS_CJK_LEFT_BRACKET_ANY_RIGHT_BRACKET.matcher(newText).replaceAll("$1 $2$3$4") + newText = LEFT_BRACKET_ANY_RIGHT_BRACKET_ANS_CJK.matcher(newText).replaceAll("$1$2$3 $4") + + newText = AN_LEFT_BRACKET.matcher(newText).replaceAll("$1 $2") + newText = RIGHT_BRACKET_AN.matcher(newText).replaceAll("$1 $2") + + newText = CJK_ANS.matcher(newText).replaceAll("$1 $2") + newText = ANS_CJK.matcher(newText).replaceAll("$1 $2") + + newText = S_A.matcher(newText).replaceAll("$1 $2") + + newText = MIDDLE_DOT.matcher(newText).replaceAll("・") + + return newText +} + + +@FunctionHookEntry +@UiItemAgentEntry +object SendPangu : CommonSwitchFunctionHook("sendMsgPangu",arrayOf(AIOTextElementCtor)) { + override val name = "发送消息自动 Pangu.kt" + override val description = "自动在中英文间加上空格,以美化排版\n若消息以空格开头,则不会被 Pangu 化" + + override val uiItemLocation = FunctionEntryRouter.Locations.Simplify.CHAT_OTHER + + override fun initOnce(): Boolean { + DexKit.requireMethodFromCache(AIOTextElementCtor) + .hookBefore { + val content = it.args[0].get("a") as String; + if(!content.startsWith(" ")) + it.args[0].set("a", SendPangu.processPangu(content)) + else + it.args[0].set("a", content.substring(1)) + } + + return true; + } + + private fun processPangu(message: String): String { + return pangu_spacing(message); + } +} diff --git a/app/src/main/java/io/github/qauxv/hook/CommonConfigFunctionHook.kt b/app/src/main/java/io/github/qauxv/hook/CommonConfigFunctionHook.kt index cc73e0facc..c231a0e20f 100644 --- a/app/src/main/java/io/github/qauxv/hook/CommonConfigFunctionHook.kt +++ b/app/src/main/java/io/github/qauxv/hook/CommonConfigFunctionHook.kt @@ -25,6 +25,7 @@ package io.github.qauxv.hook import android.app.Activity import android.content.Context import android.view.View +import cc.microblock.hook.pangu_spacing import io.github.qauxv.base.ISwitchCellAgent import io.github.qauxv.base.IUiItemAgent import io.github.qauxv.util.SyncUtils @@ -76,8 +77,12 @@ abstract class CommonConfigFunctionHook( override val uiItemAgent by lazy { uiItemAgent() } private fun uiItemAgent() = object : IUiItemAgent { - override val titleProvider: (IUiItemAgent) -> String = { _ -> name } - override val summaryProvider: (IUiItemAgent, Context) -> CharSequence? = { _, _ -> description } + override val titleProvider: (IUiItemAgent) -> String = { _ -> pangu_spacing(name) } + override val summaryProvider: (IUiItemAgent, Context) -> CharSequence? = { _, _ -> + if(description is String) + pangu_spacing(description.toString()) + else description + } override val valueState: MutableStateFlow? get() = this@CommonConfigFunctionHook.valueState override val validator: ((IUiItemAgent) -> Boolean) = { _ -> true } diff --git a/app/src/main/java/io/github/qauxv/hook/CommonSwitchFunctionHook.kt b/app/src/main/java/io/github/qauxv/hook/CommonSwitchFunctionHook.kt index 1847eae7c4..776c0f5aea 100644 --- a/app/src/main/java/io/github/qauxv/hook/CommonSwitchFunctionHook.kt +++ b/app/src/main/java/io/github/qauxv/hook/CommonSwitchFunctionHook.kt @@ -25,6 +25,7 @@ package io.github.qauxv.hook import android.app.Activity import android.content.Context import android.view.View +import cc.microblock.hook.pangu_spacing import io.github.qauxv.base.ISwitchCellAgent import io.github.qauxv.base.IUiItemAgent import io.github.qauxv.util.SyncUtils @@ -67,8 +68,12 @@ abstract class CommonSwitchFunctionHook( override val uiItemAgent by lazy { uiItemAgent() } private fun uiItemAgent() = object : IUiItemAgent { - override val titleProvider: (IUiItemAgent) -> String = { _ -> name } - override val summaryProvider: (IUiItemAgent, Context) -> CharSequence? = { _, _ -> description } + override val titleProvider: (IUiItemAgent) -> String = { _ -> pangu_spacing(name) } + override val summaryProvider: (IUiItemAgent, Context) -> CharSequence? = { _, _ -> + if(description is String) + pangu_spacing(description.toString()) + else description + } override val valueState: MutableStateFlow? = null override val validator: ((IUiItemAgent) -> Boolean) = { _ -> true } override val switchProvider: ISwitchCellAgent? by lazy { diff --git a/app/src/main/java/io/github/qauxv/util/dexkit/DexKitTarget.kt b/app/src/main/java/io/github/qauxv/util/dexkit/DexKitTarget.kt index 0bde29ae4f..8ff9bdc336 100644 --- a/app/src/main/java/io/github/qauxv/util/dexkit/DexKitTarget.kt +++ b/app/src/main/java/io/github/qauxv/util/dexkit/DexKitTarget.kt @@ -769,3 +769,10 @@ data object Multiforward_Avatar_setListener_NT : DexKitTarget.UsingDexkit() { override val declaringClass = "com.tencent.mobileqq.aio.msglist.holder.component.avatar.AIOAvatarContentComponent" override val filter = DexKitFilter.allowAll } + +data object AIOTextElementCtor: DexKitTarget.UsingStr() { + override val findMethod: Boolean = true; + override val declaringClass = "com.tencent.mobileqq.aio.msg.AIOMsgElement.AIOTextElementCtor" + override val traitString = arrayOf("textElement") + override val filter = DexKitFilter.strInClsName("com/tencent/mobileqq/aio/msg") +}