65 Commits
1.0.6 ... 1.0.7

Author SHA1 Message Date
6201d12f5f Shamrock: error fix 2023-12-02 18:08:35 +08:00
b2adc5cedf Shamrock: fix #106 2023-12-02 17:54:53 +08:00
0bb871bf01 Shamrock: fix #115 2023-12-02 17:36:34 +08:00
dc969440ee Shamrock: fix #116 2023-12-02 16:21:16 +08:00
b9b6e133d0 Shamrock: fix #117 2023-12-02 16:19:05 +08:00
b5a9884448 Shamrock: fix 群头衔推送 2023-12-02 11:11:22 +08:00
bffb7caf04 Shamrock: A60's PING-PONG 2023-12-02 09:51:39 +08:00
2c3466b4c3 Shamrock: fix 96 crash 2023-12-01 19:30:48 +08:00
007e5fef2f Merge branch 'master' of github.com:whitechi73/OpenShamrock 2023-12-01 15:22:04 +08:00
48773cc47c Shamrock: JSON メッセージのクラッシュと QQ ミュージックのカバーを修正 2023-12-01 15:21:58 +08:00
b2ad4438ab Shamrock: fix #114 2023-12-01 14:47:33 +08:00
2fdcfe332b Shamrock: fix #109 2023-11-30 22:38:44 +08:00
7d8772ebf6 Shamrock: スーパーアンチチェックのオフを許可 2023-11-30 21:36:57 +08:00
2a75160ef8 Merge remote-tracking branch 'origin/master' 2023-11-30 21:33:43 +08:00
76bd58d984 Shamrock: スーパーアンチチェックのオフを許可 2023-11-30 21:33:32 +08:00
39120bdeae Shamrock: fix 群精华消息推送 2023-11-30 19:04:25 +08:00
7b07698f7b Shamrock: fix #110 2023-11-30 12:27:07 +08:00
5c10a5a04e Shamrock: 支持篮球超表情, 新猜拳超表情 2023-11-30 10:28:12 +08:00
3a0dc41329 Shamrock: 支持NTQQ骰子消息(new_dice) 2023-11-29 22:11:36 +08:00
64c800c945 Merge remote-tracking branch 'origin/master' 2023-11-29 10:11:35 +08:00
ecb3cea5a5 Shamrock: fix #105 2023-11-29 10:11:24 +08:00
8e0ae6f85b Shamrock: fix #104 2023-11-28 23:49:21 +08:00
9d893b481d Shamrock: fix group ban user_id error 2023-11-28 17:33:33 +08:00
85aaa54e4e Shamrock: friend poke event detail fix #103 2023-11-28 17:19:08 +08:00
c6dad5677c Shamrock: #103 2023-11-28 17:10:19 +08:00
80a4a208b9 Shamrock: 尝试修复 #98 2023-11-27 12:06:02 +08:00
ae663e6b2e Shamrock: 中二通事 2023-11-27 01:13:40 +08:00
780f3577a5 Shamrock: optimize clover.cpp 2023-11-27 00:42:15 +08:00
3518f974cc Shamrock: bypass emu detection 2023-11-27 00:28:43 +08:00
911b003f7f Shamrock: Modify required fields according to the document 2023-11-27 00:23:02 +08:00
69bc80e9b3 Merge branch 'master' of github.com:whitechi73/OpenShamrock 2023-11-27 00:18:56 +08:00
da0b74db1a Shamrock: fix #92 2023-11-27 00:18:51 +08:00
7212938df3 Shamrock: fix troopowneruin 2023-11-26 22:34:00 +08:00
ae1e78b267 Merge remote-tracking branch 'origin/master' 2023-11-26 22:29:18 +08:00
b7266e490f Shamrock: アンチシミュレータ検出 2023-11-26 22:29:11 +08:00
6b4a429821 Shamrock: fix forward msg nesting(x 2023-11-26 22:06:26 +08:00
4a4507dfcd Shamrock: add switch of anti trace 2023-11-26 19:28:36 +08:00
f63bcabf1b Shamrock: 添加native检测绕过模板 2023-11-26 18:32:55 +08:00
4932b36ee1 Shamrock: fix: グループリクエストイベントの通知と処理ロジックを最適化する 2023-11-26 12:59:48 +08:00
8c307c4f6e Shamrock: fix #90 2023-11-25 20:28:14 +08:00
8d8846fafb Shamrock: 复皆如何推送,遗类也。 2023-11-25 20:05:32 +08:00
544e216ddb Merge remote-tracking branch 'origin/master' 2023-11-25 19:55:09 +08:00
4fedab719b Shamrock: Support télécharger un fichier obtenir un ID de fichier 2023-11-25 19:55:00 +08:00
75a567d5cd Shamrock: fix 重複したグループ参加イベントを削除する 2023-11-25 19:47:35 +08:00
1a814e565a Shamrock: fix #89 2023-11-25 18:50:48 +08:00
5ea260c24b Shamrock: #89 2023-11-25 18:31:08 +08:00
2d57dc021d Merge branch 'master' of github.com:whitechi73/OpenShamrock 2023-11-25 13:51:21 +08:00
dabe2ea886 Shamrock: グループファイルのハッシュ値が増加する 2023-11-25 13:51:16 +08:00
673902e514 Shamrock: fix typo 2023-11-25 13:33:06 +08:00
5062ff7c3a Shamrock: ログスライス #78 2023-11-25 11:53:39 +08:00
0de6f851a6 Merge remote-tracking branch 'origin/master' 2023-11-25 11:29:26 +08:00
c758b1576d Shamrock: アクティブWebSocketハートビートの修復 2023-11-25 11:29:06 +08:00
5ba8bd11e2 Shamrock: essence, card change and title change event 2023-11-25 02:09:16 +08:00
679b7619ce Shamrock: wsハートビート間隔の制御をサポート 2023-11-25 00:42:10 +08:00
282233131a Shamrock: fix 非常に奇妙なフォームデータの問題 2023-11-24 15:18:50 +08:00
edf857bcb6 Shamrock: _send_group_notice 2023-11-24 13:21:22 +08:00
cd1d1e928a Merge branch 'master' of github.com:whitechi73/OpenShamrock 2023-11-24 01:00:59 +08:00
45d6421153 Shamrock: _get_group_notice 2023-11-24 01:00:53 +08:00
8c6f529b4b Merge pull request #76 from DouchChunFeng/master
有个空格
2023-11-24 00:24:57 +08:00
b23620b5ef Shamrock: get_essence_msg_list 2023-11-24 00:21:05 +08:00
e09e00fcd3 有个空格 2023-11-24 00:06:50 +08:00
0d35d5834b Shamrock: 金メダル免除、システムレベルの保活をサポートする 2023-11-23 23:53:11 +08:00
ee8dc75be3 Shamrock: fix: implement request type event 2023-11-23 19:33:50 +08:00
5f91be547e Shamrock: fix group apply flag error 2023-11-23 18:15:20 +08:00
7439622cd6 Shamrock: fix #73 2023-11-23 12:51:26 +08:00
78 changed files with 2357 additions and 789 deletions

View File

@ -6,7 +6,7 @@ labels: bug
---
警告: 在进一步操作之前,请检查下列选项。如果您忽视此模板或者没有提供关键信息,您的 Issue 将直接被关闭
- 确保您使用的是 [最新开发版本](https://github.com/whitechi73/Shamrock/actions/workflows/build-apk.yml) 的 Shamrock.
- 确保您使用的是 [最新开发版本](https://github.com/whitechi73/OpenShamrock/actions/workflows/build-apk.yml) 的 Shamrock.
- 确保您的问题尚未在 Issues 列表中提出.
- 确保您的问题不是由于您的代码错误导致的.
@ -22,5 +22,6 @@ labels: bug
- Shamrock 版本:
- Android 版本:
- LSPosed 框架版本:
- 设备的制造商和型号:
- 设备的 CPU 架构:

View File

@ -7,7 +7,7 @@ labels: enhancement
警告: 在进一步操作之前,请检查下列选项。如果您忽视此模板或者没有提供关键信息,您的 Issue 将直接被关闭
- 确保您使用的是 [最新开发版本](https://github.com/whitechi73/Shamrock/actions/workflows/build-apk.yml) 的 Shamrock.
- 确保您使用的是 [最新开发版本](https://github.com/whitechi73/OpenShamrock/actions/workflows/build-apk.yml) 的 Shamrock.
- 确保您的功能请求尚未在 Issues 列表中提出.
- 确保您的功能请求是与 Shamrock 相关的,且可以实现.

View File

@ -10,7 +10,7 @@
![][onebot-12]
[![][license]](LICENSE)
[下载][download-link] | [部署][deploy-link] | [接口][api-link] | [文档][docs-link] | [加群][group-link]
[下载][download-link] | [部署][deploy-link] | [接口][api-link] | [文档][docs-link]
</div>
@ -29,8 +29,6 @@
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程。
> 若您追求小而轻便的Bot服务, [Chronocat](https://chronocat.vercel.app/)是您的不二之选。
## 权限声明
> 如出现未在此处声明的权限,请警惕 Shamrock 是否被修改/植入恶意代码
@ -45,7 +43,7 @@
## 贡献说明
<img src="https://github.com/whitechi73/OpenShamrock/assets/98259561/f04d60bc-ec40-41fc-bc15-62c146f1a1f1" width="160px"> **我可爱吗?欢迎你的到来,这里是一个很大的地方,有着无限可能,主要是有你啦!**
<img src="https://github.com/whitechi73/OpenShamrock/assets/98259561/f04d60bc-ec40-41fc-bc15-62c146f1a1f1" width="160px" alt="Shamrock"> **我可爱吗?欢迎你的到来,这里是一个很大的地方,有着无限可能,主要是有你啦!**
## 开源协议
@ -96,9 +94,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
[docs-link]: https://whitechi73.github.io/OpenShamrock/
[group-link]: https://whitechi73.github.io/OpenShamrock/group.html
[hook-system]: https://github.com/whitechi73/OpenShamrock/wiki/perm_hook_android
[hook-system]: https://github.com/whitechi73/OpenShamrock/blob/master/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/FuckAMS.kt
[voice-support]: https://whitechi73.github.io/OpenShamrock/advanced/voice.html

View File

@ -24,7 +24,7 @@ android {
minSdk = 24
targetSdk = 33
versionCode = (System.currentTimeMillis() / 1000).toInt()
versionName = "1.0.6-dev" + gitCommitHash()
versionName = "1.0.7-dev" + gitCommitHash()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -18,6 +18,8 @@
android:supportsRtl="true"
android:theme="@style/Theme.Shamrock"
android:zygotePreloadName="@string/app_name"
android:multiArch="true"
android:extractNativeLibs="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
@ -47,7 +49,7 @@
android:value="基于 Xposed 实现 OneBot 标准的 QQ 机器人框架" />
<meta-data
android:name="xposedminversion"
android:value="23" />
android:value="93" />
<meta-data
android:name="xposedscope"
android:resource="@array/xposed_scope" />

View File

@ -99,6 +99,8 @@ void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::
replace_string(cache, "&#93;", "]");
replace_string(cache, "&#44;", ",");
kv.emplace(key_tmp, cache);
} else {
kv.emplace("_type", cache);
}
dest.push_back(kv);
kv.clear();

View File

@ -120,6 +120,7 @@ private fun AppMainView() {
val coreVersion = remember { mutableStateOf(getShamrockVersion(context)) }
val coreName = remember { mutableStateOf("Xposed") }
val voiceSwitch = remember { mutableStateOf(false) }
@Suppress("LocalVariableName") val LocalString = LocalString
if (!AppRuntime.isInit) {
AppRuntime.state = remember {
@ -140,7 +141,7 @@ private fun AppMainView() {
mutableStateOf("2854200454")
}
it.nick = remember {
mutableStateOf("测试昵称")
mutableStateOf(LocalString.testName)
}
}
@ -150,13 +151,12 @@ private fun AppMainView() {
}
val ctx = LocalContext.current
@Suppress("LocalVariableName") val LocalString = LocalString
LaunchedEffect(isFined.value) {
if (isFined.value) {
AppRuntime.log("日志框架激活成功,开放操作许可。")
AppRuntime.log(LocalString.logCentralLoadSuccessfully)
Toast.makeText(ctx, LocalString.frameworkYes, Toast.LENGTH_SHORT).show()
} else {
AppRuntime.log("日志框架处于未激活状态,请检查。")
AppRuntime.log(LocalString.logCentralLoadFailed)
Toast.makeText(ctx, LocalString.frameworkNo, Toast.LENGTH_SHORT).show()
}
}

View File

@ -224,6 +224,16 @@ object ShamrockConfig {
preferences.edit().putBoolean("debug", v).apply()
}
fun isAntiTrace(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("anti_qq_trace", true)
}
fun setAntiTrace(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("anti_qq_trace", v).apply()
}
fun isInjectPacket(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("inject_packet", false)
@ -239,6 +249,11 @@ object ShamrockConfig {
return preferences.getBoolean("enable_auto_start", false)
}
fun enableAliveReply(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("alive_reply", false)
}
fun allowShell(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("shell", false)
@ -249,6 +264,11 @@ object ShamrockConfig {
preferences.edit().putBoolean("enable_auto_start", v).apply()
}
fun setAliveReply(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("alive_reply", v).apply()
}
fun setShellStatus(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("shell", v).apply()
@ -293,12 +313,14 @@ object ShamrockConfig {
"ssl_pwd" to preferences.getString("ssl_pwd", ""),
"inject_packet" to preferences.getBoolean("inject_packet", false),
"debug" to preferences.getBoolean("debug", false),
"auto_clear" to preferences.getBoolean("auto_clear", false),
"anti_qq_trace" to preferences.getBoolean("anti_qq_trace", true),
//"auto_clear" to preferences.getBoolean("auto_clear", false),
"ssl_private_pwd" to preferences.getString("ssl_private_pwd", ""),
"key_store" to preferences.getString("key_store", ""),
"enable_self_msg" to preferences.getBoolean("enable_self_msg", false),
"echo_number" to preferences.getBoolean("echo_number", false),
"shell" to preferences.getBoolean("shell", false),
"alive_reply" to preferences.getBoolean("alive_reply", false),
)
}

View File

@ -50,6 +50,7 @@ import moe.fuqiuluo.shamrock.ui.app.AppRuntime
import moe.fuqiuluo.shamrock.ui.app.Level
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
import moe.fuqiuluo.shamrock.ui.theme.LocalString
import moe.fuqiuluo.shamrock.ui.theme.ThemeColor
import moe.fuqiuluo.shamrock.ui.tools.InputDialog
@ -70,7 +71,7 @@ fun DashboardFragment(
AccountCard(nick, uin)
InformationCard(ctx)
APIInfoCard(ctx)
FunctionCard(scope, ctx, "功能设置")
FunctionCard(scope, ctx, LocalString.functionSetting)
SSLCard(ctx)
}
}
@ -80,7 +81,7 @@ private fun SSLCard(ctx: Context) {
ActionBox(
modifier = Modifier.padding(top = 12.dp),
painter = painterResource(id = R.drawable.baseline_security_24),
title = "SSL配置"
title = LocalString.sslSetting
) {
Column {
Divider(

View File

@ -1,6 +1,6 @@
package moe.fuqiuluo.shamrock.ui.fragment
import android.widget.Toast
import android.content.Context
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.absolutePadding
import androidx.compose.foundation.layout.fillMaxSize
@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import moe.fuqiuluo.shamrock.R
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
import moe.fuqiuluo.shamrock.ui.app.Level
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
import moe.fuqiuluo.shamrock.ui.theme.LocalString
@ -46,10 +47,11 @@ fun LabFragment() {
}
NoticeTextDialog(
openDialog = showNoticeDialog,
title = "温馨提示",
text = "实验室功能会导致一些奇怪的问题,请谨慎使用!"
title = LocalString.warnTitle,
text = LocalString.labWarning
)
val LocalString = LocalString
ActionBox(
modifier = Modifier.padding(top = 12.dp),
painter = painterResource(id = R.drawable.baseline_preview_24),
@ -63,19 +65,19 @@ fun LabFragment() {
)
Function(
title = "中二病模式",
desc = "也许会导致奇怪的问题,大抵就是你看不懂罢了。",
title = LocalString.b2Mode,
desc = LocalString.b2ModeDesc,
descColor = it,
isSwitch = ShamrockConfig.is2B(ctx)
) {
ShamrockConfig.set2B(ctx, it)
scope.toast(ctx, "重启生效哦!")
scope.toast(ctx, LocalString.restartToast)
return@Function true
}
Function(
title = "显示调试日志",
desc = "会导致日志刷屏。",
title = LocalString.showDebugLog,
desc = LocalString.showDebugLogDesc,
descColor = it,
isSwitch = ShamrockConfig.isDebug(ctx)
) {
@ -90,7 +92,92 @@ fun LabFragment() {
modifier = Modifier.padding(top = 12.dp),
painter = painterResource(id = R.drawable.round_logo_dev_24),
title = "实验功能"
) {
) { color ->
Column {
Divider(
modifier = Modifier,
color = GlobalColor.Divider,
thickness = 0.2.dp
)
/*
Function(
title = "自动清理QQ垃圾",
desc = "也许会导致奇怪的问题(无效)。",
descColor = color,
isSwitch = ShamrockConfig.isAutoClean(ctx)
) {
ShamrockConfig.setAutoClean(ctx, it)
ShamrockConfig.pushUpdate(ctx)
return@Function false
}*/
Function(
title = "自回复测试",
desc = "发送[ping],机器人发送一个具有调试信息的返回。",
descColor = color,
isSwitch = ShamrockConfig.enableAliveReply(ctx)
) {
ShamrockConfig.setAliveReply(ctx, it)
return@Function true
}
Function(
title = "开启Shell接口",
desc = "可能导致设备被入侵,请勿随意开启。",
descColor = color,
isSwitch = ShamrockConfig.allowShell(ctx)
) {
ShamrockConfig.setShellStatus(ctx, it)
return@Function true
}
Function(
title = "自动唤醒QQ",
desc = "QQ进程死亡时重新打开QQ进程前提本进程存活。",
descColor = color,
isSwitch = ShamrockConfig.enableAutoStart(ctx)
) {
ShamrockConfig.setAutoStart(ctx, it)
return@Function true
}
kotlin.runCatching {
ctx.getSharedPreferences("shared_config", Context.MODE_WORLD_READABLE)
}.onSuccess {
Function(
title = LocalString.persistentText,
desc = LocalString.persistentTextDesc,
descColor = color,
isSwitch = it.getBoolean("persistent", false)
) { v ->
it.edit().putBoolean("persistent", v).apply()
scope.toast(ctx, LocalString.restartSysToast)
return@Function true
}
Function(
title = "禁用Doze模式",
desc = "禁止系统进入节能模式。",
descColor = color,
isSwitch = it.getBoolean("hook_doze", false)
) { value ->
it.edit().putBoolean("hook_doze", value).apply()
scope.toast(ctx, LocalString.restartSysToast)
return@Function true
}
}.onFailure {
AppRuntime.log("无法启用附加选项LSPosed模块未激活或者不支持XSharedPreferences", Level.WARN)
}
}
}
ActionBox(
modifier = Modifier.padding(top = 12.dp),
painter = painterResource(id = R.drawable.sharp_lock_24),
title = "安全性设置"
) { color ->
Column {
Divider(
modifier = Modifier,
@ -99,20 +186,9 @@ fun LabFragment() {
)
Function(
title = "自动清理QQ垃圾",
desc = "也许会导致奇怪的问题(无效)。",
descColor = it,
isSwitch = ShamrockConfig.isAutoClean(ctx)
) {
ShamrockConfig.setAutoClean(ctx, it)
ShamrockConfig.pushUpdate(ctx)
return@Function false
}
Function(
title = "拦截QQ无用收包",
desc = "测试阶段,可能导致网络异常或掉线。",
descColor = it,
title = LocalString.injectPacket,
desc = LocalString.injectPacketDesc,
descColor = color,
isSwitch = ShamrockConfig.isInjectPacket(ctx)
) {
ShamrockConfig.setInjectPacket(ctx, it)
@ -121,26 +197,31 @@ fun LabFragment() {
}
Function(
title = "自动唤醒QQ",
desc = "QQ进程死亡时重新打开QQ进程前提本进程存活。",
descColor = it,
isSwitch = ShamrockConfig.enableAutoStart(ctx)
title = LocalString.antiTrace,
desc = LocalString.antiTraceDesc,
descColor = color,
isSwitch = ShamrockConfig.isAntiTrace(ctx)
) {
ShamrockConfig.setAutoStart(ctx, it)
ShamrockConfig.setAntiTrace(ctx, it)
ShamrockConfig.pushUpdate(ctx)
return@Function true
}
Function(
title = "开启Shell接口",
desc = "可能导致设备被入侵,请勿随意开启。",
descColor = it,
isSwitch = ShamrockConfig.allowShell(ctx)
) {
ShamrockConfig.setShellStatus(ctx, it)
return@Function true
kotlin.runCatching {
ctx.getSharedPreferences("shared_config", Context.MODE_WORLD_READABLE)
}.onSuccess {
Function(
title = "反检测加强",
desc = "可能导致某些设备频繁闪退",
descColor = color,
isSwitch = it.getBoolean("super_anti", false)
) { v ->
it.edit().putBoolean("super_anti", v).apply()
scope.toast(ctx, LocalString.restartToast)
return@Function true
}
}
}
}
ActionBox(
@ -161,7 +242,11 @@ fun LabFragment() {
descColor = it,
isSwitch = AppRuntime.state.supportVoice.value
) {
if(AppRuntime.state.supportVoice.value) {
scope.toast(ctx, "关闭请手动删除文件。")
} else {
scope.toast(ctx, "请按照Github提示手动操作。")
}
return@Function false
}
}

View File

@ -69,6 +69,24 @@ private open class Chūnibyō: Default() {
"执明起,至除免于灾祸。\n" +
"元冥浩浩,非凡不可动之。"
labWarning = "寒酥降矣,梅熟日久,莫不可测。"
logTitle = "无极"
testName = "未名之人"
logCentralLoadSuccessfully = "无极开,天地始纷争。"
logCentralLoadFailed = "无极闭,天地始归宁。"
functionSetting = "天地法则"
sslSetting = "天行御令"
warnTitle = "仙人指路"
b2Mode = "通仙之路"
b2ModeDesc = "凡人勿近"
restartToast = "复关喏哉!"
showDebugLog = "窥探天机"
showDebugLogDesc = "迷失自我,走火入魔"
antiTrace = "鬼影迷踪"
antiTraceDesc = "唐门绝学,已有取死之道"
injectPacket = "遮匿无用之禀"
injectPacketDesc = "试于试之,逆则魂飞魄散"
persistentText = "丹书铁券"
persistentTextDesc = "由天地之起也,须复动之。"
}
}
@ -84,7 +102,25 @@ private open class Default: VarString(
"同时声明本项目仅用于学习与交流请于24小时内删除。\n" +
"同时开源贡献者均享受免责条例。",
labWarning = "实验室功能可能会导致出乎意料的BUG!",
"日志"
logTitle = "日志",
testName = "测试昵称",
logCentralLoadSuccessfully = "日志框架激活成功,开放操作许可。",
logCentralLoadFailed = "日志框架处于未激活状态,请检查。",
functionSetting = "功能设置",
sslSetting = "SSL配置",
warnTitle = "温馨提示",
b2Mode = "中二病模式",
b2ModeDesc = "也许会导致奇怪的问题,大抵就是你看不懂罢了。",
restartToast = "重启生效哦!",
restartSysToast = "重启系统生效哦!",
showDebugLog = "显示调试日志",
showDebugLogDesc = "会导致日志刷屏。",
antiTrace = "防止调用栈检测",
antiTraceDesc = "防止QQ进行堆栈跟踪检测需要重新启动QQ。",
injectPacket = "拦截QQ无用收包",
injectPacketDesc = "测试阶段,可能导致网络异常或掉线。",
persistentText = "免死金牌",
persistentTextDesc = "由系统复活QQ和Shamrock需要重新启动系统。"
)
open class VarString(
@ -99,5 +135,33 @@ open class VarString(
var labWarning: String,
var logTitle: String
var logTitle: String,
var testName: String,
var logCentralLoadSuccessfully: String,
var logCentralLoadFailed: String,
var functionSetting: String,
var sslSetting: String,
var warnTitle: String,
var b2Mode: String,
var b2ModeDesc: String,
var restartToast: String,
var restartSysToast: String,
var showDebugLog: String,
var showDebugLogDesc: String,
var antiTrace: String,
var antiTraceDesc: String,
var injectPacket: String,
var injectPacketDesc: String,
var persistentText: String,
var persistentTextDesc: String
)

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#9D9D9D"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,8h-3L17,6.21c0,-2.61 -1.91,-4.94 -4.51,-5.19C9.51,0.74 7,3.08 7,6v2L4,8v14h16L20,8zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM9,8L9,6c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2L9,8z"/>
</vector>

View File

@ -33,7 +33,7 @@ public class MMKV implements SharedPreferences, SharedPreferences.Editor {
return null;
}
public SharedPreferences.Editor putBoolean(String str, boolean z) {
public SharedPreferences.Editor putBoolean(String s, boolean z) {
return this;
}

View File

@ -0,0 +1,4 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class GuildInteractiveNotificationItem {
}

View File

@ -0,0 +1,4 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class GuildNotificationAbstractInfo {
}

View File

@ -42,6 +42,10 @@ public interface IKernelMsgListener {
void onGroupTransferInfoUpdate(GroupFileListResult groupFileListResult);
void onGuildInteractiveUpdate(GuildInteractiveNotificationItem guildInteractiveNotificationItem);
void onGuildNotificationAbstractUpdate(GuildNotificationAbstractInfo guildNotificationAbstractInfo);
void onHitCsRelatedEmojiResult(DownloadRelateEmojiResultInfo downloadRelateEmojiResultInfo);
void onHitEmojiKeywordResult(HitRelatedEmojiWordsResult hitRelatedEmojiWordsResult);

View File

@ -96,5 +96,11 @@ dependencies {
//ksp("androidx.room:room-compiler:$roomVersion")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$roomVersion")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.06.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}

View File

@ -0,0 +1 @@
libclover.so

View File

@ -1,38 +1,17 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("xposed")
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
project("clover")
include_directories(helper)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
xposed.cpp)
anti_detection/anti_detection.cpp
helper/jnihelper.cpp
clover.cpp)
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)

View File

@ -0,0 +1,6 @@
#include "anti_detection.h"

View File

@ -0,0 +1,24 @@
#ifndef SHAMROCK_ANTI_DETECTION_H
#define SHAMROCK_ANTI_DETECTION_H
#include <vector>
#include <string>
#include <initializer_list>
#include "lsposed.h"
#include "jnihelper.h"
static std::vector<std::string> qemu_detect_props = {
"init.svc.qemu-props", "qemu.hw.mainkeys", "qemu.sf.fake_camera", "ro.kernel.android.qemud",
"qemu.sf.lcd_density", "init.svc.qemud", "ro.kernel.qemu",
"libc.debug.malloc"
};
static int (*backup_system_property_get)(const char *name, char *value);
static FILE* (*backup_fopen)(const char *filename, const char *mode);
//int fake_system_property_get(const char *name, char *value);
//FILE* fake_fopen(const char *filename, const char *mode);
//void on_library_loaded(const char *name, void *handle);
#endif //SHAMROCK_ANTI_DETECTION_H

View File

@ -0,0 +1,123 @@
#include <jni.h>
#include "anti_detection/anti_detection.h"
#include "helper/lsposed.h"
#include "jnihelper.h"
static JavaVM *global_jvm = nullptr;
static HookFunType hook_function = nullptr;
extern "C" [[gnu::visibility("default")]] [[gnu::used]]
jint JNI_OnLoad(JavaVM *jvm, void*) {
global_jvm = jvm;
int attach = 0;
JNIEnv *env = JNIHelper::getJNIEnv(jvm, &attach);
// do something
LOGI("[Shamrock] JNI_OnLoad NativeModule Init: %p", env);
if (attach == 1) {
JNIHelper::delJNIEnv(jvm);
}
//hook_function((void *)env->functions->FindClass, (void *)fake_FindClass, (void **)&backup_FindClass);
return JNI_VERSION_1_6;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_moe_fuqiuluo_shamrock_xposed_XposedEntry_00024Companion_injected(JNIEnv *env, jobject thiz) {
LOGI("[Shamrock] injected: %p", hook_function);
return hook_function != nullptr;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_moe_fuqiuluo_shamrock_xposed_XposedEntry_00024Companion_hasEnv(JNIEnv *env, jobject thiz) {
LOGI("[Shamrock] hasEnv: %p", global_jvm);
return global_jvm != nullptr;
}
int fake_system_property_get(const char *name, char *value) {
for (auto &prop: qemu_detect_props) {
if (strstr(name, prop.c_str())) {
LOGI("[Shamrock] bypass qemu detection");
value[0] = 0;
return 0;
}
}
if (strstr(name, "ro.debuggable")
|| strstr(name, "ro.kernel.qemu.gles")
|| strstr(name, "debug.atrace.tags.enableflags")) {
strcpy(value, "0");
return 1;
}
if (strstr(name, "ro.product.cpu.abilist")) {
int len = backup_system_property_get(name, value);
if (len > 0) {
if (strstr(value, "x86")) {
strcpy(value, "arm64-v8a,armeabi-v7a,armeabi");
return 29;
}
}
return len;
}
if (strstr(name, "ro.hardware")) {
int len = backup_system_property_get(name, value);
if (len > 0) {
if (strstr(value, "generic")
|| strstr(value, "unknown")
|| strstr(value, "emulator")
|| strstr(value, "vbox")
|| strstr(value, "genymotion")
|| strstr(value, "goldfish")) {
strcpy(value, "qcom");
return 4;
}
}
return len;
}
//LOGI("[Shamrock] fake_system_property_get(%s)", name);
return backup_system_property_get(name, value);
}
FILE* fake_fopen(const char *filename, const char *mode) {
if (strstr(filename, "qemu_pipe")) {
LOGI("[Shamrock] bypass qemu detection");
return nullptr;
}
if (strstr(filename, "libhoudini.so")) {
LOGI("[Shamrock] bypass emu detection");
return nullptr;
}
return backup_fopen(filename, mode);
}
void on_library_loaded(const char *name, void *handle) {
}
extern "C" [[gnu::visibility("default")]] [[gnu::used]]
NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
hook_function = entries->hook_func;
LOGI("[Shamrock] LSPosed NativeModule Init: %p", hook_function);
return on_library_loaded;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_moe_fuqiuluo_shamrock_xposed_actions_AntiDetection_antiNativeDetections(JNIEnv *env,
jobject thiz) {
if (hook_function == nullptr) return false;
hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get);
hook_function((void*) fopen, (void*) fake_fopen, (void**) &backup_fopen);
return true;
}

View File

@ -0,0 +1,26 @@
#include "jnihelper.h"
JNIEnv *JNIHelper::getJNIEnv(JavaVM * jvm, int *attach) {
if (jvm == NULL) return NULL;
*attach = 0;
JNIEnv *jni_env = NULL;
int status = jvm->GetEnv((void **)&jni_env, JNI_VERSION_1_6);
if (status == JNI_EDETACHED || jni_env == NULL) {
status = jvm->AttachCurrentThread(&jni_env, NULL);
if (status < 0) {
jni_env = NULL;
} else {
*attach = 1;
}
}
return jni_env;
}
jint JNIHelper::delJNIEnv(JavaVM * jvm) {
if (jvm == nullptr) return 0;
return jvm->DetachCurrentThread();
}

View File

@ -0,0 +1,14 @@
#ifndef SHAMROCK_JNIHELPER_H
#define SHAMROCK_JNIHELPER_H
#include "jni.h"
#include "android/log.h"
namespace JNIHelper {
JNIEnv *getJNIEnv(JavaVM * jvm, int *attach);
jint delJNIEnv(JavaVM * jvm);
}
#endif //SHAMROCK_JNIHELPER_H

View File

@ -0,0 +1,27 @@
#ifndef SHAMROCK_LSPOSED_H
#define SHAMROCK_LSPOSED_H
#include "stdint.h"
#define TAG "LSPosed-Bridge"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__)
typedef int (*HookFunType)(void *func, void *replace, void **backup);
typedef int (*UnhookFunType)(void *func);
typedef void (*NativeOnModuleLoaded)(const char *name, void *handle);
typedef struct {
uint32_t version;
HookFunType hook_func;
UnhookFunType unhook_func;
} NativeAPIEntries;
typedef NativeOnModuleLoaded (*NativeInit)(const NativeAPIEntries *entries);
#endif //SHAMROCK_LSPOSED_H

View File

@ -1,5 +0,0 @@
#include <jni.h>
#include <string>
#include <utility>
#include <sys/auxv.h>

View File

@ -50,7 +50,7 @@ val ProtoValue.asLong: Long
get() = (this as ProtoNumber).value.toLong()
val ProtoValue.asULong: Long
get() = (this as ProtoNumber).value.toLong() and 0xFFFFFFFFL
get() = (this as ProtoNumber).value.toLong() and Long.MAX_VALUE
val ProtoValue.asMap: ProtoMap
get() = (this as ProtoMap)

View File

@ -166,7 +166,11 @@ internal object FileSvc: BaseSvc() {
modifyTime = fileInfo.uint32_modify_time.get(),
downloadTimes = fileInfo.uint32_download_times.get(),
uploadUin = fileInfo.uint64_uploader_uin.get(),
uploadNick = fileInfo.str_uploader_name.get()
uploadNick = fileInfo.str_uploader_name.get(),
md5 = fileInfo.bytes_md5.get().toByteArray().toHexString(),
sha = fileInfo.bytes_sha.get().toByteArray().toHexString(),
// 根本没有
sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString(),
))
}
else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) {

View File

@ -13,11 +13,9 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import mqq.app.AppRuntime
import mqq.app.MobileQQ
import tencent.mobileim.structmsg.`structmsg$FlagInfo`
import tencent.mobileim.structmsg.`structmsg$ReqSystemMsgNew`
import tencent.mobileim.structmsg.`structmsg$RspSystemMsgNew`
@ -58,7 +56,10 @@ internal object FriendSvc: BaseSvc() {
)
}
suspend fun requestFriendSystemMsgNew(msgNum: Int, latestFriendSeq: Long, latestGroupSeq: Long): List<StructMsg>? {
suspend fun requestFriendSystemMsgNew(msgNum: Int, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 3): List<StructMsg>? {
if (retryCnt < 0) {
return ArrayList()
}
val req = `structmsg$ReqSystemMsgNew`()
req.msg_num.set(msgNum)
req.latest_friend_seq.set(latestFriendSeq)
@ -90,10 +91,18 @@ internal object FriendSvc: BaseSvc() {
req.uint32_req_msg_type.set(1)
req.uint32_need_uid.set(1)
val respBuffer = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Friend", true, req.toByteArray())
?: return ArrayList()
val msg = `structmsg$RspSystemMsgNew`()
msg.mergeFrom(respBuffer.slice(4))
return msg.friendmsgs.get()
return if (respBuffer == null) {
ArrayList()
} else {
try {
val msg = `structmsg$RspSystemMsgNew`()
msg.mergeFrom(respBuffer.slice(4))
return msg.friendmsgs.get()
} catch (err: Throwable) {
requestFriendSystemMsgNew(msgNum, latestFriendSeq, latestGroupSeq, retryCnt - 1)
}
}
}

View File

@ -13,7 +13,22 @@ import com.tencent.mobileqq.troop.api.ITroopMemberInfoService
import com.tencent.protofile.join_group_link.join_group_link
import com.tencent.qphone.base.remote.ToServiceMsg
import com.tencent.qqnt.kernel.nativeinterface.MemberInfo
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import friendlist.stUinInfo
import io.ktor.client.call.body
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import io.ktor.http.contentType
import io.ktor.http.headers
import io.ktor.http.parameters
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
@ -22,15 +37,34 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asInt
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.proto.protobufOf
import moe.fuqiuluo.qqinterface.servlet.entries.ProhibitedMemberInfo
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.service.data.EssenceMessage
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncement
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessage
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessageImage
import moe.fuqiuluo.shamrock.tools.EmptyJsonArray
import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asLong
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.asStringOrNull
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import moe.fuqiuluo.shamrock.tools.putBuf32Long
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import tencent.im.oidb.cmd0x899.oidb_0x899
@ -284,7 +318,7 @@ internal object GroupSvc: BaseSvc() {
fun getOwner(groupId: String): Long {
val groupInfo = getGroupInfo(groupId)
return groupInfo.troopowneruin.toLong()
return groupInfo.troopowneruin?.toLong() ?: 0
}
fun isOwner(groupId: String): Boolean {
@ -609,21 +643,6 @@ internal object GroupSvc: BaseSvc() {
notSee: Boolean? = false,
subType: String
): Result<String>{
// val app = AppRuntimeFetcher.appRuntime
// if (app !is AppInterface)
// throw RuntimeException("AppRuntime cannot cast to AppInterface")
// val service = QRoute.api(IAddFriendTempApi::class.java)
// val action = `structmsg$SystemMsgActionInfo`()
// action.type.set(if (approve != false) 11 else 12)
// action.group_code.set(gid)
// action.msg.set(msg)
// val snInfo = `structmsg$AddFrdSNInfo`()
// snInfo.uint32_not_see_dynamic.set(if (notSee != false) 1 else 0)
//// snInfo.uint32_set_sn.set(0)
// action.addFrdSNInfo.set(snInfo)
// service.sendFriendSystemMsgAction(2, msgSeq * 1000, uin, 1, 2, 30024, 1, action, 0, `structmsg$StructMsg`(), false,
// app
// )
// 实在找不到接口了 发pb吧
val buffer: ByteArray
when (subType) {
@ -654,7 +673,9 @@ internal object GroupSvc: BaseSvc() {
7 to 1,
8 to mapOf(
1 to if (approve != false) 11 else 12,
2 to gid
2 to gid,
50 to msg,
53 to if (notSee != false) 1 else 0
),
9 to 1000
).toByteArray()
@ -670,14 +691,17 @@ internal object GroupSvc: BaseSvc() {
if (result[1, 1].asInt == 0) {
Result.success(result[2].asUtf8String)
} else {
Result.failure(Exception(result[2].asUtf8String))
Result.failure(Exception(result[1, 2].asUtf8String))
}
} else {
Result.failure(Exception("操作失败"))
}
}
suspend fun requestGroupSystemMsgNew(msgNum: Int, latestFriendSeq: Long, latestGroupSeq: Long): List<StructMsg>? {
suspend fun requestGroupSystemMsgNew(msgNum: Int, reqMsgType: Int = 1, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 5): List<StructMsg> {
if (retryCnt < 0) {
return ArrayList()
}
val req = ReqSystemMsgNew()
req.msg_num.set(msgNum)
req.latest_friend_seq.set(latestFriendSeq)
@ -706,12 +730,181 @@ internal object GroupSvc: BaseSvc() {
req.is_get_frd_ribbon.set(false)
req.is_get_grp_ribbon.set(false)
req.friend_msg_type_flag.set(1)
req.uint32_req_msg_type.set(1)
req.uint32_req_msg_type.set(reqMsgType)
req.uint32_need_uid.set(1)
val respBuffer = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Group", true, req.toByteArray())
?: return ArrayList()
val msg = RspSystemMsgNew()
msg.mergeFrom(respBuffer.slice(4))
return msg.groupmsgs.get()
return if (respBuffer == null) {
ArrayList()
} else {
try {
val msg = RspSystemMsgNew()
msg.mergeFrom(respBuffer.slice(4))
return msg.groupmsgs.get().orEmpty()
} catch (err: Throwable) {
requestGroupSystemMsgNew(msgNum, reqMsgType, latestFriendSeq, latestGroupSeq, retryCnt - 1)
}
}
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun getEssenceMessageList(groupId: Long, page: Int = 0, pageSize: Int = 20): Result<List<EssenceMessage>>{
// GlobalClient.get()
val cookie = TicketSvc.getCookie("qun.qq.com")
val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin()))
val url = "https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=${bkn}&group_code=${groupId}&page_start=${page}&page_limit=${pageSize}"
val response = GlobalClient.get(url) {
header("Cookie", cookie)
}
val body = Json.decodeFromStream<JsonElement>(response.body())
if (body.jsonObject["retcode"].asInt == 0) {
val data = body.jsonObject["data"].asJsonObject
val list = data["msg_list"].asJsonArrayOrNull
?: // is_end
return Result.success(ArrayList())
return Result.success(list.map {
val obj = it.jsonObject
val msgSeq = obj["msg_seq"].asInt
val msg = EssenceMessage(
senderId = obj["sender_uin"].asString.toLong(),
senderNick = obj["sender_nick"].asString,
senderTime = obj["sender_time"].asLong,
operatorId = obj["add_digest_uin"].asString.toLong(),
operatorNick = obj["add_digest_nick"].asString,
operatorTime = obj["add_digest_time"].asLong,
messageId = 0,
messageSeq = msgSeq,
messageContent = obj["msg_content"] ?: EmptyJsonArray
)
val mapping = MessageHelper.getMsgMappingBySeq(MsgConstant.KCHATTYPEGROUP, msgSeq)
if (mapping != null) {
msg.messageId = mapping.msgHashId
}
msg
})
} else {
return Result.failure(Exception(body.jsonObject["retmsg"].asStringOrNull))
}
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun getGroupAnnouncements(groupId: Long): Result<List<GroupAnnouncement>>{
val cookie = TicketSvc.getCookie("qun.qq.com")
val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin()))
val url = "https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=${bkn}&qid=${groupId}&ft=23&s=-1&n=20"
val response = GlobalClient.get(url) {
header("Cookie", cookie)
}
val body = Json.decodeFromStream<JsonElement>(response.body())
if (body.jsonObject["ec"].asInt == 0) {
val list = body.jsonObject["feeds"].asJsonArrayOrNull
?: return Result.success(ArrayList())
return Result.success(list.map {
val obj = it.jsonObject
GroupAnnouncement(
senderId = obj["u"].asLong,
publishTime = obj["pubt"].asLong,
message = GroupAnnouncementMessage(
text = obj["msg"].asJsonObject["text"].asString,
images = obj["msg"].asJsonObject["pics"].asJsonArrayOrNull?.map {
GroupAnnouncementMessageImage(
id = it.jsonObject["id"].asString,
width = it.jsonObject["w"].asString,
height = it.jsonObject["h"].asString,
)
} ?: ArrayList()
)
)
})
} else {
return Result.failure(Exception(body.jsonObject["em"].asStringOrNull))
}
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun uploadImageTroopNotice(image: String): Result<GroupAnnouncementMessageImage> {
val file = FileUtils.parseAndSave(image)
val cookie = TicketSvc.getCookie("qun.qq.com")
val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin()))
val response = GlobalClient.post("https://web.qun.qq.com/cgi-bin/announce/upload_img") {
headers {
header("Cookie", cookie)
}
contentType(ContentType.MultiPart.FormData)
setBody(MultiPartFormDataContent(
// 黑人问号 ktor默认formdata传的tx不认。默认是name=bkn非要写成name="bkn"才认?
formData {
append("filename", "001.png", Headers.build {
append(HttpHeaders.ContentDisposition, "name=\"filename\"")
})
append("source", "troopNotice", Headers.build {
append(HttpHeaders.ContentDisposition, "name=\"source\"")
})
append("bkn", bkn, Headers.build {
append(HttpHeaders.ContentDisposition, "name=\"bkn\"")
})
append("m", "0", Headers.build {
append(HttpHeaders.ContentDisposition, "name=\"m\"")
})
append("pic_up", file.readBytes(), Headers.build {
append(HttpHeaders.ContentType, "image/png")
append(HttpHeaders.ContentDisposition, "name=\"pic_up\" filename=\"001.png\"")
})
}
))
}
val body = Json.decodeFromStream<JsonElement>(response.body())
if (body.jsonObject["ec"].asInt == 0) {
var idJsonStr = body.jsonObject["id"].asStringOrNull
return if (idJsonStr != null) {
idJsonStr = idJsonStr.replace("&quot;", "\"")
val idJson = Json.decodeFromString<JsonElement>(idJsonStr)
LogCenter.log(idJson.toString())
Result.success(GroupAnnouncementMessageImage(
height = idJson.asJsonObject["h"].asString,
width = idJson.asJsonObject["w"].asString,
id = idJson.asJsonObject["id"].asString,
))
} else {
Result.failure(Exception("图片上传失败"))
}
} else {
return Result.failure(Exception(body.jsonObject["em"].asStringOrNull))
}
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun addQunNotice(groupId: Long, text: String, image: GroupAnnouncementMessageImage?) : Result<Boolean> {
val cookie = TicketSvc.getCookie("qun.qq.com")
val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin()))
val response = GlobalClient.submitForm(
url = "https://web.qun.qq.com/cgi-bin/announce/add_qun_notice",
formParameters = parameters {
append("qid", groupId.toString())
append("bkn", bkn)
append("text", text)
append("pinned", "0")
append("type", "1")
// todo allow custom settings
append("settings", "{\"is_show_edit_card:\"1,\"tip_window_type\":1,\"confirm_required\":1}")
if (null != image) {
append("pic", image.id)
append("imgWidth", image.width)
append("imgHeight", image.height)
}
},
block = {
headers {
header("Cookie", cookie)
}
}
)
val body = Json.decodeFromStream<JsonElement>(response.body())
return if (body.jsonObject["ec"].asInt == 0) {
Result.success(true)
} else {
Result.failure(Exception(body.jsonObject["em"].asStringOrNull))
}
}
}

View File

@ -16,14 +16,13 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.time.withTimeoutOrNull
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.JsonArray
import moe.fuqiuluo.proto.protobufOf
import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.SendMsgException
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import moe.fuqiuluo.shamrock.xposed.helper.msgService
@ -174,22 +173,38 @@ internal object MsgSvc: BaseSvc() {
chatType: Int,
peedId: String,
message: JsonArray,
fromId: String = peedId
): Pair<Long, Int> {
fromId: String = peedId,
retryCnt: Int = 3
): Result<Pair<Long, Int>> {
//LogCenter.log(message.toString(), Level.ERROR)
//callback.msgHash = result.second 什么垃圾代码万一cb比你快你不就寄了
// 主动临时消息
when(chatType) {
when (chatType) {
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
prepareTempChatFromGroup(fromId, peedId).onFailure {
LogCenter.log("主动临时消息,创建临时会话失败。", Level.ERROR)
return -1L to 0
return Result.failure(Exception("主动临时消息,创建临时会话失败。"))
}
}
}
return MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, MessageCallback(peedId, 0), fromId)
val result = MessageHelper.sendMessageWithoutMsgId(
chatType,
peedId,
message,
fromId,
MessageCallback(peedId, 0)
)
return if (result.isFailure
&& result.exceptionOrNull()?.javaClass == SendMsgException::class.java
&& retryCnt > 0) {
// 发送失败,可能网络问题出现红色感叹号,重试
// 例如 rich media transfer failed
delay(100)
sendToAio(chatType, peedId, message, fromId, retryCnt - 1)
} else {
result
}
}
suspend fun getMultiMsg(resId: String): Result<List<MsgRecord>> {

View File

@ -74,8 +74,13 @@ internal object TicketSvc: BaseSvc() {
}
suspend fun getCSRF(uin: String, domain: String): String {
// 是不是要用Skey
return getBkn(getPSKey(uin, domain) ?: "")
}
fun getBkn(arg: String): String {
var v: Long = 5381
for (element in getPSKey(uin, domain) ?: "") {
for (element in arg) {
v += (v shl 5 and 2147483647L) + element.code.toLong()
}
return (v and 2147483647L).toString()

View File

@ -27,6 +27,10 @@ data class FileInfo(
@SerialName("download_times") val downloadTimes: Int,
@SerialName("uploader") val uploadUin: Long,
@SerialName("upload_name") val uploadNick: String,
@SerialName("sha") val sha: String,
@SerialName("sha3") val sha3: String,
@SerialName("md5") val md5: String,
)
@Serializable

View File

@ -22,6 +22,9 @@ import com.tencent.qqnt.kernel.nativeinterface.ReplyElement
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
import com.tencent.qqnt.kernel.nativeinterface.TextElement
import com.tencent.qqnt.kernel.nativeinterface.VideoElement
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import moe.fuqiuluo.qqinterface.servlet.CardSvc
@ -70,6 +73,8 @@ import tencent.im.oidb.cmd0xdc2.oidb_cmd0xdc2
import tencent.im.oidb.oidb_sso
import java.io.File
import kotlin.math.roundToInt
import kotlin.random.Random
import kotlin.random.nextInt
internal typealias IMaker = suspend (Int, Long, String, JsonObject) -> Result<MsgElement>
@ -96,9 +101,91 @@ internal object MessageMaker {
"touch" to MessageMaker::createTouchElem,
"weather" to MessageMaker::createWeatherElem,
"json" to MessageMaker::createJsonElem,
"new_dice" to MessageMaker::createNewDiceElem,
"new_rps" to MessageMaker::createNewRpsElem,
"basketball" to MessageMaker::createBasketballElem,
//"node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct,
)
// private suspend fun createNodeElem(
// chatType: Int,
// msgId: Long,
// peerId: String,
// data: JsonObject
// ): Result<MsgElement> {
// data.checkAndThrow("data")
// SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray)
//
// }
/**\
* msgElement.setFaceElement(new FaceElement());
* msgElement.getFaceElement().setFaceIndex(114);
* msgElement.getFaceElement().setFaceText("/篮球");
* msgElement.getFaceElement().setFaceType(3);
* msgElement.getFaceElement().setPackId("1");
* msgElement.getFaceElement().setStickerId("13");
* msgElement.getFaceElement().setRandomType(1);
* msgElement.getFaceElement().setImageType(1);
* msgElement.getFaceElement().setStickerType(2);
* msgElement.getFaceElement().setSourceType(1);
* msgElement.getFaceElement().setSurpriseId("");
* msgElement.getFaceElement().setResultId(String.valueOf(new Random().nextInt(5) + 1));
*/
private suspend fun createBasketballElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACE
val face = FaceElement()
face.faceIndex = 114
face.faceText = "/篮球"
face.faceType = 3
face.packId = "1"
face.stickerId = "13"
face.sourceType = 1
face.stickerType = 2
face.resultId = Random.nextInt(1 .. 5).toString()
face.surpriseId = ""
face.randomType = 1
elem.faceElement = face
return Result.success(elem)
}
private suspend fun createNewRpsElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACE
val face = FaceElement()
face.faceIndex = 359
face.faceText = "/包剪锤"
face.faceType = 3
face.packId = "1"
face.stickerId = "34"
face.sourceType = 1
face.stickerType = 2
face.resultId = ""
face.surpriseId = ""
face.randomType = 1
elem.faceElement = face
return Result.success(elem)
}
private suspend fun createNewDiceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACE
val face = FaceElement()
face.faceIndex = 358
face.faceText = "/骰子"
face.faceType = 3
face.packId = "1"
face.stickerId = "33"
face.sourceType = 1
face.stickerType = 2
face.resultId = ""
face.surpriseId = ""
face.randomType = 1
elem.faceElement = face
return Result.success(elem)
}
private suspend fun createJsonElem(
chatType: Int,
msgId: Long,
@ -107,7 +194,20 @@ internal object MessageMaker {
): Result<MsgElement> {
data.checkAndThrow("data")
val jsonStr = data["data"].let {
if (it is JsonObject) it.asJsonObject.toString() else it.asString
if (it is JsonObject) it.asJsonObject.toString() else {
val str = it.asStringOrNull ?: ""
// 检查字符串是否是合法json不然qq会闪退
try {
val element = Json.decodeFromString<JsonElement>(str)
if (element !is JsonObject) {
return Result.failure(Exception("不合法的JSON字符串"))
}
} catch (err: Throwable) {
LogCenter.log(err.stackTraceToString(), Level.ERROR)
return Result.failure(Exception("不合法的JSON字符串"))
}
str
}
}
val element = MsgElement()
element.elementType = MsgConstant.KELEMTYPEARKSTRUCT
@ -634,24 +734,26 @@ internal object MessageMaker {
}
private suspend fun createImageElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
data.checkAndThrow("file")
val isOriginal = data["original"].asBooleanOrNull ?: true
val isFlash = data["flash"].asBooleanOrNull ?: false
val file = data["file"].asString.let {
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
var tmpPicFile = if (md5.length == 32) {
val filePath = data["file"].asStringOrNull
val url = data["url"].asStringOrNull
var file: File? = null
if (filePath != null) {
val md5 = filePath.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
file = if (md5.length == 32) {
FileUtils.getFile(md5)
} else {
FileUtils.parseAndSave(it)
FileUtils.parseAndSave(filePath)
}
if (!tmpPicFile.exists() && data.containsKey("url")) {
tmpPicFile = FileUtils.parseAndSave(data["url"].asString)
}
return@let tmpPicFile
}
if (!file.exists()) {
if ((file == null || !file.exists()) && url != null) {
file = FileUtils.parseAndSave(url)
}
if (file?.exists() == false) {
throw LogicException("Image(${file.name}) file is not exists, please check your filename.")
}
requireNotNull(file)
Transfer with when (chatType) {
MsgConstant.KCHATTYPEGROUP -> Troop(peerId)

View File

@ -55,12 +55,42 @@ internal sealed class MessageElemConverter: IMessageConvert {
)
)
}
return MessageSegment(
type = "face",
data = hashMapOf(
"id" to face.faceIndex
when (face.faceIndex) {
114 -> {
return MessageSegment(
type = "basketball",
data = hashMapOf(
"id" to face.resultId.ifEmpty { "0" }.toInt(),
)
)
}
358 -> {
if (face.sourceType == 1) return MessageSegment("new_dice")
return MessageSegment(
type = "new_dice",
data = hashMapOf(
"id" to face.resultId.ifEmpty { "0" }.toInt()
)
)
}
359 -> {
if (face.resultId.isEmpty()) return MessageSegment("new_rps")
return MessageSegment(
type = "new_rps",
data = hashMapOf(
"id" to face.resultId.ifEmpty { "0" }.toInt()
)
)
}
else -> return MessageSegment(
type = "face",
data = hashMapOf(
"id" to face.faceIndex
)
)
)
}
}
}
@ -289,23 +319,23 @@ internal sealed class MessageElemConverter: IMessageConvert {
element: MsgElement
): MessageSegment {
val tip = element.grayTipElement
when(val tipType = tip.subElementType) {
when(tip.subElementType) {
MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
val notify = tip.jsonGrayTipElement
when(notify.busiId) {
/* 新人入群 */ 17L,
/* 群戳一戳 */1061L, /* 群撤回 */1014L -> {}
else -> LogCenter.log("不支持的灰条类型(JSON): $tipType", Level.WARN)
/* 新人入群 */ 17L, /* 群戳一戳 */1061L,
/* 群撤回 */1014L, /* 群设精消息 */2401L -> {}
else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
}
}
MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
val notify = tip.xmlElement
when(notify.busiId) {
/* 群戳一戳 */12L -> {}
else -> LogCenter.log("不支持的灰条类型(XML): $tipType", Level.WARN)
/* 群戳一戳 */1061L -> {}
else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
}
}
else -> LogCenter.log("不支持的提示类型: $tip", Level.WARN)
else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
}
// 提示类消息这里提供的是一个xml不具备解析通用性
// 在这里不推送
@ -327,10 +357,10 @@ internal sealed class MessageElemConverter: IMessageConvert {
val fileSize = fileMsg.fileSize
val expireTime = fileMsg.expireTime ?: 0
val fileId = fileMsg.fileUuid
val bizId = fileMsg.fileBizId
val bizId = fileMsg.fileBizId ?: 0
val fileSubId = fileMsg.fileSubId ?: ""
val url = if (chatType == MsgConstant.KCHATTYPEC2C) RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
else RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, fileMsg.fileBizId)
else RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
return MessageSegment(
type = "file",

View File

@ -12,3 +12,4 @@ internal class LogicException(why: String) : InternalMessageMakerError(why)
internal object ErrorTokenException : InternalMessageMakerError("access_token error")
internal class SendMsgException(why: String) : InternalMessageMakerError(why)

View File

@ -13,6 +13,7 @@ import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.xposed.actions.toast
import moe.fuqiuluo.shamrock.xposed.helper.internal.DataRequester
import mqq.app.MobileQQ
import java.io.File
import java.util.Date
internal enum class Level(
@ -26,19 +27,53 @@ internal enum class Level(
@SuppressLint("SimpleDateFormat")
internal object LogCenter {
private val logFileBaseName = MobileQQ.getMobileQQ().qqProcessName.replace(":", ".") + "_${
// 格式化时间
SimpleDateFormat("yyyy-MM-dd").format(Date())
}_"
private val LogFile = MobileQQ.getContext().getExternalFilesDir(null)!!
.parentFile!!.resolve("Tencent/Shamrock/log").also {
if (it.exists()) it.delete()
it.mkdirs()
}.let {
var i = 1
lateinit var result: File
while (true) {
result = it.resolve("$logFileBaseName$i.log")
if (result.exists()) {
i++
} else {
break
}
}
return@let result
}
.resolve(MobileQQ.getMobileQQ().qqProcessName.replace(":", ".") + "_${
// 格式化时间
SimpleDateFormat("yyyy-MM-dd").format(Date())
}_" + ".log")
private val format = SimpleDateFormat("[HH:mm:ss] ")
fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) =
log({ string }, level, toast)
fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) {
if (!ShamrockConfig.isDebug() && level == Level.DEBUG) {
return
}
if (toast) {
MobileQQ.getContext().toast(string)
}
// 把日志广播到主进程
GlobalScope.launch(Dispatchers.Default) {
DataRequester.request("send_message", bodyBuilder = {
put("string", string)
put("level", level.id)
})
}
if (!LogFile.exists()) {
LogFile.createNewFile()
}
val format = "%s%s %s\n".format(format.format(Date()), level.name, string)
LogFile.appendText(format)
}
fun log(
string: () -> String,

View File

@ -6,6 +6,10 @@ import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.msg.api.IMsgService
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
@ -20,6 +24,8 @@ import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.tools.jsonArray
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.math.abs
internal object MessageHelper {
@ -36,31 +42,61 @@ internal object MessageHelper {
}.second.filter {
it.elementType != -1
} as ArrayList<MsgElement>
return sendMessageWithoutMsgId(chatType, peerId, msg, callback, fromId)
return sendMessageWithoutMsgId(chatType, peerId, msg, fromId, callback)
}
@OptIn(DelicateCoroutinesApi::class)
suspend fun sendMessageWithoutMsgId(
chatType: Int,
peerId: String,
message: JsonArray,
callback: IOperateCallback,
fromId: String = peerId
): Pair<Long, Int> {
fromId: String = peerId,
callback: IOperateCallback
): Result<Pair<Long, Int>> {
val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, message).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
}.second.filter {
it.elementType != -1
} as ArrayList<MsgElement>
return sendMessageWithoutMsgId(chatType, peerId, msg, callback, fromId)
val totalSize = msg.filter {
it.elementType == MsgConstant.KELEMTYPEPIC ||
it.elementType == MsgConstant.KELEMTYPEPTT ||
it.elementType == MsgConstant.KELEMTYPEVIDEO
}.map {
(it.picElement?.fileSize ?: 0) + (it.pttElement?.fileSize
?: 0) + (it.videoElement?.fileSize ?: 0)
}.reduceOrNull { a, b -> a + b } ?: 0
val estimateTime = (totalSize / (300 * 1024)) * 1000 + 5000
lateinit var sendResultPair: Pair<Long, Int>
val sendRet = withTimeoutOrNull<Pair<Int, String>>(estimateTime) {
suspendCoroutine {
GlobalScope.launch {
sendResultPair = sendMessageWithoutMsgId(
chatType,
peerId,
msg,
fromId
) { code, message ->
callback.onResult(code, message)
it.resume(code to message)
}
}
}
}
if (sendRet?.first != 0) {
return Result.failure(SendMsgException(sendRet?.second ?: "发送消息超时"))
}
return Result.success(sendResultPair)
}
suspend fun sendMessageWithoutMsgId(
chatType: Int,
peerId: String,
message: ArrayList<MsgElement>,
callback: IOperateCallback,
fromId: String = peerId
fromId: String = peerId,
callback: IOperateCallback
): Pair<Long, Int> {
return sendMessageWithoutMsgId(generateContact(chatType, peerId, fromId), message, callback)
}
@ -74,7 +110,7 @@ internal object MessageHelper {
val nonMsg: Boolean = message.isEmpty()
return if (!nonMsg) {
val service = QRoute.api(IMsgService::class.java)
if(callback is MsgSvc.MessageCallback) {
if (callback is MsgSvc.MessageCallback) {
callback.msgHash = uniseq.first
}
@ -107,7 +143,7 @@ internal object MessageHelper {
val nonMsg: Boolean = message.isEmpty()
return if (!nonMsg) {
val service = QRoute.api(IMsgService::class.java)
if(callback is MsgSvc.MessageCallback) {
if (callback is MsgSvc.MessageCallback) {
callback.msgHash = uniseq.first
}
@ -132,7 +168,7 @@ internal object MessageHelper {
val nonMsg: Boolean = message.isEmpty()
return if (!nonMsg) {
val service = QRoute.api(IMsgService::class.java)
if(callback is MsgSvc.MessageCallback) {
if (callback is MsgSvc.MessageCallback) {
callback.msgHash = uniseq.first
}
@ -148,6 +184,32 @@ internal object MessageHelper {
}
}
suspend fun sendMessageNoCb(
chatType: Int,
peerId: String,
message: JsonArray,
fromId: String = peerId
): Pair<Int, Long> {
val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, message).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
}.second.filter {
it.elementType != -1
} as ArrayList<MsgElement>
val contact = generateContact(chatType, peerId, fromId)
val nonMsg: Boolean = message.isEmpty()
return if (!nonMsg) {
val service = QRoute.api(IMsgService::class.java)
return suspendCoroutine {
service.sendMsg(contact, uniseq.second, msg) { code, why ->
it.resume(code to uniseq.second)
}
}
} else {
-1 to uniseq.second
}
}
suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact {
val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) {
ContactHelper.getUidByUinAsync(id.toLong())
@ -156,7 +218,7 @@ internal object MessageHelper {
}
fun obtainMessageTypeByDetailType(detailType: String): Int {
return when(detailType) {
return when (detailType) {
"troop", "group" -> MsgConstant.KCHATTYPEGROUP
"private" -> MsgConstant.KCHATTYPEC2C
"less" -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
@ -166,7 +228,7 @@ internal object MessageHelper {
}
fun obtainDetailTypeByMsgType(msgType: Int): String {
return when(msgType) {
return when (msgType) {
MsgConstant.KCHATTYPEGROUP -> "group"
MsgConstant.KCHATTYPEC2C -> "private"
MsgConstant.KCHATTYPEGUILD -> "guild"
@ -180,9 +242,9 @@ internal object MessageHelper {
var hasActionMsg = false
messageList.forEach {
val msg = it.jsonObject
try {
val maker = MessageMaker[msg["type"].asString]
if (maker != null) {
val maker = MessageMaker[msg["type"].asString]
if (maker != null) {
try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem ->
msgList.add(msgElem)
@ -193,16 +255,19 @@ internal object MessageHelper {
hasActionMsg = true
}
}
} catch (e: Throwable) {
LogCenter.log(e.stackTraceToString(), Level.ERROR)
}
} catch (e: Throwable) {
LogCenter.log(e.stackTraceToString(), Level.ERROR)
} else {
LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR)
return false to arrayListOf()
}
}
return hasActionMsg to msgList
}
fun generateMsgIdHash(chatType: Int, msgId: Long): Int {
val key = when (chatType) {
val key = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> "grp$msgId"
MsgConstant.KCHATTYPEC2C -> "c2c$msgId"
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> "tmpgrp$msgId"

View File

@ -8,8 +8,10 @@ import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.asStringOrNull
import moe.fuqiuluo.shamrock.utils.MD5
internal object MusicHelper {
@ -53,12 +55,26 @@ internal object MusicHelper {
val trackInfo = data["track_info"].asJsonObject
val mid = trackInfo["mid"].asString
val previewMid = trackInfo["album"].asJsonObject["mid"].asString
val singerMid = trackInfo["singer"].asJsonArrayOrNull?.let {
it[0].asJsonObject["mid"].asStringOrNull
} ?: ""
val name = trackInfo["name"].asString
val title = trackInfo["title"].asString
val singerName = trackInfo["singer"].asJsonArray.first().asJsonObject["name"].asString
val vs = trackInfo["vs"].asJsonArrayOrNull?.let {
it[0].asStringOrNull
} ?: ""
val code = MD5.getMd5Hex("${mid}q;z(&l~sdf2!nK".toByteArray()).substring(0 .. 4).uppercase()
val playUrl = "http://c6.y.qq.com/rsc/fcgi-bin/fcg_pyq_play.fcg?songid=&songmid=$mid&songtype=1&fromtag=50&uin=&code=$code"
val previewUrl = "http://y.gtimg.cn/music/photo_new/T002R180x180M000$previewMid.jpg"
val previewUrl = if (vs.isNotEmpty()) {
"http://y.gtimg.cn/music/photo_new/T062R150x150M000$vs}.jpg"
} else if (previewMid.isNotEmpty()) {
"http://y.gtimg.cn/music/photo_new/T002R150x150M000$previewMid.jpg"
} else if (singerMid.isNotEmpty()){
"http://y.gtimg.cn/music/photo_new/T001R150x150M000$singerMid.jpg"
} else {
""
}
val jumpUrl = "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=${mid}&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare"
ArkMsgSvc.tryShareMusic(
chatType,

View File

@ -31,11 +31,11 @@ internal object ActionManager {
// GroupActions
ModifyTroopName, LeaveTroop, KickTroopMember, BanTroopMember, SetGroupWholeBan, SetGroupAdmin,
ModifyTroopMemberName, SetGroupUnique, GetTroopHonor, GroupPoke, SetEssenceMessage, DeleteEssenceMessage,
GetGroupSystemMsg, GetProhibitedMemberList,
GetGroupSystemMsg, GetProhibitedMemberList, GetEssenceMessageList, GetGroupNotice, SendGroupNotice,
// MSG ACTIONS
SendMessage, DeleteMessage, GetMsg, GetForwardMsg, SendGroupForwardMsg, SendGroupMessage, SendPrivateMessage,
ClearMsgs, GetHistoryMsg, GetGroupMsgHistory, SendPrivateForwardMsg,
SendMessage, DeleteMessage, GetMsg, GetForwardMsg, SendPrivateForwardMessage, SendGroupMessage, SendPrivateMessage,
ClearMsgs, GetHistoryMsg, GetGroupMsgHistory, SendGroupForwardMessage,
// RESOURCE ACTION
GetRecord, GetImage, UploadGroupFile, CreateGroupFileFolder, DeleteGroupFolder,
@ -193,8 +193,8 @@ internal class ActionSession {
return params[key].asBoolean
}
fun <T: Boolean?> getBooleanOrDefault(key: String, default: T? = null): T {
return (params[key].asBooleanOrNull as? T) ?: default as T
fun getBooleanOrDefault(key: String, default: Boolean? = null): Boolean {
return params[key].asBooleanOrNull ?: default as Boolean
}
fun getObject(key: String): JsonObject {

View File

@ -0,0 +1,33 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object GetEssenceMessageList: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getLong("group_id")
val page = session.getIntOrNull("page") ?: 0
val pageSize = session.getIntOrNull("page_size") ?: 20
return invoke(groupId, page, pageSize, session.echo)
}
suspend operator fun invoke(groupId: Long, page: Int = 0, pageSize: Int = 20, echo: JsonElement = EmptyJsonString): String {
if (page < 0 || pageSize > 50) {
return badParam("参数不正确page_size不得大于50", echo)
}
val essenceMessageList = GroupSvc.getEssenceMessageList(groupId, page, pageSize)
if (essenceMessageList.isSuccess) {
return ok(essenceMessageList.getOrNull(), echo)
}
return logic(essenceMessageList.exceptionOrNull()?.message ?: "", echo)
}
override val alias: Array<String> = arrayOf("get_essence_message_list")
override fun path(): String = "get_essence_msg_list"
}

View File

@ -0,0 +1,28 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object GetGroupNotice: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getLong("group_id")
return invoke(groupId, session.echo)
}
suspend operator fun invoke(groupId: Long, echo: JsonElement = EmptyJsonString): String {
val announcements = GroupSvc.getGroupAnnouncements(groupId)
if (announcements.isSuccess) {
return ok(announcements.getOrNull(), echo)
}
return logic(announcements.exceptionOrNull()?.message ?: "", echo)
}
override val alias: Array<String> = arrayOf("get_group_notice")
override fun path(): String = "_get_group_notice"
}

View File

@ -10,16 +10,17 @@ import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object GetGroupSystemMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
return invoke(session.echo)
return invoke(echo = session.echo)
}
suspend operator fun invoke(echo: JsonElement = EmptyJsonString): String {
val list = GroupSvc.requestGroupSystemMsgNew(20, 0, 0)
val list = GroupSvc.requestGroupSystemMsgNew(20)
val riskList = GroupSvc.requestGroupSystemMsgNew(20, 2)
val msgs = GroupSystemMessage(
invited = mutableListOf(),
join = mutableListOf()
)
list?.forEach {
(list + riskList).forEach {
when(it.msg.group_msg_type.get()) {
22, 1 -> {
// join 进群消息
@ -41,8 +42,8 @@ internal object GetGroupSystemMsg: IActionHandler() {
// invite 别人邀请我
msgs.invited += GroupRequest (
msgSeq = it.msg_seq.get(),
invitorUin = null,
invitorNick = null,
invitorUin = it.msg.action_uin.get(),
invitorNick = it.msg.action_uin_nick.get(),
groupId = it.msg.group_code.get(),
groupName = it.msg.group_name.get(),
checked = it.msg.msg_decided.get().isNotBlank(),

View File

@ -0,0 +1,160 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
sealed interface ForwardMsgNode {
class MessageIdNode(
val id: Int
) : ForwardMsgNode
open class MessageNode(
val name: String,
val content: JsonElement?
) : ForwardMsgNode
object EmptyNode : MessageNode("", null)
}
internal object SendForwardMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type")
try {
val chatType = detailType?.let {
MessageHelper.obtainMessageTypeByDetailType(it)
} ?: run {
if (session.has("user_id")) {
MsgConstant.KCHATTYPEC2C
} else if (session.has("group_id")) {
MsgConstant.KCHATTYPEGROUP
} else {
return noParam("detail_type/message_type", session.echo)
}
}
val peerId = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam(
"group_id",
session.echo
)
MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam(
"user_id",
session.echo
)
else -> error("unknown chat type: $chatType")
}
if (session.isArray("messages")) {
val messages = session.getArray("messages")
invoke(chatType, peerId, messages, echo = session.echo)
}
return logic("未知格式合并转发消息", session.echo)
} catch (e: ParamsException) {
return noParam(e.message!!, session.echo)
} catch (e: Throwable) {
return logic(e.message ?: e.toString(), session.echo)
}
}
suspend operator fun invoke(
chatType: Int,
peerId: String,
message: JsonArray,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
val selfUin = TicketSvc.getUin()
val nodes = message.map {
if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段
it.asJsonObject["data"].asJsonObject.let { data ->
if (data.containsKey("content")) {
if (data["content"] is JsonArray) {
data["content"].asJsonArray.forEach { msg ->
if (msg.asJsonObject["type"].asStringOrNull == "node") {
LogCenter.log("合并转发消息不支持嵌套", Level.WARN)
return@map ForwardMsgNode.EmptyNode
}
}
}
ForwardMsgNode.MessageNode(
name = data["name"].asStringOrNull ?: "",
content = data["content"]
)
} else ForwardMsgNode.MessageIdNode(data["id"].asInt)
}
}.map {
if (it is ForwardMsgNode.MessageIdNode) {
val recordResult = MsgSvc.getMsg(it.id)
if (!recordResult.isFailure) {
ForwardMsgNode.EmptyNode
} else {
val record = recordResult.getOrThrow()
ForwardMsgNode.MessageNode(
name = record.peerName,
content = record.toSegments().map { segment ->
segment.toJson()
}.json
)
}
} else {
it as ForwardMsgNode.MessageNode
}
}.filter {
it.content != null
}.map { node ->
val result = MessageHelper.sendMessageNoCb(MsgConstant.KCHATTYPEC2C, selfUin, node.content.let { msg ->
when (msg) {
is JsonArray -> msg
is JsonObject -> listOf(msg).jsonArray
else -> MessageHelper.decodeCQCode(msg.asString)
}
})
if (result.first != 0) {
LogCenter.log("合并转发消息节点消息发送失败", Level.WARN)
}
return@map result.second
}
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
val to = MessageHelper.generateContact(chatType, peerId)
val uniseq = MessageHelper.generateMsgId(chatType)
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
nodes.forEach { add(MultiMsgInfo(it, "Anno")) }
}.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.first))
return ok(
ForwardMessageResult(
msgId = uniseq.first,
forwardId = ""
), echo = echo
)
}.onFailure {
return error("error: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}
override val requiredParams: Array<String> = arrayOf("message")
override fun path(): String = "send_forward_msg"
}

View File

@ -1,233 +0,0 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.asStringOrNull
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* 合并转发消息节点数据类
*/
sealed interface ForwardMsgNode {
class MessageIdNode(
val id: Int
): ForwardMsgNode
open class MessageNode(
val name: String,
val content: JsonElement?
): ForwardMsgNode
object EmptyNode: MessageNode("", null)
}
/**
* 私聊合并转发
*/
internal object SendPrivateForwardMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getString("user_id")
if (session.isArray("messages")) {
val messages = session.getArray("messages")
return invoke(messages, groupId, session.echo)
}
return logic("未知格式合并转发消息", session.echo)
}
suspend operator fun invoke(
message: JsonArray,
userId: String,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
val selfUin = TicketSvc.getUin()
val msgs = message.map {
if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段
it.asJsonObject["data"].asJsonObject.let { data ->
if (data.containsKey("content"))
ForwardMsgNode.MessageNode(
name = data["name"].asStringOrNull ?: "",
content = data["content"]
)
else ForwardMsgNode.MessageIdNode(data["id"].asInt)
}
}.map {
if (it is ForwardMsgNode.MessageIdNode) {
val recordResult = MsgSvc.getMsg(it.id)
if (recordResult.isFailure) {
ForwardMsgNode.EmptyNode
} else {
val record = recordResult.getOrThrow()
ForwardMsgNode.MessageNode(
name = record.sendMemberName
.ifBlank { record.sendNickName }
.ifBlank { record.sendRemarkName }
.ifBlank { record.peerName },
content = record.toSegments().map { segment ->
segment.toJson()
}.json
)
}
} else {
it as ForwardMsgNode.MessageNode
}
}.filter {
it.content != null
}
val multiNodes = msgs.map { node ->
suspendCoroutine {
GlobalScope.launch {
var msgId: Long = 0
msgId = MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, node.content!!.let { msg ->
if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString)
}, { code, why ->
if (code != 0) {
LogCenter.log("合并转发消息节点消息发送失败:$code($why)", Level.WARN)
}
it.resume(node.name to msgId)
}).first
}
}
}
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
val to = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, userId)
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
multiNodes.forEach { add(MultiMsgInfo(it.second, it.first)) }
}.also { it.reverse() }, from, to) { code, why ->
if (code != 0)
LogCenter.log("合并转发消息:$code($why)", Level.WARN)
}
return ok(data = EmptyJsonObject, echo = echo)
}.onFailure {
return error("error: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}
override val requiredParams: Array<String> = arrayOf("user_id")
override fun path(): String = "send_private_forward_msg"
}
/**
* 群聊合并转发
*/
internal object SendGroupForwardMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getString("group_id")
if (session.isArray("messages")) {
val messages = session.getArray("messages")
return invoke(messages, groupId, session.echo)
}
return logic("未知格式合并转发消息", session.echo)
}
suspend operator fun invoke(
message: JsonArray,
groupId: String,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
val selfUin = TicketSvc.getUin()
val msgs = message.map {
if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段
it.asJsonObject["data"].asJsonObject.let { data ->
if (data.containsKey("content"))
ForwardMsgNode.MessageNode(
name = data["name"].asStringOrNull ?: "",
content = data["content"]
)
else ForwardMsgNode.MessageIdNode(data["id"].asInt)
}
}.map {
if (it is ForwardMsgNode.MessageIdNode) {
val recordResult = MsgSvc.getMsg(it.id)
if (recordResult.isFailure) {
ForwardMsgNode.EmptyNode
} else {
val record = recordResult.getOrThrow()
ForwardMsgNode.MessageNode(
name = record.sendMemberName
.ifBlank { record.sendNickName }
.ifBlank { record.sendRemarkName }
.ifBlank { record.peerName },
content = record.toSegments().map { segment ->
segment.toJson()
}.json
)
}
} else {
it as ForwardMsgNode.MessageNode
}
}.filter {
it.content != null
}
val multiNodes = msgs.map { node ->
suspendCoroutine {
GlobalScope.launch {
var msgId: Long = 0
msgId = MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, node.content!!.let { msg ->
if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString)
}, { code, why ->
if (code != 0) {
LogCenter.log("合并转发消息节点消息发送失败:$code($why)", Level.WARN)
}
it.resume(node.name to msgId)
}).first
}
}
}
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
val to = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId)
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
multiNodes.forEach { add(MultiMsgInfo(it.second, it.first)) }
}.also { it.reverse() }, from, to) { code, why ->
if (code != 0)
LogCenter.log("合并转发消息:$code($why)", Level.WARN)
}
return ok(data = EmptyJsonObject, echo = echo)
}.onFailure {
return error("error: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}
override val requiredParams: Array<String> = arrayOf("group_id")
override fun path(): String = "send_group_forward_msg"
}

View File

@ -0,0 +1,21 @@
package moe.fuqiuluo.shamrock.remote.action.handlers;
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
internal object SendGroupForwardMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getString("group_id")
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId, messages, session.echo)
} else {
logic("未知格式合并转发消息", session.echo)
}
}
override val requiredParams: Array<String> = arrayOf("messages", "group_id")
override fun path(): String = "send_group_forward_msg"
}

View File

@ -3,6 +3,7 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.jsonArray
internal object SendGroupMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
@ -11,6 +12,9 @@ internal object SendGroupMessage: IActionHandler() {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, message, autoEscape, echo = session.echo)
} else if (session.isObject("message")) {
val message = session.getObject("message")
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, listOf( message ).jsonArray, session.echo)
} else {
val message = session.getArray("message")
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, message, session.echo)

View File

@ -0,0 +1,39 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object SendGroupNotice: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getLong("group_id")
val text = session.getString("content")
val image = session.getStringOrNull("image")
return invoke(groupId, text, image, session.echo)
}
suspend operator fun invoke(groupId: Long, text: String, image: String?, echo: JsonElement = EmptyJsonString): String {
val groupAnnouncementMessageImage = if (image != null) {
GroupSvc.uploadImageTroopNotice(image).onFailure {
LogCenter.log("上传群公告图片失败:${it.message}", Level.WARN)
}.getOrNull()
} else null
val announcements = GroupSvc.addQunNotice(groupId, text, groupAnnouncementMessageImage)
if (announcements.isSuccess) {
return ok(announcements.getOrNull(), echo)
}
return logic(announcements.exceptionOrNull()?.message ?: "", echo)
}
override val requiredParams: Array<String> = arrayOf("group_id", "content")
override val alias: Array<String> = arrayOf("send_group_notice")
override fun path(): String = "_send_group_notice"
}

View File

@ -14,6 +14,7 @@ import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.jsonArray
internal object SendMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
@ -47,9 +48,12 @@ internal object SendMessage: IActionHandler() {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
invoke(chatType, peerId, message, autoEscape, echo = session.echo, fromId = fromId)
} else {
} else if (session.isArray("message")) {
val message = session.getArray("message")
invoke(chatType, peerId, message, session.echo, fromId = fromId)
} else {
val message = session.getObject("message")
invoke(chatType, peerId, listOf( message ).jsonArray, session.echo, fromId = fromId)
}
} catch (e: ParamsException) {
return noParam(e.message!!, session.echo)
@ -71,7 +75,14 @@ internal object SendMessage: IActionHandler() {
// return logic("contact is not found", echo = echo)
//}
val result = if (autoEscape) {
MsgSvc.sendToAio(chatType, peerId, arrayListOf(message).json, fromId = fromId)
MsgSvc.sendToAio(chatType, peerId, listOf(
mapOf(
"type" to "text",
"data" to mapOf(
"text" to message
)
)
).json, fromId = fromId)
} else {
val msg = MessageHelper.decodeCQCode(message)
if (msg.isEmpty()) {
@ -81,12 +92,16 @@ internal object SendMessage: IActionHandler() {
MsgSvc.sendToAio(chatType, peerId, msg, fromId = fromId)
}
}
if (result.first <= 0) {
if (result.isFailure) {
return logic(result.exceptionOrNull()?.message ?: "", echo)
}
val pair = result.getOrNull() ?: Pair(0L, 0)
if (pair.first <= 0) {
return logic("send message failed", echo = echo)
}
return ok(MessageResult(
msgId = result.second,
time = (result.first * 0.001).toLong()
msgId = pair.second,
time = (pair.first * 0.001).toLong()
), echo = echo)
}
@ -98,12 +113,16 @@ internal object SendMessage: IActionHandler() {
// return logic("contact is not found", echo = echo)
//}
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId)
if (result.first <= 0) {
if (result.isFailure) {
return logic(result.exceptionOrNull()?.message ?: "", echo)
}
val pair = result.getOrNull() ?: Pair(0L, 0)
if (pair.first <= 0) {
return logic("send message failed", echo = echo)
}
return ok(MessageResult(
msgId = result.second,
time = (result.first * 0.001).toLong()
msgId = pair.second,
time = (pair.first * 0.001).toLong()
), echo)
}

View File

@ -0,0 +1,21 @@
package moe.fuqiuluo.shamrock.remote.action.handlers;
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
internal object SendPrivateForwardMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val userId = session.getString("user_id")
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId, messages, session.echo)
} else {
logic("未知格式合并转发消息", session.echo)
}
}
override val requiredParams: Array<String> = arrayOf("messages", "user_id")
override fun path(): String = "send_private_forward_msg"
}

View File

@ -3,32 +3,42 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.jsonArray
internal object SendPrivateMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val userId = session.getString("user_id")
val groupId = session.getStringOrNull("group_id")
val chatTYpe = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
SendMessage.invoke(
chatType = chatTYpe,
chatType = chatType,
peerId = userId,
message = message,
autoEscape = autoEscape,
echo = session.echo,
fromId = groupId ?: userId
)
} else {
} else if (session.isArray("message")) {
val message = session.getArray("message")
SendMessage(
chatType = chatTYpe,
chatType = chatType,
peerId = userId,
message = message,
echo = session.echo,
fromId = groupId ?: userId
)
} else {
val message = session.getObject("message")
SendMessage(
chatType = chatType,
peerId = userId,
message = listOf( message ).jsonArray,
echo = session.echo,
fromId = groupId ?: userId
)
}
}

View File

@ -10,18 +10,27 @@ import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object SetFriendAddRequest: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val flag = session.getString("flag")
val approve = session.getBoolean("approve")
val approve = session.getBooleanOrDefault("approve", true)
val remark = session.getStringOrNull("remark")
val notSeen = session.getBoolean("notSeen")
val notSeen = session.getBooleanOrDefault("notSeen", false)
return invoke(flag, approve, remark, notSeen, session.echo)
}
operator fun invoke(flag: String, approve: Boolean? = true, remark: String? = "", notSeen: Boolean? = false, echo: JsonElement = EmptyJsonString): String {
suspend operator fun invoke(flag: String, approve: Boolean? = true, remark: String? = "", notSeen: Boolean? = false, echo: JsonElement = EmptyJsonString): String {
val flags = flag.split(";")
val ts = flags[0].toLong()
var ts = flags[0].toLong()
// val src = flags[1].toInt()
// val subSrc = flags[2].toInt()
val applier = flags[3].toLong()
if (ts.toString().length < 13) {
// time but not seq, query seq again
val reqs = FriendSvc.requestFriendSystemMsgNew(20, 0, 0, 1)
val req = reqs?.first {
it.msg_time.get() == ts
}
// 好友请求seq貌似就是time*1000查不到直接*1000
ts = req?.msg_seq?.get() ?: (ts * 1000)
}
return try {
FriendSvc.requestFriendRequest(ts, applier, remark ?: "", approve, notSeen)
ok("成功", echo)
@ -33,4 +42,6 @@ internal object SetFriendAddRequest: IActionHandler() {
}
override fun path(): String = "set_friend_add_request"
override val requiredParams: Array<String> = arrayOf("flag")
}

View File

@ -2,6 +2,8 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
@ -9,16 +11,31 @@ import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object SetGroupAddRequest: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val flag = session.getString("flag")
val approve = session.getBoolean("approve")
val approve = session.getBooleanOrDefault("approve", true)
val remark = session.getStringOrNull("reason")
val notSeen = session.getBoolean("notSeen")
val notSeen = session.getBooleanOrDefault("not_seen", false)
val subType = session.getString("sub_type")
return invoke(flag, approve, subType, remark, notSeen, session.echo)
}
suspend operator fun invoke(flag: String, approve: Boolean? = true, subType: String, remark: String? = "", notSeen: Boolean? = false, echo: JsonElement = EmptyJsonString): String {
val flags = flag.split(";")
val ts = flags[0].toLong()
var ts = flags[0].toLong()
try {
if (ts.toString().length < 13) {
// time but not seq, query seq again
var reqs = GroupSvc.requestGroupSystemMsgNew(20, 1)
val riskReqs = GroupSvc.requestGroupSystemMsgNew(20, 2)
reqs = reqs + riskReqs
val req = reqs.first {
it.msg_time.get() == ts
}
ts = req.msg_seq?.get() ?: return error("失败:未找到该请求", echo)
}
} catch (err: Throwable) {
LogCenter.log(err.stackTraceToString(), Level.WARN)
return error("查找请求失败:${err.message}", echo)
}
val groupCode = flags[1].toLong()
val uin = flags[2].toLong()
return try {
@ -26,14 +43,15 @@ internal object SetGroupAddRequest: IActionHandler() {
if (result.isSuccess) {
ok(result.getOrNull(), echo)
} else {
logic(result.getOrNull() ?: "", echo)
logic(result.exceptionOrNull()?.message ?: "", echo)
}
} catch (err: Throwable) {
err.printStackTrace()
error("失败:${err.message}", echo)
}
}
override fun path(): String = "set_group_add_request"
override val requiredParams: Array<String> = arrayOf("flag", "sub_type")
}

View File

@ -5,12 +5,15 @@ import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.FileElement
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.msg.api.IMsgService
import com.tencent.qqnt.msg.api.IMsgUtilApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@ -19,11 +22,13 @@ import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.TransfileHelper
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.api.RichMediaUploadHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
import java.io.File
import java.io.FileOutputStream
import kotlin.coroutines.resume
internal object UploadGroupFile : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
@ -94,16 +99,36 @@ internal object UploadGroupFile : IActionHandler() {
msgElement.elementType = MsgConstant.KELEMTYPEFILE
msgElement.fileElement = fileElement
// 根据文件大小调整超时时间
val msgIdPair = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEGROUP)
val msgService = QRoute.api(IMsgService::class.java)
msgService.sendMsgWithMsgId(
MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId), msgIdPair.second, arrayListOf(msgElement)
) { code, reason ->
LogCenter.log("群文件消息发送异常(code = $code, reason = $reason)")
}
val info = (withTimeoutOrNull((srcFile.length() / (300 * 1024)) * 1000 + 5000) {
val msgService = QRoute.api(IMsgService::class.java)
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId)
suspendCancellableCoroutine<FileTransNotifyInfo?> {
msgService.sendMsgWithMsgId(
contact, msgIdPair.second, arrayListOf(msgElement)
) { code, reason ->
LogCenter.log("群文件消息发送异常(code = $code, reason = $reason)")
it.resume(null)
}
RichMediaUploadHandler.registerListener(msgIdPair.second) {
it.resume(this)
return@registerListener true
}
}
} ?: return error("上传文件失败", echo)).also {
if (it.commonFileInfo == null) {
return error(it.fileErrMsg ?: "上传文件失败", echo)
}
}.commonFileInfo
return ok(data = FileUploadResult(
msgHash = msgIdPair.first
msgHash = msgIdPair.first,
bizid = info.bizType ?: 0,
md5 = info.md5,
sha = info.sha,
sha3 = info.sha3,
fileId = info.uuid
), echo = echo)
}
@ -114,5 +139,10 @@ internal object UploadGroupFile : IActionHandler() {
@Serializable
data class FileUploadResult(
@SerialName("msg_id") val msgHash: Int,
@SerialName("bizid") val bizid: Int,
@SerialName("md5") val md5: String,
@SerialName("sha") val sha: String,
@SerialName("sha3") val sha3: String,
@SerialName("file_id") val fileId: String
)
}

View File

@ -5,12 +5,15 @@ import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.FileElement
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.msg.api.IMsgService
import com.tencent.qqnt.msg.api.IMsgUtilApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@ -20,11 +23,13 @@ import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.TransfileHelper
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.api.RichMediaUploadHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
import java.io.File
import java.io.FileOutputStream
import kotlin.coroutines.resume
internal object UploadPrivateFile : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
@ -95,26 +100,40 @@ internal object UploadPrivateFile : IActionHandler() {
msgElement.elementType = MsgConstant.KELEMTYPEFILE
msgElement.fileElement = fileElement
// 根据文件大小调整超时时间
val msgIdPair = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEC2C)
val msgService = QRoute.api(IMsgService::class.java)
msgService.sendMsgWithMsgId(
MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, userId), msgIdPair.second, arrayListOf(msgElement)
) { code, reason ->
if (code != 0)
LogCenter.log("私聊文件消息发送异常(code = $code, reason = $reason)")
}
val info = (withTimeoutOrNull((srcFile.length() / (300 * 1024)) * 1000 + 5000) {
val msgService = QRoute.api(IMsgService::class.java)
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, userId)
suspendCancellableCoroutine<FileTransNotifyInfo?> {
msgService.sendMsgWithMsgId(
contact, msgIdPair.second, arrayListOf(msgElement)
) { code, reason ->
LogCenter.log("私聊文件消息发送异常(code = $code, reason = $reason)")
it.resume(null)
}
RichMediaUploadHandler.registerListener(msgIdPair.second) {
it.resume(this)
return@registerListener true
}
}
} ?: return error("上传文件失败", echo)).also {
if (it.commonFileInfo == null) {
return error(it.fileErrMsg ?: "上传文件失败", echo)
}
}.commonFileInfo
return ok(data = FileUploadResult(
msgHash = msgIdPair.first
return ok(data = UploadGroupFile.FileUploadResult(
msgHash = msgIdPair.first,
bizid = info.bizType ?: 0,
md5 = info.md5,
sha = info.sha,
sha3 = info.sha3,
fileId = info.uuid
), echo = echo)
}
override val requiredParams: Array<String> = arrayOf("user_id", "file", "name")
override fun path(): String = "upload_private_file"
@Serializable
data class FileUploadResult(
@SerialName("msg_id") val msgHash: Int,
)
}

View File

@ -8,6 +8,7 @@ import io.ktor.server.routing.Routing
import moe.fuqiuluo.shamrock.remote.action.ActionManager
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.handlers.*
import moe.fuqiuluo.shamrock.tools.fetch
import moe.fuqiuluo.shamrock.tools.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.getOrPost
@ -116,8 +117,27 @@ fun Routing.troopAction() {
call.respondText(DeleteEssenceMessage(messageId), ContentType.Application.Json)
}
getOrPost("/get_essence_msg_list") {
val groupId = fetchOrThrow("group_id").toLong()
val page = fetchOrNull("page")?.toIntOrNull() ?: 0
val pageSize = fetchOrNull("page_size")?.toIntOrNull() ?: 20
call.respondText(GetEssenceMessageList(groupId, page, pageSize), ContentType.Application.Json)
}
getOrPost("/get_group_system_msg") {
call.respondText(GetGroupSystemMsg(), ContentType.Application.Json)
}
getOrPost("/_get_group_notice") {
val groupId = fetchOrThrow("group_id").toLong()
call.respondText(GetGroupNotice(groupId), ContentType.Application.Json)
}
getOrPost("/_send_group_notice") {
val groupId = fetchOrThrow("group_id").toLong()
val text = fetchOrThrow("content")
val image = fetchOrNull("image")
call.respondText(SendGroupNotice(groupId, text, image), ContentType.Application.Json)
}
}

View File

@ -17,12 +17,15 @@ import moe.fuqiuluo.shamrock.tools.fetchGetOrThrow
import moe.fuqiuluo.shamrock.tools.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject
import moe.fuqiuluo.shamrock.tools.fetchPostJsonString
import moe.fuqiuluo.shamrock.tools.fetchPostOrNull
import moe.fuqiuluo.shamrock.tools.fetchPostOrThrow
import moe.fuqiuluo.shamrock.tools.getOrPost
import moe.fuqiuluo.shamrock.tools.isJsonData
import moe.fuqiuluo.shamrock.tools.isJsonObject
import moe.fuqiuluo.shamrock.tools.isJsonString
import moe.fuqiuluo.shamrock.tools.jsonArray
import moe.fuqiuluo.shamrock.tools.respond
fun Routing.messageAction() {
@ -30,20 +33,22 @@ fun Routing.messageAction() {
post {
val groupId = fetchPostOrNull("group_id")
val messages = fetchPostJsonArray("messages")
call.respondText(SendGroupForwardMsg(messages, groupId ?: ""), ContentType.Application.Json)
call.respondText(SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId ?: "", messages), ContentType.Application.Json)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
post("/send_group_forward_msg") {
}
post("/send_private_forward_msg") {
val userId = fetchPostOrNull("user_id")
val messages = fetchPostJsonArray("messages")
call.respondText(SendPrivateForwardMsg(messages, userId ?: ""), ContentType.Application.Json)
route("/send_private_forward_msg") {
post {
val userId = fetchPostOrNull("user_id")
val messages = fetchPostJsonArray("messages")
call.respondText(SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: "", messages), ContentType.Application.Json)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
getOrPost("/get_forward_msg") {
@ -116,20 +121,30 @@ fun Routing.messageAction() {
val userId = fetchPostOrNull("user_id")
val groupId = fetchPostOrNull("group_id")
val peerId = if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!
call.respondText(if (isJsonData() && !isJsonString("message")) {
SendMessage(
chatType = chatType,
peerId = if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!,
message = fetchPostJsonArray("message"),
fromId = groupId ?: userId ?: ""
)
if (isJsonObject("message")) {
SendMessage(
chatType = chatType,
peerId = peerId,
message = listOf(fetchPostJsonObject("message")).jsonArray,
fromId = groupId ?: userId ?: ""
)
} else {
SendMessage(
chatType = chatType,
peerId = peerId,
message = fetchPostJsonArray("message"),
fromId = groupId ?: userId ?: ""
)
}
} else {
val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false
//SendMessage(chatType, peerId, fetchPostOrThrow("message"), autoEscape)
SendMessage(
chatType = chatType,
peerId = if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!,
peerId = peerId,
message = fetchPostOrThrow("message"),
autoEscape = autoEscape,
fromId = groupId ?: userId ?: ""
@ -154,7 +169,19 @@ fun Routing.messageAction() {
if (isJsonString("message")) {
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, fetchPostJsonString("message"), autoEscape)
} else {
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, fetchPostJsonArray("message"))
if (isJsonObject("message")) {
SendMessage(
chatType = MsgConstant.KCHATTYPEGROUP,
peerId = groupId,
message = listOf(fetchPostJsonObject("message")).jsonArray
)
} else {
SendMessage(
chatType = MsgConstant.KCHATTYPEGROUP,
peerId = groupId,
message = fetchPostJsonArray("message")
)
}
}
} else {
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, fetchPostOrThrow("message"), autoEscape)
@ -183,27 +210,43 @@ fun Routing.messageAction() {
val groupId = fetchPostOrNull("group_id")
val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false
val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
val fromId = groupId ?: userId
val result = if (isJsonData()) {
if (isJsonString("message")) {
SendMessage(
chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
userId,
fetchPostJsonString("message"),
autoEscape
chatType = chatType,
peerId = userId,
message = fetchPostJsonString("message"),
autoEscape = autoEscape,
fromId = fromId
)
} else {
SendMessage(
chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
userId,
fetchPostJsonArray("message")
)
if (isJsonObject("message")) {
SendMessage(
chatType = chatType,
peerId = userId,
message = listOf(fetchPostJsonObject("message")).jsonArray,
fromId = fromId
)
} else {
SendMessage(
chatType = chatType,
peerId = userId,
message = fetchPostJsonArray("message"),
fromId = fromId
)
}
}
} else {
SendMessage(
chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
userId,
fetchPostOrThrow("message"),
autoEscape
chatType = chatType,
peerId = userId,
message = fetchPostOrThrow("message"),
autoEscape = autoEscape,
fromId = fromId
)
}

View File

@ -4,6 +4,7 @@ import io.ktor.http.ContentType
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.http.content.streamProvider
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.request.receiveMultipart
import io.ktor.server.response.respondText
@ -27,6 +28,7 @@ import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray
import moe.fuqiuluo.shamrock.tools.getOrPost
import moe.fuqiuluo.shamrock.tools.isJsonArray
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.tools.respond
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
@ -39,7 +41,7 @@ fun Routing.otherAction() {
post("/shell") {
val runtime = Runtime.getRuntime()
val dir = fetchOrThrow("dir")
val out = StringBuilder()
val out = hashMapOf<String, Any>()
withTimeoutOrNull(5000L) {
if (isJsonArray("cmd")) {
val cmd = fetchPostJsonArray("cmd").map {
@ -59,17 +61,15 @@ fun Routing.otherAction() {
respond(false, Status.IAmTired, "执行超时")
} else {
it.inputStream.use {
out.append("stdout:\n")
out.append(it.readBytes().toString(Charsets.UTF_8))
out["out"] = it.readBytes().toString(Charsets.UTF_8)
}
it.errorStream.use {
out.append("\nstderr:\n")
out.append(it.readBytes().toString(Charsets.UTF_8))
out["err"] = it.readBytes().toString(Charsets.UTF_8)
}
}
}
call.respondText(out.toString())
call.respondText(out.json.toString(), ContentType.Application.Json)
}
}

View File

@ -54,6 +54,12 @@ internal object HttpService: HttpTransmitServlet() {
GlobalEventTransmitter.onNoticeEvent { event ->
pushTo(event)
}
})
submitFlowJob(GlobalScope.launch {
GlobalEventTransmitter.onRequestEvent {
pushTo(it)
}
})
LogCenter.log("HttpService: 初始化服务", Level.WARN)
}

View File

@ -7,15 +7,15 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import moe.fuqiuluo.shamrock.remote.service.api.WebSocketClientServlet
import moe.fuqiuluo.shamrock.remote.service.data.push.*
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
internal class WebSocketClientService(
override val address: String,
heartbeatInterval: Long,
wsHeaders: Map<String, String>
) : WebSocketClientServlet(address, wsHeaders) {
) : WebSocketClientServlet(address, heartbeatInterval, wsHeaders) {
private val eventJobList = mutableSetOf<Job>()
override fun submitFlowJob(job: Job) {
@ -33,6 +33,11 @@ internal class WebSocketClientService(
pushTo(event)
}
})
submitFlowJob(GlobalScope.launch {
GlobalEventTransmitter.onRequestEvent { event ->
pushTo(event)
}
})
LogCenter.log("WebSocketClientService: 初始化服务", Level.WARN)
}

View File

@ -21,7 +21,11 @@ import org.java_websocket.WebSocket
import org.java_websocket.handshake.ClientHandshake
import java.net.URI
internal class WebSocketService(host: String, port: Int): WebSocketTransmitServlet(host, port) {
internal class WebSocketService(
host: String,
port: Int,
heartbeatInterval: Long,
): WebSocketTransmitServlet(host, port, heartbeatInterval) {
private val eventJobList = mutableSetOf<Job>()
override fun submitFlowJob(job: Job) {
@ -39,6 +43,11 @@ internal class WebSocketService(host: String, port: Int): WebSocketTransmitServl
pushTo(event)
}
})
submitFlowJob(GlobalScope.launch {
GlobalEventTransmitter.onRequestEvent { event ->
pushTo(event)
}
})
LogCenter.log("WebSocketService: 初始化服务", Level.WARN)
}
@ -78,7 +87,6 @@ internal class WebSocketService(host: String, port: Int): WebSocketTransmitServl
private fun pushMetaLifecycle() {
GlobalScope.launch {
val runtime = AppRuntimeFetcher.appRuntime
val curUin = runtime.currentAccountUin
pushTo(PushMetaEvent(
time = System.currentTimeMillis() / 1000,
selfId = app.longAccountUin,
@ -86,9 +94,9 @@ internal class WebSocketService(host: String, port: Int): WebSocketTransmitServl
type = MetaEventType.LifeCycle,
subType = MetaSubType.Connect,
status = BotStatus(
Self("qq", curUin.toLong()), runtime.isLogin, status = "正常", good = true
Self("qq", runtime.longAccountUin), runtime.isLogin, status = "正常", good = true
),
interval = 15000
interval = heartbeatInterval
))
}
}

View File

@ -19,7 +19,11 @@ import moe.fuqiuluo.shamrock.remote.service.data.push.MessageTempSource
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeEvent
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeSubType
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeType
import moe.fuqiuluo.shamrock.remote.service.data.push.PokeDetail
import moe.fuqiuluo.shamrock.remote.service.data.push.PrivateFileMsg
import moe.fuqiuluo.shamrock.remote.service.data.push.RequestEvent
import moe.fuqiuluo.shamrock.remote.service.data.push.RequestSubType
import moe.fuqiuluo.shamrock.remote.service.data.push.RequestType
import moe.fuqiuluo.shamrock.remote.service.data.push.Sender
import moe.fuqiuluo.shamrock.tools.ShamrockDsl
import moe.fuqiuluo.shamrock.tools.json
@ -32,9 +36,14 @@ internal object GlobalEventTransmitter: BaseSvc() {
private val noticeEventFlow by lazy {
MutableSharedFlow<NoticeEvent>()
}
private val requestEventFlow by lazy {
MutableSharedFlow<RequestEvent>()
}
private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent)
private suspend fun pushRequest(requestEvent: RequestEvent) = requestEventFlow.emit(requestEvent)
private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message)
/**
@ -72,10 +81,10 @@ internal object GlobalEventTransmitter: BaseSvc() {
sender = Sender(
userId = record.senderUin,
nickname = record.sendNickName
.ifBlank { record.sendMemberName }
.ifBlank { record.sendRemarkName }
.ifBlank { record.sendMemberName }
.ifBlank { record.peerName },
card = record.sendMemberName,
card = record.sendMemberName.ifBlank { record.sendNickName },
role = when (record.senderUin) {
GroupSvc.getOwner(record.peerUin.toString()) -> MemberRole.Owner
in GroupSvc.getAdminList(record.peerUin.toString()) -> MemberRole.Admin
@ -213,7 +222,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
* 群聊通知 通知器
*/
object GroupNoticeTransmitter {
suspend fun transGroupPoke(time: Long, operation: Long, target: Long, groupCode: Long): Boolean {
suspend fun transGroupPoke(time: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?, groupCode: Long): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
@ -223,7 +232,12 @@ internal object GlobalEventTransmitter: BaseSvc() {
operatorId = operation,
userId = operation,
groupId = groupCode,
target = target
target = target,
pokeDetail = PokeDetail(
action = action,
suffix = suffix,
actionImg = actionImg
)
))
return true
}
@ -284,7 +298,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
type = NoticeType.GroupBan,
subType = if (duration == 0) NoticeSubType.LiftBan else NoticeSubType.Ban,
operatorId = operation,
userId = operation,
userId = target,
senderId = operation,
target = target,
groupId = groupCode,
@ -315,35 +329,73 @@ internal object GlobalEventTransmitter: BaseSvc() {
return true
}
suspend fun transGroupApply(
suspend fun transCardChange(
time: Long,
operator: Long,
reason: String,
groupCode: Long,
flag: String,
targetId: Long,
oldCard: String,
newCard: String,
groupId: Long
): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.GroupCard,
userId = targetId,
cardNew = newCard,
cardOld = oldCard,
groupId = groupId
))
return true
}
suspend fun transTitleChange(
time: Long,
targetId: Long,
title: String,
groupId: Long
): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.Notify,
userId = targetId,
groupId = groupId,
title = title,
subType = NoticeSubType.Title
))
return true
}
suspend fun transEssenceChange(
time: Long,
senderUin: Long,
operatorUin: Long,
msgId: Int,
groupId: Long,
subType: NoticeSubType
): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.GroupApply,
operatorId = operator,
tip = reason,
groupId = groupCode,
subType = subType,
flag = flag
type = NoticeType.Essence,
senderId = senderUin,
groupId = groupId,
operatorId = operatorUin,
msgId = msgId,
subType = subType
))
return true
}
}
/**
* 私聊通知 通知器
*/
object PrivateNoticeTransmitter {
suspend fun transPrivatePoke(msgTime: Long, operation: Long, target: Long): Boolean {
suspend fun transPrivatePoke(msgTime: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?): Boolean {
pushNotice(NoticeEvent(
time = msgTime,
selfId = app.longAccountUin,
@ -353,7 +405,12 @@ internal object GlobalEventTransmitter: BaseSvc() {
operatorId = operation,
userId = operation,
senderId = operation,
target = target
target = target,
pokeDetail = PokeDetail(
actionImg = actionImg,
action = action,
suffix = suffix
)
))
return true
}
@ -372,15 +429,42 @@ internal object GlobalEventTransmitter: BaseSvc() {
return true
}
suspend fun transFriendApply(time: Long, operation: Long, tipText: String, flag: String): Boolean {
pushNotice(NoticeEvent(
}
/**
* 请求 通知器
*/
object RequestTransmitter {
suspend fun transFriendApp(time: Long, operation: Long, tipText: String, flag: String): Boolean {
pushRequest(RequestEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.FriendApply,
operatorId = operation,
postType = PostType.Request,
type = RequestType.Friend,
userId = operation,
tip = tipText,
comment = tipText,
flag = flag
))
return true
}
suspend fun transGroupApply(
time: Long,
operator: Long,
reason: String,
groupCode: Long,
flag: String,
subType: RequestSubType
): Boolean {
pushRequest(RequestEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Request,
type = RequestType.Group,
userId = operator,
comment = reason,
groupId = groupCode,
subType = subType,
flag = flag
))
return true
@ -397,6 +481,12 @@ internal object GlobalEventTransmitter: BaseSvc() {
noticeEventFlow
.collect(collector)
}
@ShamrockDsl
suspend inline fun onRequestEvent(collector: FlowCollector<RequestEvent>) {
requestEventFlow
.collect(collector)
}
}

View File

@ -0,0 +1,27 @@
package moe.fuqiuluo.shamrock.remote.service.api
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
internal object RichMediaUploadHandler {
private val listeners by lazy {
mutableMapOf<Long, FileTransNotifyInfo.() -> Boolean>()
}
fun registerListener(key: Long, value: FileTransNotifyInfo.() -> Boolean) {
listeners[key] = value
}
fun removeListener(key: Long) {
listeners.remove(key)
}
fun notify(info: FileTransNotifyInfo): Boolean {
listeners[info.msgId]?.let {
if (it(info)) {
listeners.remove(info.msgId)
return true
}
}
return false
}
}

View File

@ -36,6 +36,7 @@ import kotlin.concurrent.timer
internal abstract class WebSocketClientServlet(
url: String,
private val heartbeatInterval: Long,
wsHeaders: Map<String, String>
) : BaseTransmitServlet, WebSocketClient(URI(url), wsHeaders) {
private val sendLock = Mutex()
@ -105,17 +106,17 @@ internal abstract class WebSocketClientServlet(
}
private fun startHeartbeatTimer() {
if (heartbeatInterval <= 0) return
timer(
name = "heartbeat",
initialDelay = 0,
period = 1000L * 15,
period = heartbeatInterval,
) {
if (isClosed || isClosing || !isOpen) {
cancel()
return@timer
}
val runtime = AppRuntimeFetcher.appRuntime
val curUin = runtime.currentAccountUin
send(
GlobalJson.encodeToString(
PushMetaEvent(
@ -125,7 +126,7 @@ internal abstract class WebSocketClientServlet(
type = MetaEventType.Heartbeat,
subType = MetaSubType.Connect,
status = BotStatus(
Self("qq", curUin.toLong()),
Self("qq", runtime.longAccountUin),
runtime.isLogin,
status = "正常",
good = true
@ -151,7 +152,7 @@ internal abstract class WebSocketClientServlet(
status = BotStatus(
Self("qq", curUin.toLong()), runtime.isLogin, status = "正常", good = true
),
interval = 15000
interval = heartbeatInterval
)
)
}

View File

@ -35,7 +35,8 @@ import kotlin.concurrent.timer
internal abstract class WebSocketTransmitServlet(
host:String,
port: Int
port: Int,
protected val heartbeatInterval: Long,
) : BaseTransmitServlet, WebSocketServer(InetSocketAddress(host, port)) {
private val sendLock = Mutex()
protected val eventReceivers: MutableList<WebSocket> = Collections.synchronizedList(mutableListOf<WebSocket>())
@ -56,20 +57,27 @@ internal abstract class WebSocketTransmitServlet(
}
init {
timer("heartbeat", true, 0, 1000L * 5) {
val runtime = AppRuntimeFetcher.appRuntime
val curUin = runtime.currentAccountUin
broadcastAnyEvent(PushMetaEvent(
time = System.currentTimeMillis() / 1000,
selfId = app.longAccountUin,
postType = PostType.Meta,
type = MetaEventType.Heartbeat,
subType = MetaSubType.Connect,
status = BotStatus(
Self("qq", curUin.toLong()), runtime.isLogin, status = "正常", good = true
),
interval = 15000
))
if (heartbeatInterval > 0) {
timer("heartbeat", true, 0, heartbeatInterval) {
val runtime = AppRuntimeFetcher.appRuntime
val curUin = runtime.currentAccountUin
broadcastAnyEvent(
PushMetaEvent(
time = System.currentTimeMillis() / 1000,
selfId = app.longAccountUin,
postType = PostType.Meta,
type = MetaEventType.Heartbeat,
subType = MetaSubType.Connect,
status = BotStatus(
Self("qq", curUin.toLong()),
runtime.isLogin,
status = "正常",
good = true
),
interval = heartbeatInterval
)
)
}
}
}
@ -112,7 +120,7 @@ internal abstract class WebSocketTransmitServlet(
}
override fun onStart() {
LogCenter.log("WSServer start running on ws://0.0.0.0:$port!")
LogCenter.log("WSServer start running on ws://${getAddress()}!")
initTransmitter()
}

View File

@ -18,6 +18,7 @@ data class ConnectionConfig(
@SerialName("address") val address: String? = null,
@SerialName("port") var port: Int? = null,
@SerialName("token") val token: String? = null,
@SerialName("heartbeat_interval") var heartbeatInterval: Long? = null,
)
@Serializable

View File

@ -1,8 +1,11 @@
package moe.fuqiuluo.shamrock.remote.service.config
import android.content.Intent
import com.tencent.mmkv.MMKV
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.GlobalJson5
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import mqq.app.MobileQQ
@ -14,10 +17,14 @@ internal object ShamrockConfig {
if (it.exists()) it.delete()
it.mkdirs()
}
private val Config: ServiceConfig by lazy {
GlobalJson5.decodeFromString(ConfigDir.resolve("config.json").also {
private val Config = kotlin.runCatching {
GlobalJson5.decodeFromString<ServiceConfig>(ConfigDir.resolve("config.json").also {
if (!it.exists()) it.writeText("{}")
}.readText())
}.onFailure {
LogCenter.log("您的配置文件出现错误: ${it.stackTraceToString()}", Level.ERROR)
}.getOrElse {
ServiceConfig()
}
fun isInit(): Boolean {
@ -33,16 +40,17 @@ internal object ShamrockConfig {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
mmkv.apply {
putBoolean( "tablet", intent.getBooleanExtra("tablet", false)) // 强制平板模式
putInt( "port", intent.getIntExtra("port", 5700)) // 主动HTTP端口
putInt( "port", intent.getIntExtra("port", 5700)) // 主动HTTP端口
putBoolean( "ws", intent.getBooleanExtra("ws", false)) // 主动WS开关
putBoolean( "http", intent.getBooleanExtra("http", false)) // HTTP回调开关
putString( "http_addr", intent.getStringExtra("http_addr")) // WebHook回调地址
putString( "http_addr", intent.getStringExtra("http_addr")) // WebHook回调地址
putBoolean( "ws_client", intent.getBooleanExtra("ws_client", false)) // 被动WS开关
putBoolean( "use_cqcode", intent.getBooleanExtra("use_cqcode", false)) // 使用CQ码
putBoolean( "inject_packet", intent.getBooleanExtra("inject_packet", false)) // 拦截无用包
putBoolean( "debug", intent.getBooleanExtra("debug", false)) // 调试模式
putBoolean( "debug", intent.getBooleanExtra("debug", false)) // 调试模式
Config.defaultToken = intent.getStringExtra("token")
Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
val wsPort = intent.getIntExtra("ws_port", 5800)
Config.activeWebSocket = if (Config.activeWebSocket == null) ConnectionConfig(
@ -58,22 +66,29 @@ internal object ShamrockConfig {
ConnectionConfig(address = it)
}?.toMutableList()
putString( "key_store", intent.getStringExtra("key_store")) // 证书路径
putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码
putString( "ssl_private_pwd", intent.getStringExtra("ssl_private_pwd")) // 证书私钥密码
putString( "ssl_alias", intent.getStringExtra("ssl_alias")) // 证书别名
putInt( "ssl_port", intent.getIntExtra("ssl_port", 5701)) // 主动HTTP端口
putString( "key_store", intent.getStringExtra("key_store")) // 证书路径
putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码
putString( "ssl_private_pwd", intent.getStringExtra("ssl_private_pwd")) // 证书私钥密码
putString( "ssl_alias", intent.getStringExtra("ssl_alias")) // 证书别名
putInt( "ssl_port", intent.getIntExtra("ssl_port", 5701)) // 主动HTTP端口
putBoolean("auto_clear", intent.getBooleanExtra("auto_clear", false)) // 自动清理
putBoolean("alive_reply", intent.getBooleanExtra("alive_reply", false)) // 自回复测试
putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息
putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息
putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
putBoolean("isInit", true)
}
updateConfig()
}
private val mmkv: MMKV
get() = MMKVFetcher.mmkvWithId("shamrock_config")
fun aliveReply(): Boolean {
return mmkv.getBoolean("alive_reply", false)
}
fun allowTempSession(): Boolean {
return Config.allowTempSession
}
@ -87,12 +102,10 @@ internal object ShamrockConfig {
}
fun enableSelfMsg(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("enable_self_msg", false)
}
fun openWebSocketClient(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("ws_client", false)
}
@ -101,7 +114,6 @@ internal object ShamrockConfig {
}
fun openWebSocket(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("ws", false)
}
@ -114,37 +126,30 @@ internal object ShamrockConfig {
}
fun useCQ(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("use_cqcode", false)
}
fun allowWebHook(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("http", false)
}
fun getWebHookAddress(): String {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getString("http_addr", "") ?: ""
}
fun forceTablet(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("tablet", true)
}
fun getPort(): Int {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getInt("port", 5700)
}
fun isInjectPacket(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("inject_packet", false)
}
fun isDebug(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("debug", false)
}
@ -153,7 +158,6 @@ internal object ShamrockConfig {
}
fun getKeyStorePath(): File? {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
mmkv.getString("key_store", null)?.let {
return File(it)
}
@ -161,52 +165,42 @@ internal object ShamrockConfig {
}
fun sslPwd(): CharArray? {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getString("ssl_pwd", null)?.toCharArray()
}
fun sslPrivatePwd(): String? {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getString("ssl_private_pwd", null)
}
fun sslAlias(): String? {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getString("ssl_alias", null)
}
fun getSslPort(): Int {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getInt("ssl_port", getPort())
}
fun isDev(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("dev", false)
}
operator fun set(key: String, value: String) {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
mmkv.putString(key, value)
}
operator fun set(key: String, value: Boolean) {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
mmkv.putBoolean(key, value)
}
operator fun set(key: String, value: Int) {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
mmkv.putInt(key, value)
}
operator fun set(key: String, value: Long) {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
mmkv.putLong(key, value)
}
operator fun set(key: String, value: Float) {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
mmkv.putFloat(key, value)
}
@ -215,7 +209,6 @@ internal object ShamrockConfig {
}
fun allowShell(): Boolean {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
return mmkv.getBoolean("shell", false)
}
}

View File

@ -0,0 +1,25 @@
package moe.fuqiuluo.shamrock.remote.service.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class GroupAnnouncement (
@SerialName("sender_id") val senderId: Long = 0,
@SerialName("publish_time") val publishTime: Long,
@SerialName("message") val message: GroupAnnouncementMessage,
)
@Serializable
internal data class GroupAnnouncementMessage (
@SerialName("text") val text: String,
@SerialName("images") val images: List<GroupAnnouncementMessageImage>,
)
@Serializable
internal data class GroupAnnouncementMessageImage (
@SerialName("height") val height: String,
@SerialName("width") val width: String,
@SerialName("id") val id: String,
)

View File

@ -9,6 +9,11 @@ internal data class MessageResult(
@SerialName("message_id") val msgId: Int,
@SerialName("time") val time: Long
)
@Serializable
internal data class ForwardMessageResult(
@SerialName("message_id") val msgId: Int,
@SerialName("forward_id") val forwardId: String
)
@Serializable
internal data class MessageDetail(
@ -30,4 +35,17 @@ internal data class MessageSender(
@SerialName("sex") val sex: String,
@SerialName("age") val age: Int,
@SerialName("uid") val uid: String,
)
@Serializable
internal data class EssenceMessage(
@SerialName("sender_id") val senderId: Long,
@SerialName("sender_nick") val senderNick: String,
@SerialName("sender_time") val senderTime: Long,
@SerialName("operator_id") val operatorId: Long,
@SerialName("operator_nick") val operatorNick: String,
@SerialName("operator_time") val operatorTime: Long,
@SerialName("message_id") var messageId: Int,
@SerialName("message_seq") val messageSeq: Int,
@SerialName("message_content") val messageContent: JsonElement,
)

View File

@ -33,6 +33,7 @@ internal enum class MsgType {
internal enum class PostType {
@SerialName("meta_event") Meta,
@SerialName("notice") Notice,
@SerialName("request") Request,
@SerialName("message") Msg,
@SerialName("message_sent") MsgSent,
}

View File

@ -10,14 +10,20 @@ internal enum class NoticeType {
@SerialName("group_decrease") GroupMemDecrease,
@SerialName("group_increase") GroupMemIncrease,
@SerialName("group_recall") GroupRecall,
@SerialName("group_apply") GroupApply,
@SerialName("group_card") GroupCard,
@SerialName("essence") Essence,
@SerialName("friend_recall") FriendRecall,
@SerialName("friend_add") FriendApply,
@SerialName("notify") Notify,
@SerialName("group_upload") GroupUpload,
@SerialName("private_upload") PrivateUpload
}
@Serializable
internal enum class RequestType {
@SerialName("friend ") Friend,
@SerialName("group") Group,
}
@Serializable
internal enum class NoticeSubType {
@SerialName("none") None,
@ -36,6 +42,17 @@ internal enum class NoticeSubType {
@SerialName("kick_me") KickMe,
@SerialName("poke") Poke,
@SerialName("title") Title,
@SerialName("delete") Delete,
}
@Serializable
internal enum class RequestSubType {
@SerialName("none") None,
@SerialName("add") Add,
@SerialName("invite") Invite,
}
/**
@ -59,8 +76,36 @@ internal data class NoticeEvent(
@SerialName("file") val file: GroupFileMsg? = null,
@SerialName("private_file") val privateFile: PrivateFileMsg? = null,
@SerialName("flag") val flag: String? = null,
// 群名片
@SerialName("card_new") val cardNew: String? = null,
@SerialName("card_old") val cardOld: String? = null,
// 群头衔
@SerialName("title") val title: String? = null,
// 戳一戳
@SerialName("poke_detail") val pokeDetail: PokeDetail? = null,
)
/**
* 不要使用继承的方式实现通用字段,那样会很难维护!
*/
@Serializable
internal data class RequestEvent(
@SerialName("time") val time: Long,
@SerialName("self_id") val selfId: Long,
@SerialName("post_type") val postType: PostType,
@SerialName("request_type") val type: RequestType,
@SerialName("sub_type") val subType: RequestSubType = RequestSubType.None,
@SerialName("group_id") val groupId: Long = 0,
@SerialName("user_id") val userId: Long = 0,
@SerialName("comment") val comment: String = "",
@SerialName("flag") val flag: String? = null,
)
@Serializable
internal data class GroupFileMsg(
val id: String,
@ -78,4 +123,12 @@ internal data class PrivateFileMsg(
@SerialName("sub_id") val subId: String,
val url: String,
val expire: Long,
)
@Serializable
internal data class PokeDetail (
val action: String? = "戳了戳",
val suffix: String? = "",
@SerialName("action_img_url")
val actionImg: String? = "https://tianquan.gtimg.cn/nudgeaction/item/0/expression.jpg",
)

View File

@ -7,6 +7,7 @@ import com.tencent.qqnt.kernel.nativeinterface.*
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toCQCode
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
@ -15,6 +16,7 @@ import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
import moe.fuqiuluo.shamrock.remote.service.api.RichMediaUploadHandler
import moe.fuqiuluo.shamrock.remote.service.data.push.MessageTempSource
import moe.fuqiuluo.shamrock.remote.service.data.push.PostType
import mqq.app.MobileQQ
@ -64,6 +66,10 @@ internal object AioListener: IKernelMsgListener {
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString())
if (rawMsg.isEmpty()) return
if (ShamrockConfig.aliveReply() && rawMsg == "ping") {
MessageHelper.sendMessageWithoutMsgId(record.chatType, record.peerUin.toString(), "pong", { _, _ -> })
}
//if (rawMsg.contains("forward")) {
// LogCenter.log(record.extInfoForUI.decodeToString(), Level.WARN)
//}
@ -72,8 +78,8 @@ internal object AioListener: IKernelMsgListener {
MsgConstant.KCHATTYPEGROUP -> {
LogCenter.log("群消息(group = ${record.peerName}(${record.peerUin}), uin = ${record.senderUin}, id = $msgHash|${record.msgSeq}, msg = $rawMsg)")
ShamrockConfig.getGroupMsgRule()?.let { rule ->
if (rule.black?.contains(record.peerUin) == true) return
if (rule.white?.contains(record.peerUin) == false) return
if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
}
if(!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(
@ -85,8 +91,8 @@ internal object AioListener: IKernelMsgListener {
MsgConstant.KCHATTYPEC2C -> {
LogCenter.log("私聊消息(private = ${record.senderUin}, id = [$msgHash | ${record.msgId} | ${record.msgSeq}], msg = $rawMsg)")
ShamrockConfig.getPrivateRule()?.let { rule ->
if (rule.black?.contains(record.peerUin) == true) return
if (rule.white?.contains(record.peerUin) == false) return
if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
}
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
@ -97,14 +103,12 @@ internal object AioListener: IKernelMsgListener {
}
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
if (!ShamrockConfig.allowTempSession()) {
return
}
if (!ShamrockConfig.allowTempSession()) return
LogCenter.log("私聊临时消息(private = ${record.senderUin}, id = $msgHash, msg = $rawMsg)")
ShamrockConfig.getPrivateRule()?.let { rule ->
if (rule.black?.contains(record.peerUin) == true) return
if (rule.white?.contains(record.peerUin) == false) return
if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
}
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
@ -120,6 +124,10 @@ internal object AioListener: IKernelMsgListener {
}
}
override fun onMsgRecall(chatType: Int, peerId: String, msgId: Long) {
LogCenter.log("onMsgRecall($chatType, $peerId, $msgId)")
}
override fun onAddSendMsg(record: MsgRecord) {
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return // TODO: 频道消息暂不处理
if (record.peerUin == TicketSvc.getLongUin()) return // 发给自己的消息不处理
@ -138,10 +146,7 @@ internal object AioListener: IKernelMsgListener {
time = record.msgTime
)
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString())
if (rawMsg.isEmpty()) return@launch
LogCenter.log("发送消息($msgHash | ${record.msgSeq} | ${record.msgId}): $rawMsg")
LogCenter.log("预发送消息($msgHash | ${record.msgSeq} | ${record.msgId})")
} catch (e: Throwable) {
LogCenter.log(e.stackTraceToString(), Level.WARN)
}
@ -150,14 +155,14 @@ internal object AioListener: IKernelMsgListener {
override fun onMsgInfoListUpdate(msgList: ArrayList<MsgRecord>?) {
msgList?.forEach { record ->
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return@forEach// TODO: 频道消息暂不处理
if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED
|| record.sendStatus == MsgConstant.KSENDSTATUSSENDING) {
return@forEach
}
GlobalScope.launch {
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return@launch// TODO: 频道消息暂不处理
if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED
|| record.sendStatus == MsgConstant.KSENDSTATUSSENDING) {
return@launch
}
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
@ -177,25 +182,32 @@ internal object AioListener: IKernelMsgListener {
.updateMsgSeqByMsgHash(msgHash, record.msgSeq.toInt())
}
if (!ShamrockConfig.enableSelfMsg())
if (!ShamrockConfig.enableSelfMsg() || record.senderUin != TicketSvc.getLongUin())
return@launch
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString())
if (rawMsg.isEmpty()) return@launch
LogCenter.log("自发消息(target = ${record.peerUin}, id = $msgHash, msg = $rawMsg)")
when (record.chatType) {
MsgConstant.KCHATTYPEGROUP -> {
GlobalEventTransmitter.MessageTransmitter
.transGroupMessage(record, record.elements, rawMsg, msgHash, PostType.MsgSent)
if(!GlobalEventTransmitter.MessageTransmitter
.transGroupMessage(record, record.elements, rawMsg, msgHash, PostType.MsgSent)) {
LogCenter.log("自发群消息推送失败 -> MessageTransmitter", Level.WARN)
}
}
MsgConstant.KCHATTYPEC2C -> {
GlobalEventTransmitter.MessageTransmitter
.transPrivateMessage(record, record.elements, rawMsg, msgHash, PostType.MsgSent)
if(!GlobalEventTransmitter.MessageTransmitter
.transPrivateMessage(record, record.elements, rawMsg, msgHash, PostType.MsgSent)) {
LogCenter.log("自发私聊消息推送失败 -> MessageTransmitter", Level.WARN)
}
}
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
if (!ShamrockConfig.allowTempSession()) return@launch
GlobalEventTransmitter.MessageTransmitter
.transPrivateMessage(record, record.elements, rawMsg, msgHash, PostType.MsgSent, MessageTempSource.Group)
if(!GlobalEventTransmitter.MessageTransmitter
.transPrivateMessage(record, record.elements, rawMsg, msgHash, PostType.MsgSent, MessageTempSource.Group)) {
LogCenter.log("自发私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
}
}
else -> LogCenter.log("不支持SELF PUSH事件: ${record.chatType}")
}
@ -347,6 +359,11 @@ internal object AioListener: IKernelMsgListener {
}
}
override fun onRichMediaUploadComplete(notifyInfo: FileTransNotifyInfo) {
LogCenter.log("onRichMediaUploadComplete($notifyInfo)", Level.DEBUG)
RichMediaUploadHandler.notify(notifyInfo)
}
override fun onRecvOnlineFileMsg(arrayList: ArrayList<MsgRecord>?) {
LogCenter.log(("onRecvOnlineFileMsg" + arrayList?.joinToString { ", " }), Level.DEBUG)
}
@ -359,10 +376,6 @@ internal object AioListener: IKernelMsgListener {
}
override fun onRichMediaUploadComplete(fileTransNotifyInfo: FileTransNotifyInfo) {
}
override fun onSearchGroupFileInfoUpdate(searchGroupFileResult: SearchGroupFileResult?) {
LogCenter.log("onSearchGroupFileInfoUpdate($searchGroupFileResult)", Level.DEBUG)
}
@ -387,6 +400,14 @@ internal object AioListener: IKernelMsgListener {
LogCenter.log("onGroupTransferInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG)
}
override fun onGuildInteractiveUpdate(guildInteractiveNotificationItem: GuildInteractiveNotificationItem?) {
}
override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) {
}
override fun onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: DownloadRelateEmojiResultInfo?) {
}
@ -435,10 +456,6 @@ internal object AioListener: IKernelMsgListener {
}
override fun onMsgRecall(chatType: Int, peerId: String?, msgId: Long) {
LogCenter.log("onMsgRecall($chatType, $peerId, $msgId)")
}
override fun onMsgSecurityNotify(msgRecord: MsgRecord?) {
LogCenter.log("onMsgSecurityNotify($msgRecord)")
}

View File

@ -1,7 +1,7 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.shamrock.remote.service.listener
import com.arthenica.smartexception.ThrowableWrapper
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.coroutines.DelicateCoroutinesApi
import moe.fuqiuluo.shamrock.helper.ContactHelper
@ -10,7 +10,8 @@ import kotlinx.coroutines.launch
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
import kotlinx.io.core.readUInt
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.proto.ProtoByteString
import moe.fuqiuluo.proto.ProtoMap
import moe.fuqiuluo.proto.asInt
@ -29,6 +30,9 @@ import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
import moe.fuqiuluo.shamrock.remote.service.data.push.RequestSubType
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.readBuf32Long
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.xposed.helper.PacketHandler
@ -59,27 +63,27 @@ internal object PrimitiveListener {
subType = pb[1, 2, 2].asInt
}
val msgTime = pb[1, 2, 6].asLong
when(msgType) {
when (msgType) {
33 -> onGroupMemIncreased(msgTime, pb)
34 -> onGroupMemberDecreased(msgTime, pb)
44 -> onGroupAdminChange(msgTime, pb)
84 -> onGroupApply(msgTime, pb)
87 -> onInviteGroup(msgTime, pb)
528 -> when(subType) {
528 -> when (subType) {
35 -> onFriendApply(msgTime, pb)
39 -> onCardChange(msgTime, pb)
// invite
68 -> onGroupApply(msgTime, pb)
138 -> onC2CRecall(msgTime, pb)
290 -> onC2cPoke(msgTime, pb)
}
732 -> when(subType) {
732 -> when (subType) {
12 -> onGroupBan(msgTime, pb)
17 -> {
onGroupRecall(msgTime, pb)
// invite
onGroupMemIncreased(msgTime, pb)
}
16 -> onGroupTitleChange(msgTime, pb)
17 -> onGroupRecall(msgTime, pb)
20 -> onGroupPoke(msgTime, pb)
21 -> onEssenceMessage(msgTime, pb)
}
}
}
@ -92,20 +96,29 @@ internal object PrimitiveListener {
lateinit var target: String
lateinit var operation: String
var suffix: String? = null
var actionImg: String? = null
var action: String? = null
detail[7]
.asList
.value
.forEach {
val value = it[2].asUtf8String
when(it[1].asUtf8String) {
when (it[1].asUtf8String) {
"uin_str1" -> operation = value
"uin_str2" -> target = value
"action_str" -> action = value
"alt_str1" -> action = value
"suffix_str" -> suffix = value
"action_img_url" -> actionImg = value
}
}
LogCenter.log("私聊戳一戳: $operation -> $target")
if(!GlobalEventTransmitter.PrivateNoticeTransmitter
.transPrivatePoke(msgTime, operation.toLong(), target.toLong())) {
LogCenter.log("私聊戳一戳: $operation $action $target $suffix")
if (!GlobalEventTransmitter.PrivateNoticeTransmitter
.transPrivatePoke(msgTime, operation.toLong(), target.toLong(), action, suffix, actionImg)
) {
LogCenter.log("私聊戳一戳推送失败!", Level.WARN)
}
}
@ -118,30 +131,135 @@ internal object PrimitiveListener {
if (applier == 0L) {
applier = pb[4, 3, 8].asLong
}
val msg_time = pb[1, 3, 2, 1, 9].asLong
val src = pb[1, 3, 2, 1, 7].asInt
val subSrc = pb[1, 3, 2, 1, 8].asInt
val reqs = requestFriendSystemMsgNew(20, 0, 0)
val req = reqs?.first {
it.msg_time.get() == msg_time
val flag: String = try {
val reqs = requestFriendSystemMsgNew(20, 0, 0)
val req = reqs?.first {
it.msg_time.get() == msgTime
}
val seq = req?.msg_seq?.get()
"$seq;$src;$subSrc;$applier"
} catch (err: Throwable) {
"$msgTime;$src;$subSrc;$applier"
}
val seq = req?.msg_seq?.get()
val flag = "$seq;$src;$subSrc;$applier"
LogCenter.log("来自$applier 的好友申请:$msg ($source)")
if(!GlobalEventTransmitter.PrivateNoticeTransmitter
.transFriendApply(msgTime, applier, msg, flag)) {
if (!GlobalEventTransmitter.RequestTransmitter
.transFriendApp(msgTime, applier, msg, flag)
) {
LogCenter.log("好友申请推送失败!", Level.WARN)
}
}
private suspend fun onCardChange(msgTime: Long, pb: ProtoMap) {
val targetId = pb[1, 3, 2, 1, 13, 2].asUtf8String
val newCardList = pb[1, 3, 2, 1, 13, 3].asList
var newCard = ""
newCardList
.value
.forEach {
if (it[1].asInt == 1) {
newCard = it[2].asUtf8String
}
}
val groupId = pb[1, 3, 2, 1, 13, 4].asLong
var oldCard = ""
val targetQQ = ContactHelper.getUinByUidAsync(targetId).toLong()
LogCenter.log("群组[$groupId]成员$targetQQ 群名片变动 -> $newCard")
// oldCard暂时获取不到
// GroupSvc.getTroopMemberInfoByUin(groupId.toString(), targetQQ.toString()).onSuccess {
// oldCard = it.troopnick
// }.onFailure {
// LogCenter.log("获取群成员信息失败!", Level.WARN)
// }
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transCardChange(msgTime, targetQQ, oldCard, newCard, groupId)
) {
LogCenter.log("群名片变动推送失败!", Level.WARN)
}
}
private suspend fun onGroupTitleChange(msgTime: Long, pb: ProtoMap) {
val groupCode = pb[1, 1, 1].asULong
val readPacket = ByteReadPacket(pb[1, 3, 2].asByteArray)
val detail = if (readPacket.readBuf32Long() == groupCode) {
readPacket.discardExact(1)
ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
} else pb[1, 3, 2]
val targetUin = detail[5, 5].asLong
val groupId = detail[4].asLong
// 恭喜<{\"cmd\":5,\"data\":\"qq\",\"text}\":\"nickname\"}>获得群主授予的<{\"cmd\":1,\"data\":\"https://qun.qq.com/qqweb/m/qun/medal/detail.html?_wv=16777223&bid=2504&gc=gid&isnew=1&medal=302&uin=uin\",\"text\":\"title\",\"url\":\"https://qun.qq.com/qqweb/m/qun/medal/detail.html?_wv=16777223&bid=2504&gc=gid&isnew=1&medal=302&uin=uin\"}>头衔
val titleChangeInfo = detail[5, 2].asUtf8String
if (titleChangeInfo.indexOf("群主授予") == -1) {
return
}
val titleJson = titleChangeInfo.split("获得群主授予的<")[1].replace(">头衔", "")
val titleJsonObj = Json.decodeFromString<JsonElement>(titleJson).asJsonObject
val title = titleJsonObj["text"].asString
LogCenter.log("群组[$groupId]成员$targetUin 获得群头衔 -> $title")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transTitleChange(msgTime, targetUin, title, groupId)
) {
LogCenter.log("群头衔变动推送失败!", Level.WARN)
}
}
private suspend fun onEssenceMessage(msgTime: Long, pb: ProtoMap) {
val groupCode = pb[1, 1, 1].asULong
val readPacket = ByteReadPacket(pb[1, 3, 2].asByteArray)
val detail = if (readPacket.readBuf32Long() == groupCode) {
readPacket.discardExact(1)
ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
} else pb[1, 3, 2]
val groupId = detail[4].asLong
val mesSeq = detail[37].asInt
val senderUin = detail[33, 5].asLong
val operatorUin = detail[33, 6].asLong
var msgId = 0
MessageHelper.getMsgMappingBySeq(MsgConstant.KCHATTYPEGROUP, mesSeq).also {
if (it != null) {
msgId = it.msgHashId
}
}
val subType = when (val type = detail[33, 4].asInt) {
1 -> {
// add essence
LogCenter.log("群设精消息($groupId): $senderUin $msgId $operatorUin")
NoticeSubType.Add
}
2 -> {
// remove essence
LogCenter.log("群取精消息($groupId): $senderUin $msgId $operatorUin")
NoticeSubType.Delete
}
else -> error("onEssenceMessage unknown type: $type")
}
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transEssenceChange(msgTime, senderUin, operatorUin, msgId, groupId, subType)
) {
LogCenter.log("精华消息变动推送失败!", Level.WARN)
}
}
private suspend fun onGroupPoke(time: Long, pb: ProtoMap) {
val groupCode1 = pb[1, 1, 1].asULong
var groupCode: Long = groupCode1
val readPacket = ByteReadPacket( pb[1, 3, 2].asByteArray )
val readPacket = ByteReadPacket(pb[1, 3, 2].asByteArray)
val groupCode2 = readPacket.readBuf32Long()
var detail = if (groupCode2 == groupCode1) {
@ -158,20 +276,28 @@ internal object PrimitiveListener {
lateinit var target: String
lateinit var operation: String
var action: String? = null
var suffix: String? = null
var actionImg: String? = null
detail[26][7]
.asList
.value
.forEach {
val value = it[2].asUtf8String
when(it[1].asUtf8String) {
"uin_str1" -> operation = value
"uin_str2" -> target = value
val value = it[2].asUtf8String
when (it[1].asUtf8String) {
"uin_str1" -> operation = value
"uin_str2" -> target = value
"action_str" -> action = value
"alt_str1" -> action = value
"suffix_str" -> suffix = value
"action_img_url" -> actionImg = value
}
}
}
LogCenter.log("群戳一戳($groupCode): $operation -> $target")
LogCenter.log("群戳一戳($groupCode): $operation $action $target $suffix")
if(!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupPoke(time, operation.toLong(), target.toLong(), groupCode)) {
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupCode)
) {
LogCenter.log("群戳一戳推送失败!", Level.WARN)
}
}
@ -189,54 +315,31 @@ internal object PrimitiveListener {
LogCenter.log("私聊消息撤回: $operation, seq = $msgSeq, hash = ${mapping.msgHashId}, tip = $tipText")
if(!GlobalEventTransmitter.PrivateNoticeTransmitter
.transPrivateRecall(time, operation, mapping.msgHashId, tipText)) {
if (!GlobalEventTransmitter.PrivateNoticeTransmitter
.transPrivateRecall(time, operation, mapping.msgHashId, tipText)
) {
LogCenter.log("私聊消息撤回推送失败!", Level.WARN)
}
}
private suspend fun onGroupMemIncreased(time: Long, pb: ProtoMap) {
when(pb[1, 2, 1].asInt) {
732 -> {
// invite
val groupCode = pb[1, 3, 2, 4].asULong
lateinit var target: String
lateinit var operation: String
pb[1, 3, 2, 26, 7].asList
.value
.forEach {
val value = it[2].asUtf8String
when (it[1].asUtf8String) {
"invitee" -> operation = value
"invitor" -> target = value
}
val groupCode = pb[1, 3, 2, 1].asULong
val targetUid = pb[1, 3, 2, 3].asUtf8String
val type = pb[1, 3, 2, 4].asInt
val operation = ContactHelper.getUinByUidAsync(pb[1, 3, 2, 5].asUtf8String).toLong()
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群成员增加($groupCode): $target, type = $type")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMemberNumChanged(
time, target, groupCode, operation, NoticeType.GroupMemIncrease, when (type) {
130 -> NoticeSubType.Approve
131 -> NoticeSubType.Invite
else -> NoticeSubType.Approve
}
val type = 131
LogCenter.log("群成员增加($groupCode): $target, type = $type")
if(!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMemberNumChanged(time, target.toLong(), groupCode, operation.toLong(), NoticeType.GroupMemIncrease, NoticeSubType.Invite)) {
LogCenter.log("群成员增加推送失败!", Level.WARN)
}
}
33 -> {
// approve
val groupCode = pb[1, 3, 2, 1].asULong
val targetUid = pb[1, 3, 2, 3].asUtf8String
val type = pb[1, 3, 2, 4].asInt
val operation = ContactHelper.getUinByUidAsync(pb[1, 3, 2, 5].asUtf8String).toLong()
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群成员增加($groupCode): $target, type = $type")
if(!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMemberNumChanged(time, target, groupCode, operation, NoticeType.GroupMemIncrease, when(type) {
130 -> NoticeSubType.Approve
131 -> NoticeSubType.Invite
else -> NoticeSubType.Approve
})) {
LogCenter.log("群成员增加推送失败!", Level.WARN)
}
}
)
) {
LogCenter.log("群成员增加推送失败!", Level.WARN)
}
}
@ -245,18 +348,22 @@ internal object PrimitiveListener {
val targetUid = pb[1, 3, 2, 3].asUtf8String
val type = pb[1, 3, 2, 4].asInt
val operation = ContactHelper.getUinByUidAsync(pb[1, 3, 2, 5].asUtf8String).toLong()
// 131 passive | 130 active | 3 kick_self
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群成员减少($groupCode): $target, type = $type")
val subtype = when (type) {
130 -> NoticeSubType.Leave
131 -> NoticeSubType.Kick
3 -> NoticeSubType.KickMe
else -> {
NoticeSubType.Kick
}
}
if(!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMemberNumChanged(time, target, groupCode, operation, NoticeType.GroupMemDecrease, when(type) {
130 -> NoticeSubType.Kick
131 -> NoticeSubType.Leave
3 -> NoticeSubType.KickMe
else -> NoticeSubType.Kick
})) {
LogCenter.log("群成员减少($groupCode): $target, type = $subtype ($type)")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMemberNumChanged(time, target, groupCode, operation, NoticeType.GroupMemDecrease, subtype)
) {
LogCenter.log("群成员减少推送失败!", Level.WARN)
}
}
@ -275,8 +382,9 @@ internal object PrimitiveListener {
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群管理员变动($groupCode): $target, isSetAdmin = $isSetAdmin")
if(!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupAdminChanged(msgTime, target, groupCode, isSetAdmin)) {
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupAdminChanged(msgTime, target, groupCode, isSetAdmin)
) {
LogCenter.log("群管理员变动推送失败!", Level.WARN)
}
}
@ -290,20 +398,21 @@ internal object PrimitiveListener {
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群禁言($groupCode): $operation -> $target, 时长 = ${duration}s")
if(!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupBan(msgTime, operation, target, groupCode, duration)) {
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupBan(msgTime, operation, target, groupCode, duration)
) {
LogCenter.log("群禁言推送失败!", Level.WARN)
}
}
private suspend fun onGroupRecall(time: Long, pb: ProtoMap) {
val groupCode = pb[1, 1, 1].asULong
val readPacket = ByteReadPacket( pb[1, 3, 2].asByteArray )
val readPacket = ByteReadPacket(pb[1, 3, 2].asByteArray)
try {
/**
* 真是不理解这个傻呗设计有些群是正常的Protobuf有些群要去掉7字节
*/
val detail = if (readPacket.readUInt().toLong() == groupCode) {
val detail = if (readPacket.readBuf32Long() == groupCode) {
readPacket.discardExact(1)
ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
} else pb[1, 3, 2]
@ -322,8 +431,9 @@ internal object PrimitiveListener {
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群消息撤回($groupCode): $operator -> $target, seq = $msgSeq, hash = $msgHash, tip = $tipText")
if(!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMsgRecall(time, operator, target, groupCode, msgHash, tipText)) {
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMsgRecall(time, operator, target, groupCode, msgHash, tipText)
) {
LogCenter.log("群消息撤回推送失败!", Level.WARN)
}
} finally {
@ -332,7 +442,7 @@ internal object PrimitiveListener {
}
private suspend fun onGroupApply(time: Long, pb: ProtoMap) {
when(pb[1, 2, 1].asInt) {
when (pb[1, 2, 1].asInt) {
84 -> {
val groupCode = pb[1, 3, 2, 1].asULong
val applierUid = pb[1, 3, 2, 3].asUtf8String
@ -340,25 +450,25 @@ internal object PrimitiveListener {
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
LogCenter.log("入群申请($groupCode) $applier: \"$reason\"")
try {
val reqs = requestGroupSystemMsgNew(20, 0, 0)
val req = reqs?.first {
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(10, 2)
reqs = reqs + riskReqs
val req = reqs.first {
it.msg_time.get() == time
}
val seq = req?.msg_seq?.get()
val flag = "$seq;$groupCode;$applierUid"
if(!seq?.let {
GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupApply(it, applier, reason, groupCode, flag, NoticeSubType.Add)
}!!) {
LogCenter.log("入群申请推送失败!", Level.WARN)
}
val seq = req.msg_seq?.get()
"$seq;$groupCode;$applier"
} catch (err: Throwable) {
LogCenter.log("入群申请推送失败!", Level.WARN)
LogCenter.log(err.stackTraceToString(), Level.ERROR)
"$time;$groupCode;$applier"
}
if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, reason, groupCode, flag, RequestSubType.Add)
) {
LogCenter.log("入群申请推送失败!", Level.WARN)
}
}
528 -> {
val groupCode = pb[1, 3, 2, 2, 3].asULong
val applierUid = pb[1, 3, 2, 2, 5].asUtf8String
@ -368,49 +478,49 @@ internal object PrimitiveListener {
return
}
LogCenter.log("邀请入群申请($groupCode): $applier")
try {
val reqs = requestGroupSystemMsgNew(20, 0, 0)
val req = reqs?.first {
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(10, 2)
reqs = reqs + riskReqs
val req = reqs.first {
it.msg_time.get() == time
}
val seq = req?.msg_seq?.get()
val flag = "$seq;$groupCode;$applierUid"
if(!seq?.let {
GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupApply(it, applier, "", groupCode, flag, NoticeSubType.Add)
}!!) {
LogCenter.log("邀请入群申请推送失败!", Level.WARN)
}
val seq = req.msg_seq?.get()
"$seq;$groupCode;$applier"
} catch (err: Throwable) {
"$time;$groupCode;$applierUid"
}
if (GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, "", groupCode, flag, RequestSubType.Add)
) {
LogCenter.log("邀请入群申请推送失败!", Level.WARN)
LogCenter.log(err.stackTraceToString(), Level.ERROR)
}
}
}
}
private suspend fun onInviteGroup(time: Long, pb: ProtoMap) {
val groupCode = pb[1, 3, 2, 1].asULong
val invitorUid = pb[1, 3, 2, 5].asUtf8String
val invitor = ContactHelper.getUinByUidAsync(invitorUid).toLong()
val uin = pb[1, 1, 5].asLong
LogCenter.log("邀请入群$groupCode 邀请者: \"$invitor\"")
try {
val reqs = requestGroupSystemMsgNew(20, 0, 0)
val req = reqs?.first {
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(10, 2)
reqs = reqs + riskReqs
val req = reqs.first {
it.msg_time.get() == time
}
val seq = req?.msg_seq?.get()
val flag = "$seq;$groupCode;$uin"
if(!seq?.let {
GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupApply(it, invitor, "", groupCode, flag, NoticeSubType.Invite)
}!!) {
LogCenter.log("邀请入群推送失败!", Level.WARN)
}
val seq = req.msg_seq?.get()
"$seq;$groupCode;$uin"
} catch (err: Throwable) {
"$time;$groupCode;$uin"
}
if (GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, invitor, "", groupCode, flag, RequestSubType.Invite)
) {
LogCenter.log("邀请入群推送失败!", Level.WARN)
LogCenter.log(err.stackTraceToString(), Level.ERROR)
}
}

View File

@ -7,7 +7,7 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage
import de.robv.android.xposed.XposedBridge.log
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import moe.fuqiuluo.shamrock.xposed.loader.ActionLoader
import moe.fuqiuluo.shamrock.xposed.loader.FuckAMS
import moe.fuqiuluo.shamrock.xposed.loader.KeepAlive
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
import moe.fuqiuluo.shamrock.tools.FuzzySearchClass
import moe.fuqiuluo.shamrock.tools.afterHook
@ -26,6 +26,12 @@ internal class XposedEntry: IXposedHookLoadPackage {
companion object {
@JvmStatic
var sec_static_stage_inited = false
@JvmStatic
var sec_static_nativehook_inited = false
external fun injected(): Boolean
external fun hasEnv(): Boolean
}
private var firstStageInit = false
@ -33,7 +39,7 @@ internal class XposedEntry: IXposedHookLoadPackage {
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) {
when (param.packageName) {
PACKAGE_NAME_QQ -> entryMQQ(param.classLoader)
"android" -> FuckAMS.injectAMS(param.classLoader)
"android" -> KeepAlive(param.classLoader)
PACKAGE_NAME_TIM -> entryTim(param.classLoader)
}
}
@ -108,7 +114,6 @@ internal class XposedEntry: IXposedHookLoadPackage {
if (sec_static_stage_inited) return
val classLoader = ctx.classLoader.also { requireNotNull(it) }
LuoClassloader.hostClassLoader = classLoader
if(injectClassloader(XposedEntry::class.java.classLoader)) {
@ -116,12 +121,7 @@ internal class XposedEntry: IXposedHookLoadPackage {
System.setProperty("qxbot_flag", "1")
} else return
log("Process Name = " + MobileQQ.getMobileQQ().qqProcessName.apply {
// if (!contains("msf", ignoreCase = true)) return // 非MSF进程 退出
//if (contains("peak")) {
// PlatformUtils.killProcess(ctx, this)
//}
})
log("Process Name = " + MobileQQ.getMobileQQ().qqProcessName)
PlatformUtils.isTim()

View File

@ -5,23 +5,26 @@ import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.VersionedPackage
import android.os.Build
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XSharedPreferences
import de.robv.android.xposed.XposedHelpers
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.hookMethod
import moe.fuqiuluo.shamrock.xposed.XposedEntry
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
import mqq.app.MobileQQ
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
/**
* 反检测
*/
class AntiDetection: IAction {
external fun antiNativeDetections(): Boolean
override fun invoke(ctx: Context) {
antiFindPackage(ctx)
antiNativeDetection()
if (ShamrockConfig.isAntiTrace())
antiTrace()
antiMemoryWalking()
@ -38,6 +41,30 @@ class AntiDetection: IAction {
return false
}
private fun antiNativeDetection() {
try {
//System.loadLibrary("clover")
NativeLoader.load("clover")
val env = XposedEntry.hasEnv()
val injected = XposedEntry.injected()
if (!env || !injected) {
LogCenter.log("[Shamrock] Shamrock反检测启动失败(env=$env, injected=$injected)", Level.ERROR)
} else {
XposedEntry.sec_static_nativehook_inited = true
val pref = XSharedPreferences("moe.fuqiuluo.shamrock", "shared_config")
if (pref.file.canRead()) {
if (pref.getBoolean("super_anti", false)) {
LogCenter.log("[Shamrock] Shamrock反检测启动成功: ${antiNativeDetections()}", Level.INFO)
}
} else {
LogCenter.log("[Shamrock] unable to load XSharedPreferences", Level.WARN)
}
}
} catch (e: Throwable) {
LogCenter.log("[Shamrock] Shamrock反检测启动失败请检查LSPosed版本使用大于100: ${e.message}", Level.ERROR)
}
}
private fun antiFindPackage(context: Context) {
val packageManager = context.packageManager
val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock", 0)

View File

@ -33,16 +33,21 @@ internal class InitRemoteService : IAction {
if (!PlatformUtils.isMqqPackage()) return
if (ShamrockConfig.allowWebHook()) {
HttpService.initTransmitter()
}
val runtime = AppRuntimeFetcher.appRuntime
if (!runtime.isLogin) {
LogCenter.log("未登录不启动任何WebSocket服务登录完成后请重新启动QQ。", Level.WARN)
return
}
if (ShamrockConfig.openWebSocket()) {
startWebSocketServer()
}
if (ShamrockConfig.openWebSocketClient()) {
val runtime = AppRuntimeFetcher.appRuntime
val curUin = runtime.currentAccountUin
val defaultToken = ShamrockConfig.getToken()
ShamrockConfig.getWebSocketClientAddress().forEach { conn ->
@ -61,7 +66,7 @@ internal class InitRemoteService : IAction {
wsHeaders["authorization"] = "bearer $token"
}
LogCenter.log("尝试链接WebSocketClient(url = ${conn.address})",Level.WARN)
startWebSocketClient(conn.address, wsHeaders)
startWebSocketClient(conn.address, conn.heartbeatInterval ?: 15000, wsHeaders)
}
}
}
@ -80,7 +85,7 @@ internal class InitRemoteService : IAction {
return@launch
}
require(config.port in 0 .. 65536) { "WebSocketServer端口不合法" }
val server = WebSocketService(config.address, config.port!!)
val server = WebSocketService(config.address, config.port!!, config.heartbeatInterval ?: (15 * 1000))
server.isReuseAddr = true
server.start()
} catch (e: Throwable) {
@ -89,11 +94,15 @@ internal class InitRemoteService : IAction {
}
}
private fun startWebSocketClient(url: String, wsHeaders: HashMap<String, String>) {
private fun startWebSocketClient(
url: String,
interval: Long,
wsHeaders: HashMap<String, String>
) {
GlobalScope.launch {
try {
if (url.startsWith("ws://") || url.startsWith("wss://")) {
val wsClient = WebSocketClientService(url, wsHeaders)
val wsClient = WebSocketClientService(url, interval, wsHeaders)
wsClient.connect()
timer(initialDelay = 5000L, period = 5000L) {
if (wsClient.isClosed || wsClient.isClosing) {

View File

@ -3,12 +3,15 @@ package moe.fuqiuluo.shamrock.xposed.loader
import android.content.pm.ApplicationInfo
import android.os.Build
import android.os.Process
import de.robv.android.xposed.XSharedPreferences
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import moe.fuqiuluo.shamrock.tools.hookMethod
import java.lang.reflect.Method
import kotlin.concurrent.timer
internal object FuckAMS {
internal object KeepAlive {
private val KeepPackage = arrayOf(
"com.tencent.mobileqq", "moe.fuqiuluo.shamrock"
)
@ -16,23 +19,71 @@ internal object FuckAMS {
private lateinit var KeepThread: Thread
private lateinit var METHOD_IS_KILLED: Method
private var allowPersistent: Boolean = false
fun injectAMS(loader: ClassLoader) {
operator fun invoke(loader: ClassLoader) {
val pref = XSharedPreferences("moe.fuqiuluo.shamrock", "shared_config")
hookAMS(pref, loader)
hookDoze(pref, loader)
}
private fun hookDoze(pref: XSharedPreferences, loader: ClassLoader) {
if (pref.file.canRead() && pref.getBoolean("hook_doze", false)) {
val result = runCatching {
val DeviceIdleController = XposedHelpers.findClass("com.android.server.DeviceIdleController", loader)
?: return@runCatching -1
val becomeActiveLocked = XposedHelpers.findMethodBestMatch(DeviceIdleController, "becomeActiveLocked", String::class.java, Integer.TYPE)
?: return@runCatching -2
if (!becomeActiveLocked.isAccessible) {
becomeActiveLocked.isAccessible = true
}
DeviceIdleController.hookMethod("onStart").after {
XposedBridge.log("[Shamrock] DeviceIdleController onStart")
timer(initialDelay = 120_000L, period = 240_000L) {
XposedBridge.log("[Shamrock] try to wakeup screen")
becomeActiveLocked.invoke(it.thisObject, "screen", Process.myUid())
}
}
DeviceIdleController.hookMethod("becomeInactiveIfAppropriateLocked").before {
XposedBridge.log("[Shamrock] DeviceIdleController becomeInactiveIfAppropriateLocked")
it.result = Unit
}
DeviceIdleController.hookMethod("stepIdleStateLocked").before {
XposedBridge.log("[Shamrock] DeviceIdleController stepIdleStateLocked")
it.result = Unit
}
return@runCatching 0
}.getOrElse { -5 }
if(result < 0) {
XposedBridge.log("[Shamrock] Unable to hookDoze: $result")
}
}
}
private fun hookAMS(pref: XSharedPreferences, loader: ClassLoader) {
kotlin.runCatching {
val ActivityManagerService = XposedHelpers.findClass("com.android.server.am.ActivityManagerService", loader)
ActivityManagerService.hookMethod("newProcessRecordLocked").after {
increaseAdj(it.result)
}
}.onFailure {
XposedBridge.log("Plan A failed: ${it.message}")
XposedBridge.log("[Shamrock] Plan A failed: ${it.message}")
}
if (pref.file.canRead()) {
allowPersistent = pref.getBoolean("persistent", false)
XposedBridge.log("[Shamrock] allowPersistent = $allowPersistent")
} else {
XposedBridge.log("[Shamrock] unable to load XSharedPreferences")
}
kotlin.runCatching {
val ProcessList = XposedHelpers.findClass("com.android.server.am.ProcessList", loader)
ProcessList.hookMethod("newProcessRecordLocked").after {
increaseAdj(it.result)
}
}.onFailure {
XposedBridge.log("Plan B failed: ${it.message}")
XposedBridge.log("[Shamrock] Plan B failed: ${it.message}")
}
}
@ -75,14 +126,14 @@ internal object FuckAMS {
if (!it.isAccessible) it.isAccessible = true
}.get(record) as ApplicationInfo
if(applicationInfo.processName in KeepPackage) {
XposedBridge.log("Process is keeping: $record")
XposedBridge.log("[Shamrock] Process is keeping: $record")
KeepRecords.add(record)
keepByAdj(record)
// Error
//if (noDied.exists()) {
// XposedBridge.log("Open NoDied Mode!!!")
// keepByPersistent(record)
//}
if (allowPersistent) {
XposedBridge.log("[Shamrock] Open NoDied Mode!!!")
keepByPersistent(record)
}
checkThread()
}
}

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import de.robv.android.xposed.XposedBridge
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.xposed.XposedEntry
import mqq.app.MobileQQ
import java.io.File
@ -16,21 +17,24 @@ internal object NativeLoader {
return externalLibPath.resolve("libffmpegkit.so").exists()
}
private var isInitShamrock = false
/**
* 使目标进程可以使用来自模块的库
*/
@SuppressLint("UnsafeDynamicallyLoadedCode")
fun load(name: String) {
try {
if (name == "shamrock") {
if (name == "shamrock" || name == "clover") {
val context = MobileQQ.getContext()
val packageManager = context.packageManager
val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock.hided", 0)
val file = File(applicationInfo.nativeLibraryDir)
LogCenter.log("LoadLibrary(name = $name)")
System.load(file.resolve("lib$name.so").absolutePath)
System.load(file.resolve("lib$name.so").also {
if (!it.exists()) {
LogCenter.log("LoadLibrary(name = $name) failed, file not exists.", level = Level.ERROR)
return
}
}.absolutePath)
} else {
val sourceFile = externalLibPath.resolve("lib$name.so")
val soFile = MobileQQ.getContext().filesDir.parentFile!!.resolve("txlib").resolve("lib$name.so")