mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 05:12:17 +00:00
Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
6201d12f5f | |||
b2adc5cedf | |||
0bb871bf01 | |||
dc969440ee | |||
b9b6e133d0 | |||
b5a9884448 | |||
bffb7caf04 | |||
2c3466b4c3 | |||
007e5fef2f | |||
48773cc47c | |||
b2ad4438ab | |||
2fdcfe332b | |||
7d8772ebf6 | |||
2a75160ef8 | |||
76bd58d984 | |||
39120bdeae | |||
7b07698f7b | |||
5c10a5a04e | |||
3a0dc41329 | |||
64c800c945 | |||
ecb3cea5a5 | |||
8e0ae6f85b | |||
9d893b481d | |||
85aaa54e4e | |||
c6dad5677c | |||
80a4a208b9 | |||
ae663e6b2e | |||
780f3577a5 | |||
3518f974cc | |||
911b003f7f | |||
69bc80e9b3 | |||
da0b74db1a | |||
7212938df3 | |||
ae1e78b267 | |||
b7266e490f | |||
6b4a429821 | |||
4a4507dfcd | |||
f63bcabf1b | |||
4932b36ee1 | |||
8c307c4f6e | |||
8d8846fafb | |||
544e216ddb | |||
4fedab719b | |||
75a567d5cd | |||
1a814e565a | |||
5ea260c24b | |||
2d57dc021d | |||
dabe2ea886 | |||
673902e514 | |||
5062ff7c3a | |||
0de6f851a6 | |||
c758b1576d | |||
5ba8bd11e2 | |||
679b7619ce | |||
282233131a | |||
edf857bcb6 | |||
cd1d1e928a | |||
45d6421153 | |||
8c6f529b4b | |||
b23620b5ef | |||
e09e00fcd3 | |||
0d35d5834b | |||
ee8dc75be3 | |||
5f91be547e | |||
7439622cd6 |
3
.github/ISSUE_TEMPLATE/bug.md
vendored
3
.github/ISSUE_TEMPLATE/bug.md
vendored
@ -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 架构:
|
||||
|
2
.github/ISSUE_TEMPLATE/feature.md
vendored
2
.github/ISSUE_TEMPLATE/feature.md
vendored
@ -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 相关的,且可以实现.
|
||||
|
||||
|
10
README.md
10
README.md
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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" />
|
||||
|
@ -99,6 +99,8 @@ void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::
|
||||
replace_string(cache, "]", "]");
|
||||
replace_string(cache, ",", ",");
|
||||
kv.emplace(key_tmp, cache);
|
||||
} else {
|
||||
kv.emplace("_type", cache);
|
||||
}
|
||||
dest.push_back(kv);
|
||||
kv.clear();
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
5
app/src/main/res/drawable/sharp_lock_24.xml
Normal file
5
app/src/main/res/drawable/sharp_lock_24.xml
Normal 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>
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
package com.tencent.qqnt.kernel.nativeinterface;
|
||||
|
||||
public class GuildInteractiveNotificationItem {
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package com.tencent.qqnt.kernel.nativeinterface;
|
||||
|
||||
public class GuildNotificationAbstractInfo {
|
||||
}
|
@ -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);
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
1
xposed/src/main/assets/native_init
Normal file
1
xposed/src/main/assets/native_init
Normal file
@ -0,0 +1 @@
|
||||
libclover.so
|
@ -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)
|
||||
|
6
xposed/src/main/cpp/anti_detection/anti_detection.cpp
Normal file
6
xposed/src/main/cpp/anti_detection/anti_detection.cpp
Normal file
@ -0,0 +1,6 @@
|
||||
#include "anti_detection.h"
|
||||
|
||||
|
||||
|
||||
|
||||
|
24
xposed/src/main/cpp/anti_detection/anti_detection.h
Normal file
24
xposed/src/main/cpp/anti_detection/anti_detection.h
Normal 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
|
123
xposed/src/main/cpp/clover.cpp
Normal file
123
xposed/src/main/cpp/clover.cpp
Normal 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;
|
||||
}
|
26
xposed/src/main/cpp/helper/jnihelper.cpp
Normal file
26
xposed/src/main/cpp/helper/jnihelper.cpp
Normal 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();
|
||||
}
|
||||
|
14
xposed/src/main/cpp/helper/jnihelper.h
Normal file
14
xposed/src/main/cpp/helper/jnihelper.h
Normal 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
|
27
xposed/src/main/cpp/helper/lsposed.h
Normal file
27
xposed/src/main/cpp/helper/lsposed.h
Normal 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
|
@ -1,5 +0,0 @@
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <sys/auxv.h>
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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(""", "\"")
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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>> {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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(),
|
||||
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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)
|
||||
|
@ -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"
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
@ -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")
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
@ -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,
|
||||
}
|
||||
|
@ -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",
|
||||
)
|
@ -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)")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
Reference in New Issue
Block a user