91 Commits

Author SHA1 Message Date
48b720bdd7 Merge remote-tracking branch 'origin/master' 2024-01-20 03:03:07 +08:00
2038d81ce8 Shamrock: 调整包拦截策略 2024-01-20 03:02:55 +08:00
81be383b5f revert 2024-01-18 20:05:18 +08:00
0fb88e3e44 give more love to 2024-01-18 16:51:44 +08:00
e92b04ad0f Shamrock: fix build error #201 2024-01-18 09:58:16 +08:00
160d1a11ac Shamrock: #201 2024-01-18 09:49:56 +08:00
b9cfe73eae Shamrock: fix #150 2024-01-17 04:53:07 +08:00
8d6d984849 Shamrock: 尝试修复重复Client连接 #187 2024-01-17 04:15:24 +08:00
25fe9fab37 Shamrock: fix 9.0.8 2024-01-17 03:52:49 +08:00
ba7058a838 add retryCnt 2024-01-14 16:24:37 +08:00
0858395e60 add retryCnt 2024-01-14 15:49:44 +08:00
fdd769d9ff should fix #176 2024-01-10 00:17:53 +08:00
f47ae69653 Merge pull request #193 from MrXiaoM/fix-group-ban
支持全员禁言通知事件
2024-01-07 15:13:43 +08:00
f311ae3797 Merge pull request #194 from MrXiaoM/fix-group-title-change
修复群头衔更改通知报错
2024-01-07 15:13:19 +08:00
5a941f889f add group-title-change notify busiId 2024-01-07 04:39:42 +08:00
69e50c6f93 fix group title change notice on 8.9.80 2024-01-07 04:38:45 +08:00
f7ac3a5d23 whole group ban notice support (go-cqhttp) 2024-01-07 03:52:29 +08:00
b6b54a805e more debug log when error 2024-01-07 03:31:49 +08:00
131f56a468 Shamrock: fix #191 2024-01-06 18:53:42 +08:00
e45e9e7fa0 Merge pull request #189 from super1207/patch-3
fix groupid get
2024-01-05 10:46:32 +08:00
cac0aad1f2 fix groupid get 2024-01-04 19:40:01 +08:00
2c1bd9e726 Shamrock: fix #186 2024-01-04 00:30:36 +08:00
2645e8f451 Merge remote-tracking branch 'origin/master' 2024-01-03 17:45:43 +08:00
e92c227bba Shamrock: 对QQ9支持,并尝试修复#181 2024-01-03 17:45:32 +08:00
a31fe92c0b feat: 获取频道列表接口 (TODO 状态 返回空数组) 2024-01-01 19:01:38 +08:00
70cb876439 Merge remote-tracking branch 'origin/master' 2024-01-01 19:00:48 +08:00
9aa4c37354 fix: WebSocket 部分操作未正常返回数据 (close #182) 2024-01-01 19:00:28 +08:00
d44150ea1a Shamrock: 在获取某些字段错误时,将字段值定义为0 #150 2024-01-01 00:42:47 +08:00
0360c81bee Shamrock: fix #173 2024-01-01 00:40:55 +08:00
cb904c1f1c Shamrock: fix #179 2024-01-01 00:38:44 +08:00
bc754db959 Merge remote-tracking branch 'origin/master' 2024-01-01 00:37:35 +08:00
8df799a6e4 Shamrock: fix #180 2024-01-01 00:37:12 +08:00
71dd9469ca Merge pull request #178 from super1207/patch-2
fix cq code parse
2023-12-27 20:00:49 +09:00
79788d2cdc fix cq code parse 2023-12-26 19:53:00 +08:00
c28c9981c4 Shamrock: 修复QQ收藏异常 2023-12-23 05:13:16 +08:00
8fadd0016a Shamrock: fix #171 #165 2023-12-23 05:06:37 +08:00
77504d68fd Shamrock: 支持获取QQ收藏的列表 /fav/get_item_list 2023-12-23 04:47:11 +08:00
1490450178 Shamrock: 支持获取指定QQ收藏的内容 /fav/get_item_content 2023-12-23 04:16:11 +08:00
88beaf8b6f Shamrock: 支持添加QQ收藏(图片) /fav/add_image_msg 2023-12-22 14:19:56 +08:00
df25b0bc76 Shamrock: 支持添加QQ收藏(文本) /fav/add_rich_media_msg 2023-12-22 03:08:46 +08:00
d07eea7766 Shamrock: Shamrock更新后自动重新启动 2023-12-22 02:26:39 +08:00
5000453002 Shamrock: 完成QQ收藏数据分析 2023-12-21 02:25:20 +08:00
35c82fcc51 Shamrock: fix #169 2023-12-21 00:55:56 +08:00
89a4912ed7 Shamrock: fix #166 2023-12-20 18:58:17 +08:00
aeabc66067 Shamrock: 兼容性正反向HTTP调整 2023-12-20 18:52:24 +08:00
ccbfc9a1e1 Shamrock: fix #159 2023-12-19 22:11:22 +08:00
31936feb98 Merge remote-tracking branch 'origin/master' 2023-12-19 22:10:03 +08:00
538db69754 Shamrock: fix #159 2023-12-19 22:09:23 +08:00
25ea5bd6a8 Merge pull request #162 from callng/master
`Shamrock`: Upgrade build tools
2023-12-19 22:07:32 +08:00
460cd84258 Shamrock: Upgrade build tools 2023-12-19 21:13:28 +08:00
3f9613c43c Shamrock: 输出心跳日志 2023-12-19 21:07:28 +08:00
34eccda233 Shamrock: Määritetty tunnus ei vaadi suuraakkosia 2023-12-19 01:21:08 +08:00
741d2c7a84 Shamrock: fix #117 2023-12-19 01:13:39 +08:00
6ee5ceb321 Shamrock: fix #155 2023-12-18 21:15:56 +08:00
6107ec6ffb Shamrock: fix #156 2023-12-17 11:40:05 +08:00
4ef014a8ac Shamrock: fix card event and kickme operation 2023-12-13 15:09:46 +08:00
135a7c2f56 Shamrock: 优化构建流程 2023-12-13 01:23:40 +08:00
420f11784d Shamrock: fix field error 2023-12-13 00:22:06 +08:00
951e7462c4 Shamrock: add shut_up_timestamp field to get_group_member_list 2023-12-13 00:18:57 +08:00
1b0550b5e1 readme: fix broken link 2023-12-12 17:17:31 +08:00
919c4a7d80 Shamrock: fix #149 again 2023-12-12 11:11:21 +08:00
dcb2b0a26f Shamrock: fix #149 2023-12-12 10:28:24 +08:00
d388e5df0c Shamrock: fix #149 2023-12-11 14:29:07 +08:00
5776524579 Shamrock: 好友请求系统消息 2023-12-10 18:40:33 +08:00
1d0a0731fb Shamrock: 修复群消息撤回事件群号获取错误 2023-12-10 01:57:38 +08:00
4dafa75944 Shamrock: 修复群禁言事件群号获取错误 #148 2023-12-10 01:21:38 +08:00
85cdc86b07 Merge pull request #144 from PisLuanyao/master
`Clover.cpp`: NoxAppPlayer里的鸡脚so
2023-12-10 01:09:21 +08:00
cd19426d1b Clover.cpp: NoxAppPlayer
稍微动了一下,方便看,希望没搞错
2023-12-09 19:14:22 +08:00
48b1b40e1c Clover.cpp: NoxAppPlayer
也许是无效的?
2023-12-09 18:56:00 +08:00
50d469cc45 Clover.cpp: NoxAppPlayer
夜神模拟器里的鸡脚so
2023-12-09 18:41:53 +08:00
37f74d5284 Shamrock: 优化检测逻辑 2023-12-09 16:18:30 +08:00
adb7b12c16 Shamrock: feat 群打卡 2023-12-08 21:52:35 +08:00
27791cc848 Shamrock: fix friend request type typo 2023-12-08 12:54:34 +08:00
bd45523e25 Shamrock: 修正被动WebSocket日志 2023-12-07 23:44:55 +08:00
c0f2242679 Shamrock: 修复防止系统打盹错误 2023-12-07 00:24:06 +08:00
c309a2b3ed Shamrock: fix 部分事件日志错误打印 2023-12-06 16:42:00 +08:00
48c9048a00 Merge branch 'master' of github.com:whitechi73/OpenShamrock 2023-12-06 15:53:11 +08:00
4d2f7a794b Shamrock: 补充群成员信息area、age和level字段 2023-12-06 15:53:01 +08:00
ab6e431872 Merge remote-tracking branch 'origin/master' 2023-12-06 09:44:20 +08:00
9423df2670 Shamrock: 修复回复类型id错误 2023-12-06 09:44:10 +08:00
a2b3e42eee Shamrock: fix #125 2023-12-06 00:52:57 +08:00
d9a045bbf0 Shamrock: 修复构建 2023-12-05 18:26:15 +08:00
42ca17339e Shamrock: 不处理发给自己的消息 2023-12-05 18:24:22 +08:00
003c4d4456 Shamrock: 修复群戳一戳丶群打卡获取的群号错误 2023-12-04 19:42:46 +08:00
ec39aa7bc3 Shamrock: fix #124 2023-12-04 15:07:33 +08:00
4e3870a512 Shamrock: fix #124 2023-12-04 12:58:53 +08:00
97534b01a6 Merge pull request #121 from Miuzarte/patch-1
go-cqhttp like card and nickname field
2023-12-03 12:43:16 +08:00
61bed61bfb go-cqhttp like card and nickname field 2023-12-03 02:15:17 +08:00
a0ff4782db Merge remote-tracking branch 'origin/master' 2023-12-02 23:55:57 +08:00
41dd1de8f8 Shamrock: try fix #120 2023-12-02 23:55:48 +08:00
fa6634d6af Shamrock: fix #111 and add group sign event 2023-12-02 22:59:37 +08:00
112 changed files with 3760 additions and 733 deletions

View File

@ -13,24 +13,38 @@ on:
jobs:
build:
name: Build APK
name: Build Shamrock
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: "temurin"
fetch-depth: 0
- name: Setup JDK 17
uses: actions/setup-java@v4.0.0
with:
java-version: 17
cache: 'gradle'
distribution: "adopt"
- name: Setup cmake
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "cmake;3.22.1" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "sdk.dir=${ANDROID_HOME}" > local.properties
- name: Cache Gradle Dependencies
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
!~/.gradle/caches/build-cache-*
key: gradle-deps-core-${{ hashFiles('**/build.gradle.kts') }}
restore-keys: gradle-deps
- name: Setup Gradle
uses: gradle/gradle-build-action@v2.9.0
- name: Cache Gradle Build
uses: actions/cache@v3
with:
path: |
~/.gradle/caches/build-cache-*
~/.gradle/buildOutputCleanup/cache.properties
key: gradle-builds-core-${{ github.sha }}
restore-keys: gradle-builds
- name: Build with Gradle
run: |
@ -46,42 +60,41 @@ jobs:
KEY_ALIAS: ${{ secrets.SIGN_ALIAS }}
KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }}
- name: Install aapt
run: sudo apt-get update && sudo apt-get install -y aapt
- name: Set Shamrock Version
run: |
apk_file=${{ env.APK_FILE_ALL }}
apk_dump=$(aapt dump badging "$apk_file")
version_name=$(sed -n "s/.*versionName='\([^']*\)'.*/\1/p" <<< "$apk_dump")
echo "SHAMROCK_VERSION=$version_name" >> $GITHUB_ENV
version_name_all=$(basename -s .apk "${{ env.APK_FILE_ALL }}")
version_name_arm64=$(basename -s .apk "${{ env.APK_FILE_ARM64 }}")
version_name_x86_64=$(basename -s .apk "${{ env.APK_FILE_X86_64 }}")
echo "SHAMROCK_VERSION_ALL=$version_name_all" >> $GITHUB_ENV
echo "SHAMROCK_VERSION_ARM64=$version_name_arm64" >> $GITHUB_ENV
echo "SHAMROCK_VERSION_x86_64=$version_name_x86_64" >> $GITHUB_ENV
- name: Show Artifacts SHA256
run: |
echo "### Build Success :rocket:" >> $GITHUB_STEP_SUMMARY
echo "|ABI|SHA256|" >> $GITHUB_STEP_SUMMARY
echo "|:--------:|:----------|" >> $GITHUB_STEP_SUMMARY
all=($(sha256sum ${{ env.APK_FILE_ALL }}))
all=($(sha256sum "${{ env.APK_FILE_ALL }}"))
echo "|all|$all" >> $GITHUB_STEP_SUMMARY
arm64=($(sha256sum ${{ env.APK_FILE_ARM64 }}))
arm64=($(sha256sum "${{ env.APK_FILE_ARM64 }}"))
echo "|arm64|$arm64" >> $GITHUB_STEP_SUMMARY
x86_64=($(sha256sum ${{ env.APK_FILE_X86_64 }}))
x86_64=($(sha256sum "${{ env.APK_FILE_X86_64 }}"))
echo "|x86_64|$x86_64" >> $GITHUB_STEP_SUMMARY
- name: Upload ALL APK RELEASE
uses: actions/upload-artifact@v3
with:
name: Shamrock-v${{ env.SHAMROCK_VERSION }}-all
path: ${{ env.APK_FILE_ALL }}
name: "${{ env.SHAMROCK_VERSION_ALL }}"
path: "${{ env.APK_FILE_ALL }}"
- name: Upload ARM64 APK RELEASE
uses: actions/upload-artifact@v3
with:
name: Shamrock-v${{ env.SHAMROCK_VERSION }}-arm64
path: ${{ env.APK_FILE_ARM64 }}
name: "${{ env.SHAMROCK_VERSION_ARM64 }}"
path: "${{ env.APK_FILE_ARM64 }}"
- name: Upload X86_64 APK RELEASE
uses: actions/upload-artifact@v3
with:
name: Shamrock-v${{ env.SHAMROCK_VERSION }}-x86_64
path: ${{ env.APK_FILE_X86_64 }}
name: "${{ env.SHAMROCK_VERSION_x86_64 }}"
path: "${{ env.APK_FILE_X86_64 }}"

View File

@ -1,57 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '24 7 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
timeout-minutes: 360
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'cpp', 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3.6.0
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Setup JDK 17
uses: actions/setup-java@v3.12.0
with:
distribution: "temurin"
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@v2.8.0
- name: Setup cmake
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "cmake;3.22.1" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "sdk.dir=${ANDROID_HOME}" > local.properties
- name: Build
run: |
chmod +x ./gradlew
./gradlew :app:assembleAppRelease
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@ -1,73 +0,0 @@
name: Shamrock Release
on:
push:
tags:
- 'v*'
jobs:
build:
name: Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: "temurin"
java-version: 17
cache: 'gradle'
- name: Setup cmake
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "cmake;3.22.1" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "sdk.dir=${ANDROID_HOME}" > local.properties
- name: Setup Gradle
uses: gradle/gradle-build-action@v2.9.0
- name: Build with Gradle
run: |
echo ${{ secrets.SIGN_KEYSTORE_BASE64 }} | base64 -d > keystore.jks
chmod +x ./gradlew
./gradlew :app:assembleRelease --build-cache --parallel --daemon --warning-mode all --stacktrace
echo "APK_FILE_ALL=$(find app/build/outputs/apk/app/release -name '*.apk')" >> $GITHUB_ENV
echo "APK_FILE_ARM64=$(find app/build/outputs/apk/arm64/release -name '*.apk')" >> $GITHUB_ENV
echo "APK_FILE_X86_64=$(find app/build/outputs/apk/x64/release -name '*.apk')" >> $GITHUB_ENV
env:
KEYSTORE_PATH: "../keystore.jks"
KEYSTORE_PASSWORD: ${{ secrets.SIGN_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.SIGN_ALIAS }}
KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }}
- name: Install aapt
run: sudo apt-get update && sudo apt-get install -y aapt
- name: Set Shamrock Version
run: |
apk_file=${{ env.APK_FILE_ALL }}
apk_dump=$(aapt dump badging "$apk_file")
version_name=$(sed -n "s/.*versionName='\([^']*\)'.*/\1/p" <<< "$apk_dump")
echo "SHAMROCK_VERSION=$version_name" >> $GITHUB_ENV
- name: Show Artifacts SHA256
run: |
echo "### Build Success :rocket:" >> $GITHUB_STEP_SUMMARY
echo "|ABI|SHA256|" >> $GITHUB_STEP_SUMMARY
echo "|:--------:|:----------|" >> $GITHUB_STEP_SUMMARY
all=($(sha256sum ${{ env.APK_FILE_ALL }}))
echo "|all|$all" >> $GITHUB_STEP_SUMMARY
arm64=($(sha256sum ${{ env.APK_FILE_ARM64 }}))
echo "|arm64|$arm64" >> $GITHUB_STEP_SUMMARY
x86_64=($(sha256sum ${{ env.APK_FILE_X86_64 }}))
echo "|x86_64|$x86_64" >> $GITHUB_STEP_SUMMARY
- name: Release
uses: marvinpinto/action-automatic-releases@latest
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
automatic_release_tag: Shamrock v${{ env.SHAMROCK_VERSION }}
files: |
${{ env.APK_FILE_ALL }}
${{ env.APK_FILE_ARM64 }}
${{ env.APK_FILE_X86_64 }}

View File

@ -16,10 +16,11 @@
## 简介
☘ 基于 Xposed 实现 OneBot 标准的 QQ 机器人框架,原作者[**fuqiuluo**](https://github.com/fuqiuluo)已脱离开发接下来由白池接手哦本项目为OpenShamrock不会有任何收费行为欢迎大家的加入
☘ 基于 Lsposed(**Non**-Riru) 实现 OneBot 标准的 QQ 机器人框架,原作者[**fuqiuluo**](https://github.com/fuqiuluo)已脱离开发接下来由白池接手哦本项目为OpenShamrock不会有任何收费行为欢迎大家的加入
> 本项目仅提供学习与交流用途请在24小时内删除。
> 本项目目的是研究 Xposed 和 LSPosed 框架的使用。 Epic 框架开发相关知识。
> Riru可能导致封禁请减少使用。
> 如有违反法律,请联系删除。
> 请勿在任何平台宣传,宣扬,转发本项目,请勿恶意修改企业安装包造成相关企业产生损失,如有违背,必将追责到底。
> 官方论坛,[点我直达](https://forum.libfekit.so/)
@ -94,7 +95,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
[docs-link]: https://whitechi73.github.io/OpenShamrock/
[hook-system]: https://github.com/whitechi73/OpenShamrock/blob/master/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/FuckAMS.kt
[hook-system]: https://github.com/whitechi73/OpenShamrock/blob/master/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/KeepAlive.kt
[voice-support]: https://whitechi73.github.io/OpenShamrock/advanced/voice.html

View File

@ -1,12 +1,5 @@
import com.android.build.api.dsl.ApplicationExtension
fun gitCommitHash(): String {
val builder = ProcessBuilder("git", "rev-parse", "--short", "HEAD")
val process = builder.start()
val reader = process.inputReader()
val hash = reader.readText().trim()
return if (hash.isNotEmpty()) ".$hash" else ""
}
import java.io.ByteArrayOutputStream
plugins {
id("com.android.application")
@ -17,19 +10,20 @@ plugins {
android {
namespace = "moe.fuqiuluo.shamrock"
ndkVersion = "25.1.8937393"
compileSdk = 33
compileSdk = 34
defaultConfig {
applicationId = "moe.fuqiuluo.shamrock"
minSdk = 24
targetSdk = 33
versionCode = (System.currentTimeMillis() / 1000).toInt()
versionName = "1.0.7-dev" + gitCommitHash()
minSdk = 27
targetSdk = 34
versionCode = getVersionCode()
versionName = "1.0.7" + ".r${getGitCommitCount()}." + getVersionName()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
@Suppress("UnstableApiUsage")
externalNativeBuild {
cmake {
cppFlags += ""
@ -50,8 +44,7 @@ android {
android.applicationVariants.all {
outputs.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach {
val abi = it.outputFileName.split("-")[1].split(".apk")[0]
val abiName = when (abi) {
val abiName = when (val abi = it.outputFileName.split("-")[1].split(".apk")[0]) {
"app" -> "all"
"x64" -> "x86_64"
else -> abi
@ -69,6 +62,7 @@ android {
println("Full architecture and full compilation.")
abiFilters.add("arm64-v8a")
abiFilters.add("x86_64")
abiFilters.add("x86")
}
}
create("arm64") {
@ -133,6 +127,11 @@ android {
}
configureAppSigningConfigsForRelease(project)
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
}
fun configureAppSigningConfigsForRelease(project: Project) {
@ -154,10 +153,39 @@ fun configureAppSigningConfigsForRelease(project: Project) {
release {
signingConfig = signingConfigs.findByName("release")
}
debug {
signingConfig = signingConfigs.findByName("release")
}
}
}
}
fun getGitCommitCount(): Int {
val out = ByteArrayOutputStream()
exec {
commandLine("git", "rev-list", "--count", "HEAD")
standardOutput = out
}
return out.toString().trim().toInt()
}
fun getGitCommitHash(): String {
val out = ByteArrayOutputStream()
exec {
commandLine("git", "rev-parse", "--short", "HEAD")
standardOutput = out
}
return out.toString().trim()
}
fun getVersionCode(): Int {
return (System.currentTimeMillis() / 1000L).toInt()
}
fun getVersionName(): String {
return getGitCommitHash()
}
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")

View File

@ -3,10 +3,6 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name=".app.MyApplication"
@ -19,7 +15,6 @@
android:theme="@style/Theme.Shamrock"
android:zygotePreloadName="@string/app_name"
android:multiArch="true"
android:extractNativeLibs="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"

View File

@ -9,52 +9,61 @@ inline void replace_string(std::string& str, const std::string& from, const std:
}
}
inline int utf8_next_len(const std::string& str, size_t offset)
{
uint8_t c = (uint8_t)str[offset];
if (c >= 0xFC)
return 6;
else if (c >= 0xF8)
return 5;
else if (c >= 0xF0)
return 4;
else if (c >= 0xE0)
return 3;
else if (c >= 0xC0)
return 2;
else if (c > 0x00)
return 1;
else
return 0;
}
void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::string, std::string>>& dest) {
std::string cache;
bool is_start = false;
std::string key_tmp;
std::unordered_map<std::string, std::string> kv;
for(int i = 0; i < code.size(); i++) {
auto c = code[i];
if (c == '[') {
for(size_t i = 0; i < code.size(); i++) {
int utf8_char_len = utf8_next_len(code, i);
if(utf8_char_len == 0) {
continue;
}
std::string_view c(&code[i],utf8_char_len);
if (c == "[") {
if (is_start) {
throw illegal_code();
} else {
if (!cache.empty()) {
std::unordered_map<std::string, std::string> kv;
replace_string(cache, "&#91;", "[");
replace_string(cache, "&#93;", "]");
replace_string(cache, "&amp;", "&");
kv.emplace("_type", "text");
kv.emplace("text", cache);
dest.push_back(kv);
cache.clear();
}
auto c1 = code[i + 1];
if (c1 == 'C') {
i++;
auto c2 = code[i + 1];
if(c2 == 'Q') {
i++;
auto c3 = code[i + 1];
if (c3 == ':') {
i++;
is_start = true;
} else {
cache += c;
cache += c1;
cache += c2;
continue;
}
} else {
cache += c;
cache += c1;
continue;
}
} else {
std::string_view cq_flag(&code[i],4);
if(cq_flag == "[CQ:"){
is_start = true;
i += 3;
}else{
cache += c;
continue;
}
}
}
else if (c == '=') {
else if (c == "=") {
if (is_start) {
if (cache.empty()) {
throw illegal_code();
@ -70,17 +79,17 @@ void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::
cache += c;
}
}
else if (c == ',') {
else if (c == ",") {
if (is_start) {
if (kv.count("_type") == 0 && !cache.empty()) {
kv.emplace("_type", cache);
cache.clear();
} else {
if (!key_tmp.empty()) {
replace_string(cache, "&amp;", "&");
replace_string(cache, "&#91;", "[");
replace_string(cache, "&#93;", "]");
replace_string(cache, "&#44;", ",");
replace_string(cache, "&amp;", "&");
kv.emplace(key_tmp, cache);
cache.clear();
key_tmp.clear();
@ -90,14 +99,14 @@ void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::
cache += c;
}
}
else if (c == ']') {
else if (c == "]") {
if (is_start) {
if (!cache.empty()) {
if (!key_tmp.empty()) {
replace_string(cache, "&amp;", "&");
replace_string(cache, "&#91;", "[");
replace_string(cache, "&#93;", "]");
replace_string(cache, "&#44;", ",");
replace_string(cache, "&amp;", "&");
kv.emplace(key_tmp, cache);
} else {
kv.emplace("_type", cache);
@ -114,10 +123,14 @@ void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::
}
else {
cache += c;
i += (utf8_char_len - 1);
}
}
if (!cache.empty()) {
std::unordered_map<std::string, std::string> kv;
replace_string(cache, "&#91;", "[");
replace_string(cache, "&#93;", "]");
replace_string(cache, "&amp;", "&");
kv.emplace("_type", "text");
kv.emplace("text", cache);
dest.push_back(kv);

View File

@ -37,6 +37,26 @@ Java_moe_fuqiuluo_shamrock_utils_MD5_genFileMd5Hex(JNIEnv *env, jobject thiz, js
return env->NewStringUTF(md5Hex.c_str());
}
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_moe_fuqiuluo_shamrock_utils_MD5_genFileMd5(JNIEnv *env, jobject thiz, jstring file_path) {
auto cPathStr = env->GetStringUTFChars(file_path, nullptr);
std::filesystem::path filePath(cPathStr);
if (!std::filesystem::exists(filePath)) {
jclass exClass = env->FindClass("java/io/FileNotFoundException");
env->ThrowNew(exClass, "目标文件不存在");
env->DeleteLocalRef(exClass);
return nullptr;
}
auto file = std::ifstream(filePath.c_str(), std::ios::binary);
MD5 md5;
md5.update(file);
auto md5Bytes = md5.digest();
auto jByteArray = env->NewByteArray(16);
env->SetByteArrayRegion(jByteArray, 0, 16, reinterpret_cast<const jbyte*>(md5Bytes));
return jByteArray;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_moe_fuqiuluo_shamrock_utils_MD5_getMd5Hex(JNIEnv *env, jobject thiz, jbyteArray bytes) {

View File

@ -52,6 +52,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
@ -120,7 +121,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
@Suppress("LocalVariableName") val LocalString = LocalString.init()
if (!AppRuntime.isInit) {
AppRuntime.state = remember {
@ -147,7 +148,7 @@ private fun AppMainView() {
AppRuntime.requestCount = remember { mutableIntStateOf(0) }
AppRuntime.isInit = false
AppRuntime.isInit = true
}
val ctx = LocalContext.current

View File

@ -279,19 +279,19 @@ object ShamrockConfig {
return preferences.getBoolean("enable_self_msg", false)
}
fun enableSyncMsgAsSentMsg(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("enable_sync_msg_as_sent_msg", false)
}
fun setEnableSelfMsg(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("enable_self_msg", v).apply()
}
fun isEchoNumber(ctx: Context): Boolean {
fun setEnableSyncMsgAsSentMsg(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("echo_number", false)
}
fun setEchoNumber(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("echo_number", v).apply()
preferences.edit().putBoolean("enable_sync_msg_as_sent_msg", v).apply()
}
fun getConfigMap(ctx: Context): Map<String, Any?> {
@ -321,6 +321,7 @@ object ShamrockConfig {
"echo_number" to preferences.getBoolean("echo_number", false),
"shell" to preferences.getBoolean("shell", false),
"alive_reply" to preferences.getBoolean("alive_reply", false),
"enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false),
)
}

View File

@ -278,9 +278,7 @@ private fun APIInfoCard(
text = authToken,
hint = "请填写鉴权token",
error = "输入的参数不合法",
checker = {
it.length in 0 .. 36
},
checker = { true },
confirm = {
ShamrockConfig.setToken(ctx, authToken.value)
AppRuntime.log("设置鉴权Token为[${authToken.value}]。")

View File

@ -275,6 +275,17 @@ fun LabFragment() {
return@Function true
}
Function(
title = "同步消息推送类型异换",
desc = "推送来自同号异设备消息,将同步消息作为自发消息推送。",
descColor = it,
isSwitch = ShamrockConfig.enableSyncMsgAsSentMsg(ctx)
) {
ShamrockConfig.setEnableSyncMsgAsSentMsg(ctx, it)
ShamrockConfig.pushUpdate(ctx)
return@Function true
}
/*
Function(
title = "使用纯数字ECHO",

View File

@ -8,6 +8,7 @@ import android.os.Bundle
import androidx.core.content.ContextCompat.startActivity
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.parameter
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
@ -58,7 +59,8 @@ object DashboardInitializer {
url("http://127.0.0.1:$servicePort/get_account_info")
val token = ShamrockConfig.getToken(context)
if (token.isNotBlank()) {
header("Authorization", "Bearer $token")
//header("Authorization", "Bearer $token")
parameter("access_token", token)
}
}.let {
if (it.status == HttpStatusCode.OK) {

View File

@ -6,7 +6,9 @@ package moe.fuqiuluo.shamrock.ui.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import moe.fuqiuluo.shamrock.R
private val LocalStringDefault = Default()
@ -164,4 +166,14 @@ open class VarString(
var persistentText: String,
var persistentTextDesc: String
)
) {
private var inited = false
@Composable
fun init(): VarString {
if (inited) return this
inited = true
return this
}
}

View File

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.1.2" apply false
id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
id("com.android.library") version "8.1.2" apply false
id("com.android.library") version "8.2.0" apply false
//id("io.realm.kotlin") version "1.11.0" apply false
}

Binary file not shown.

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStorePath=wrapper/dists

3
gradlew vendored
View File

@ -83,7 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

View File

@ -5,10 +5,10 @@ plugins {
android {
namespace = "moe.fuqiuluo.qqinterface"
compileSdk = 33
compileSdk = 34
defaultConfig {
minSdk = 24
minSdk = 27
consumerProguardFiles("consumer-rules.pro")
}

View File

@ -0,0 +1,7 @@
package com.tencent.mobileqq.filemanager.api;
import com.tencent.mobileqq.qroute.QRouteApi;
public interface IFileManagerUtil extends QRouteApi {
byte[] getSHA(String str);
}

View File

@ -21,6 +21,10 @@ public abstract class PBField<T> {
return new PBBytesField(byteStringMicro, false);
}
public static PBFloatField initFloat(float paramFloat) {
return new PBFloatField(paramFloat, false);
}
public static PBBoolField initBool(boolean z) {
return new PBBoolField(z, false);
}

View File

@ -0,0 +1,13 @@
package com.tencent.mobileqq.pb;
public class PBFloatField extends PBPrimitiveField<Float> {
public PBFloatField(float i2, boolean z) {
}
public int get() {
return 0;
}
public void set(int i2) {
}
}

View File

@ -0,0 +1,51 @@
package com.tencent.mobileqq.transfile;
public class HttpNetReq extends NetReq {
public static final int HTTP_GET = 0;
public static final int HTTP_POST = 1;
public IFlowDecoder decoder;
public DnsParseCallback mDnsParseCallback;
public boolean mHaveIpConnect;
public String mHostForHttpsVerify;
public int mHttpMethod;
public boolean mIsHostIP;
public boolean mIsHttps;
public boolean mIsPreStructPic;
public boolean mIsSync;
public boolean mNeedIpConnect;
public boolean mNeedNotReferer;
public boolean mNeedRedirectCallback;
public String mReqUrl;
public TimeoutParam mTimeoutParam;
public boolean mUseCmwapConnectionTypeFromDpc;
public String[] mWhiteListContentType;
public interface DecoderState {
public static final int STATE_END = 2;
public static final int STATE_INIT = 0;
public static final int STATE_RUNNING = 1;
}
public interface DnsParseCallback {
void end();
void start();
}
public interface IFlowDecoder {
byte[] decode(byte[] bArr);
boolean isFinish();
void reset();
}
public HttpNetReq() {
this.mHttpMethod = 0;
this.mNeedIpConnect = false;
this.mHaveIpConnect = false;
this.mNeedRedirectCallback = false;
this.mUseCmwapConnectionTypeFromDpc = false;
this.mNeedNotReferer = false;
}
}

View File

@ -0,0 +1,7 @@
package com.tencent.mobileqq.transfile;
public interface INetEngineListener {
void onResp(NetResp netResp);
void onUpdateProgeress(NetReq netReq, long j2, long j3);
}

View File

@ -0,0 +1,5 @@
package com.tencent.mobileqq.transfile;
public interface NetFailedListener {
void onFailed(NetResp netResp);
}

View File

@ -0,0 +1,89 @@
package com.tencent.mobileqq.transfile;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
public class NetReq {
public static final int PRIOTY_HIGH = 0;
public static final int PRIOTY_LOW = 2;
public static final int PRIOTY_NORMAL = 1;
public boolean bAcceptNegativeContentLength;
public int mBusiProtoType;
public INetEngineListener mCallback;
public boolean mCanPrintUrl;
public int mContinuErrorLimit;
public int mContinueConnReusedErrorLimit;
public long mDelayTime;
public long mEndDownOffset;
public long mExcuteTimeLimit;
public NetFailedListener mFailedListener;
public int mFileType;
public HostParseToIp mHostParseToIp;
public boolean mIsNetChgAsError;
public boolean mIsRenameInEngine;
public String mKey;
public String mMsgId;
public String mOutPath;
public OutputStream mOutStream;
public int mPrioty;
public Object mPrivate;
public HashMap<String, String> mReqProperties;
public NetResp mResp;
public byte[] mSendData;
public List<ServerAddr> mServerList;
public long mStartDownOffset;
public boolean mSupportBreakResume;
public String mTempPath;
public boolean mUseByteArrayPool;
public boolean mUseRaf;
private Object mUserData;
public long taskStartTime;
public interface HostParseToIp {
List<ServerAddr> getIpByHost(String str);
}
public NetReq() {
this.mStartDownOffset = 0L;
this.mEndDownOffset = 0L;
this.mIsRenameInEngine = true;
this.mDelayTime = 0L;
this.mExcuteTimeLimit = 480000L;
this.mContinuErrorLimit = 8;
this.mContinueConnReusedErrorLimit = 5;
this.mIsNetChgAsError = false;
this.mPrioty = 1;
this.mResp = null;
this.mCanPrintUrl = true;
this.bAcceptNegativeContentLength = true;
this.mUseByteArrayPool = false;
this.mKey = null;
this.taskStartTime = -1L;
this.mReqProperties = new HashMap<>();
}
public long getTaskCostTime() {
return System.currentTimeMillis() - taskStartTime;
}
public synchronized Object getUserData() {
return mUserData;
}
public boolean isWriteToFile() {
return this.mOutPath != null;
}
public boolean isWriteToStream() {
return this.mOutStream != null;
}
public boolean saveRecvDataInTransLayer() {
return this.mOutPath != null || this.mOutStream != null;
}
public synchronized void setUserData(Object obj) {
this.mUserData = obj;
}
}

View File

@ -0,0 +1,72 @@
package com.tencent.mobileqq.transfile;
import java.util.HashMap;
public class NetResp {
public static final String KEY_FIRST_USE_IP = "firstserverip";
public static final String KEY_RAW_REQ_HTTP_HEADER = "param_reqHeader";
public static final String KEY_RAW_RESP_HTTP_HEADER = "param_rspHeader";
public static final String KEY_REASON = "netresp_param_reason";
public static final String KEY_USE_SERVER_IP = "serverip";
public static final String KEY_USE_URL = "param_url";
public static final int RESULT_DOWNLOADING = 3;
public static final int RESULT_FAIL = 1;
public static final int RESULT_NOT_SET = 2;
public static final int RESULT_OK = 0;
public long inQueueCost;
public long mConsumeTime;
public int mErrCode;
public String mErrDesc;
public int mHttpCode;
public long mLastReqStartTime;
public int mRedirectCount;
public long mRedirectTime;
public NetReq mReq;
public byte[] mRespData;
public HashMap<String, String> mRespProperties;
public int mResult;
public long mTotalBlockLen;
public long mTotalFileLen;
public int mTryTime;
public long mWrittenBlockLen;
public long reqCost;
public NetResp(NetReq netReq) {
this.mResult = 2;
this.mTotalFileLen = 0L;
this.mTotalBlockLen = 0L;
this.mWrittenBlockLen = 0L;
this.mConsumeTime = 0L;
this.mTryTime = 0;
this.mRespProperties = new HashMap<>();
this.mRedirectCount = 0;
this.mRedirectTime = 0L;
this.reqCost = 0L;
this.inQueueCost = 0L;
this.mReq = netReq;
this.mLastReqStartTime = System.currentTimeMillis();
}
public void reset() {
this.mResult = 2;
this.mErrCode = 0;
this.mErrDesc = "";
this.mHttpCode = 0;
this.mTotalFileLen = 0L;
this.mTotalBlockLen = 0L;
this.mWrittenBlockLen = 0L;
this.mConsumeTime = 0L;
this.mTryTime = 0;
this.mRespData = null;
this.mRespProperties.clear();
}
public void setResult(int i2, int i3, String str, HashMap<String, String> hashMap) {
this.mResult = i2;
this.mErrCode = i3;
this.mErrDesc = str;
if (hashMap != null) {
this.mRespProperties.putAll(hashMap);
}
}
}

View File

@ -0,0 +1,72 @@
package com.tencent.mobileqq.transfile;
public class TimeoutParam {
public static final int TIMEOUT_STEP = 2000;
private int connectTimeoutBias;
public int connectTimeoutFor2G;
public int connectTimeoutFor3G;
public int connectTimeoutForWifi;
public int readTimeoutFor2G;
public int readTimeoutFor3G;
public int readTimeoutForWifi;
public TimeoutParam() {
this.readTimeoutFor2G = 40000;
this.readTimeoutFor3G = 30000;
this.readTimeoutForWifi = 20000;
this.connectTimeoutFor2G = 20000;
this.connectTimeoutFor3G = 15000;
this.connectTimeoutForWifi = 10000;
this.connectTimeoutBias = 0;
}
public void adjustConnectTimeout(int i2) {
this.connectTimeoutBias = i2 * 2000;
}
public int getConnectTimeout(int i2) {
if (i2 != 1) {
if (i2 == 3) {
return this.connectTimeoutFor3G + this.connectTimeoutBias;
}
if (i2 != 4 && i2 != 5) {
return this.connectTimeoutFor2G + this.connectTimeoutBias;
}
}
return this.connectTimeoutForWifi + this.connectTimeoutBias;
}
public int getReadTimeout(int i2) {
if (i2 != 1) {
if (i2 == 3) {
return this.readTimeoutFor3G;
}
if (i2 != 4 && i2 != 5) {
return this.readTimeoutFor2G;
}
}
return this.readTimeoutForWifi;
}
//public TimeoutParam clone() {
// IPatchRedirector iPatchRedirector = $redirector_;
// return (iPatchRedirector == null || !iPatchRedirector.hasPatch((short) 4)) ? new TimeoutParam(this) : (TimeoutParam) iPatchRedirector.redirect((short) 4, (Object) this);
//}
TimeoutParam(TimeoutParam timeoutParam) {
this.readTimeoutFor2G = 40000;
this.readTimeoutFor3G = 30000;
this.readTimeoutForWifi = 20000;
this.connectTimeoutFor2G = 20000;
this.connectTimeoutFor3G = 15000;
this.connectTimeoutForWifi = 10000;
this.connectTimeoutBias = 0;
this.readTimeoutFor2G = timeoutParam.readTimeoutFor2G;
this.readTimeoutFor3G = timeoutParam.readTimeoutFor3G;
this.readTimeoutForWifi = timeoutParam.readTimeoutForWifi;
this.connectTimeoutFor2G = timeoutParam.connectTimeoutFor2G;
this.connectTimeoutFor3G = timeoutParam.connectTimeoutFor3G;
this.connectTimeoutForWifi = timeoutParam.connectTimeoutForWifi;
}
}

View File

@ -0,0 +1,15 @@
package com.tencent.mobileqq.transfile.api;
import com.tencent.mobileqq.transfile.NetReq;
import com.tencent.mobileqq.transfile.NetResp;
import mqq.app.api.IRuntimeService;
@Deprecated
public interface IHttpEngineService extends IRuntimeService {
void cancelReq(NetReq netReq);
void sendReq(NetReq netReq);
NetResp sendReqSync(NetReq netReq);
}

View File

@ -0,0 +1,14 @@
package com.tencent.mobileqq.transfile.api;
import com.tencent.mobileqq.qroute.QRouteApi;
import com.tencent.mobileqq.transfile.NetReq;
public interface IOldHttpEngineProcessor extends QRouteApi {
//void cancelMsg(HttpMsg httpMsg);
void cancelReq(NetReq netReq);
//int sendMsg(HttpMsg httpMsg);
void sendReq(NetReq netReq);
}

View File

@ -18,7 +18,6 @@ public final class MarkdownElement {
}
public MarkdownElement(String str) {
this.content = "";
this.content = str;
}
}

View File

@ -3,6 +3,7 @@ package mqq.manager;
import android.content.Context;
import oicq.wlogin_sdk.request.Ticket;
import oicq.wlogin_sdk.request.WtTicketPromise;
public interface TicketManager extends Manager {
String getA2(String uin);
@ -15,6 +16,8 @@ public interface TicketManager extends Manager {
String getPskey(String uin, String domain);
Ticket getPskey(String str, long j2, String[] strArr, WtTicketPromise wtTicketPromise);
String getPt4Token(String uin, String domain);
String getSkey(String uin); // 假的Skey

View File

@ -0,0 +1,11 @@
package oicq.wlogin_sdk.request;
import oicq.wlogin_sdk.tools.ErrMsg;
public interface WtTicketPromise {
void Done(Ticket ticket);
void Failed(ErrMsg errMsg);
void Timeout(ErrMsg errMsg);
}

View File

@ -0,0 +1,118 @@
package oicq.wlogin_sdk.tools;
import android.os.Parcel;
import android.os.Parcelable;
public class ErrMsg implements Cloneable, Parcelable {
public static final Parcelable.Creator<ErrMsg> CREATOR = new Parcelable.Creator<ErrMsg>() { // from class: oicq.wlogin_sdk.tools.ErrMsg.1
@Override // android.os.Parcelable.Creator
public ErrMsg createFromParcel(Parcel parcel) {
return new ErrMsg(parcel);
}
@Override // android.os.Parcelable.Creator
public ErrMsg[] newArray(int i2) {
return new ErrMsg[i2];
}
};
private String message;
private String otherinfo;
private String title;
private int type;
private int version;
public ErrMsg() {
this.version = 0;
this.type = 0;
//this.title = InternationMsg.a(InternationMsg.MSG_TYPE.MSG_0);
//this.message = InternationMsg.a(InternationMsg.MSG_TYPE.MSG_1);
this.otherinfo = "";
}
public ErrMsg(int i2, int i3, String str, String str2, String str3) {
this.version = i2;
this.type = i3;
this.title = str;
this.message = str2;
this.otherinfo = str3;
}
private ErrMsg(Parcel parcel) {
readFromParcel(parcel);
}
@Override // android.os.Parcelable
public int describeContents() {
return 0;
}
public String getMessage() {
return this.message;
}
public String getOtherinfo() {
return this.otherinfo;
}
public String getTitle() {
return this.title;
}
public int getType() {
return this.type;
}
public int getVersion() {
return this.version;
}
public void readFromParcel(Parcel parcel) {
this.version = parcel.readInt();
this.type = parcel.readInt();
this.title = parcel.readString();
this.message = parcel.readString();
this.otherinfo = parcel.readString();
}
public void setMessage(String str) {
this.message = str;
}
public void setOtherinfo(String str) {
this.otherinfo = str;
}
public void setTitle(String str) {
this.title = str;
}
public void setType(int i2) {
this.type = i2;
}
public void setVersion(int i2) {
this.version = i2;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("(");
int i2 = this.version;
sb.append(i2 < 0 ? Integer.valueOf(i2) : Integer.toString(i2));
sb.append(")(");
int i3 = this.type;
sb.append(i3 < 0 ? Integer.valueOf(i3) : Integer.toString(i3));
sb.append(")[");
String sb2 = sb.toString();
return sb2 + this.title + "]" + this.message + "[" + this.otherinfo + "]";
}
@Override // android.os.Parcelable
public void writeToParcel(Parcel parcel, int i2) {
parcel.writeInt(this.version);
parcel.writeInt(this.type);
parcel.writeString(this.title);
parcel.writeString(this.message);
parcel.writeString(this.otherinfo);
}
}

View File

@ -0,0 +1,326 @@
package tencent.im.group;
import com.tencent.mobileqq.pb.ByteStringMicro;
import com.tencent.mobileqq.pb.MessageMicro;
import com.tencent.mobileqq.pb.PBBoolField;
import com.tencent.mobileqq.pb.PBBytesField;
import com.tencent.mobileqq.pb.PBField;
import com.tencent.mobileqq.pb.PBRepeatField;
import com.tencent.mobileqq.pb.PBRepeatMessageField;
import com.tencent.mobileqq.pb.PBStringField;
import com.tencent.mobileqq.pb.PBUInt32Field;
import com.tencent.mobileqq.pb.PBUInt64Field;
public class group_member_info {
public static class CustomEntry extends MessageMicro<tencent.im.group.group_member_info.CustomEntry> {
static final FieldMap __fieldMap__;
public final PBBoolField bool_clicked;
public final PBBytesField str_name;
public final PBBytesField str_url;
public final PBBytesField str_value;
public final PBUInt64Field uint64_report_id;
static {
ByteStringMicro byteStringMicro = ByteStringMicro.EMPTY;
Boolean bool = Boolean.FALSE;
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 24, 34, 40 }, new String[] { "str_name", "str_value", "bool_clicked", "str_url", "uint64_report_id" }, new Object[] { byteStringMicro, byteStringMicro, bool, byteStringMicro, Long.valueOf(0L) }, tencent.im.group.group_member_info.CustomEntry.class);
}
public CustomEntry() {
ByteStringMicro byteStringMicro = ByteStringMicro.EMPTY;
this.str_name = PBField.initBytes(byteStringMicro);
this.str_value = PBField.initBytes(byteStringMicro);
this.bool_clicked = PBField.initBool(false);
this.str_url = PBField.initBytes(byteStringMicro);
this.uint64_report_id = PBField.initUInt64(0L);
}
}
public static class ErrorInfo extends MessageMicro<tencent.im.group.group_member_info.ErrorInfo> {
static final FieldMap __fieldMap__;
public final PBUInt32Field error_code = PBField.initUInt32(0);
public final PBBytesField error_desc = PBField.initBytes(ByteStringMicro.EMPTY);
static {
ByteStringMicro byteStringMicro = ByteStringMicro.EMPTY;
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 18 }, new String[] { "error_code", "error_desc" }, new Object[] { Integer.valueOf(0), byteStringMicro }, tencent.im.group.group_member_info.ErrorInfo.class);
}
}
public static class FlowersEntry extends MessageMicro<tencent.im.group.group_member_info.FlowersEntry> {
static final FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 8 }, new String[] { "uint64_flower_count" }, new Object[] { Long.valueOf(0L) }, tencent.im.group.group_member_info.FlowersEntry.class);
public final PBUInt64Field uint64_flower_count = PBField.initUInt64(0L);
}
public static class GBarInfo extends MessageMicro<tencent.im.group.group_member_info.GBarInfo> {
static final FieldMap __fieldMap__;
public final PBBytesField bytes_gbar_name;
public final PBBytesField str_head_portrait;
public final PBUInt32Field uint32_gbar_id = PBField.initUInt32(0);
public final PBUInt32Field uint32_uin_lev = PBField.initUInt32(0);
static {
Integer integer = Integer.valueOf(0);
ByteStringMicro byteStringMicro = ByteStringMicro.EMPTY;
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 16, 26, 34 }, new String[] { "uint32_gbar_id", "uint32_uin_lev", "str_head_portrait", "bytes_gbar_name" }, new Object[] { integer, integer, byteStringMicro, byteStringMicro }, tencent.im.group.group_member_info.GBarInfo.class);
}
public GBarInfo() {
ByteStringMicro byteStringMicro = ByteStringMicro.EMPTY;
this.str_head_portrait = PBField.initBytes(byteStringMicro);
this.bytes_gbar_name = PBField.initBytes(byteStringMicro);
}
}
public static class MemberGameInfo extends MessageMicro<tencent.im.group.group_member_info.MemberGameInfo> {
static final FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 26, 34, 42, 50, 58 }, new String[] { "str_game_name", "str_level_name", "str_level_icon", "str_game_font_color", "str_game_background_color", "str_game_url", "str_desc_info" }, new Object[] { "", "", "", "", "", "", "" }, tencent.im.group.group_member_info.MemberGameInfo.class);
public final PBRepeatField<String> str_desc_info = PBField.initRepeat(PBField.initString(""));
public final PBStringField str_game_background_color = PBField.initString("");
public final PBStringField str_game_font_color = PBField.initString("");
public final PBStringField str_game_name = PBField.initString("");
public final PBStringField str_game_url = PBField.initString("");
public final PBStringField str_level_icon = PBField.initString("");
public final PBStringField str_level_name = PBField.initString("");
}
public static class MemberInfo extends MessageMicro<tencent.im.group.group_member_info.MemberInfo> {
public static final int CONCERN_TYPE_CONCERN = 1;
public static final int CONCERN_TYPE_GENERAL = 0;
public static final int CONCERN_TYPE_HATE = 2;
static final FieldMap __fieldMap__;
public final PBBoolField bool_is_allow_mod_card;
public final PBBoolField bool_is_concerned;
public final PBBoolField bool_is_friend;
public final PBBoolField bool_is_super_qq;
public final PBBoolField bool_is_super_vip;
public final PBBoolField bool_is_vip;
public final PBBoolField bool_is_year_vip;
public final PBBoolField bool_location_shared;
public final PBBytesField bytes_group_honor;
public final PBBytesField bytes_job;
public final PBBytesField bytes_phone_num;
public final PBBytesField bytes_special_title;
public final PBUInt32Field medal_id;
public tencent.im.group.group_member_info.FlowersEntry msg_flower_entry;
public tencent.im.group.group_member_info.MemberGameInfo msg_game_info;
public group_member_info.TeamEntry msg_team_entry;
// public group_member_info.RspGroupCardGetStory qqstory_infocard;
public final PBRepeatMessageField<tencent.im.group.group_member_info.CustomEntry> rpt_msg_custom_enties;
public final PBRepeatMessageField<tencent.im.group.group_member_info.GBarInfo> rpt_msg_gbar_concerned;
public final PBBytesField str_card;
public final PBBytesField str_errmsg;
public final PBBytesField str_gbar_title;
public final PBBytesField str_gbar_url;
public final PBBytesField str_lev;
public final PBBytesField str_location;
public final PBBytesField str_nick;
public final PBBytesField str_remark;
public final PBUInt32Field uint32_age;
public final PBUInt32Field uint32_concern_type;
public final PBUInt32Field uint32_credit;
public final PBUInt32Field uint32_gbar_cnt;
public final PBUInt32Field uint32_group_honor_bit;
public final PBUInt32Field uint32_level;
public final PBUInt32Field uint32_result = PBField.initUInt32(0);
public final PBUInt32Field uint32_role;
public final PBUInt32Field uint32_sex;
public final PBUInt32Field uint32_special_title_expire_time;
public final PBUInt32Field uint32_vip_lev;
public final PBUInt64Field uint64_distance;
public final PBUInt64Field uint64_join;
public final PBUInt64Field uint64_last_speak;
public final PBUInt64Field uint64_uin = PBField.initUInt64(0L);
static {
Long long_ = Long.valueOf(0L);
Integer integer = Integer.valueOf(0);
ByteStringMicro byteStringMicro = ByteStringMicro.EMPTY;
Boolean bool = Boolean.FALSE;
__fieldMap__ = MessageMicro.initFieldMap(new int[] {
8, 16, 26, 32, 42, 48, 56, 66, 72, 82,
90, 96, 106, 112, 120, 130, 138, 146, 154, 160,
168, 176, 184, 192, 200, 208, 216, 224, 232, 240,
250, 256, 266, 274, 282, 290, 296, 306, 312, 322,
330, 336 }, new String[] {
"uint64_uin", "uint32_result", "str_errmsg", "bool_is_friend", "str_remark", "bool_is_concerned", "uint32_credit", "str_card", "uint32_sex", "str_location",
"str_nick", "uint32_age", "str_lev", "uint64_join", "uint64_last_speak", "rpt_msg_custom_enties", "rpt_msg_gbar_concerned", "str_gbar_title", "str_gbar_url", "uint32_gbar_cnt",
"bool_is_allow_mod_card", "bool_is_vip", "bool_is_year_vip", "bool_is_super_vip", "bool_is_super_qq", "uint32_vip_lev", "uint32_role", "bool_location_shared", "uint64_distance", "uint32_concern_type",
"bytes_special_title", "uint32_special_title_expire_time", "msg_flower_entry", "msg_team_entry", "bytes_phone_num", "bytes_job", "medal_id", "qqstory_infocard", "uint32_level", "msg_game_info",
"bytes_group_honor", "uint32_group_honor_bit" }, new Object[] {
long_, integer, byteStringMicro, bool, byteStringMicro, bool, integer, byteStringMicro, integer, byteStringMicro,
byteStringMicro, integer, byteStringMicro, long_, long_, null, null, byteStringMicro, byteStringMicro, integer,
bool, bool, bool, bool, bool, integer, integer, bool, long_, integer,
byteStringMicro, integer, null, null, byteStringMicro, byteStringMicro, integer, null, integer, null,
byteStringMicro, integer }, tencent.im.group.group_member_info.MemberInfo.class);
}
public MemberInfo() {
ByteStringMicro byteStringMicro = ByteStringMicro.EMPTY;
this.str_errmsg = PBField.initBytes(byteStringMicro);
this.bool_is_friend = PBField.initBool(false);
this.str_remark = PBField.initBytes(byteStringMicro);
this.bool_is_concerned = PBField.initBool(false);
this.uint32_credit = PBField.initUInt32(0);
this.str_card = PBField.initBytes(byteStringMicro);
this.uint32_sex = PBField.initUInt32(0);
this.str_location = PBField.initBytes(byteStringMicro);
this.str_nick = PBField.initBytes(byteStringMicro);
this.uint32_age = PBField.initUInt32(0);
this.str_lev = PBField.initBytes(byteStringMicro);
this.uint64_join = PBField.initUInt64(0L);
this.uint64_last_speak = PBField.initUInt64(0L);
this.rpt_msg_custom_enties = PBField.initRepeatMessage(tencent.im.group.group_member_info.CustomEntry.class);
this.rpt_msg_gbar_concerned = PBField.initRepeatMessage(tencent.im.group.group_member_info.GBarInfo.class);
this.str_gbar_title = PBField.initBytes(byteStringMicro);
this.str_gbar_url = PBField.initBytes(byteStringMicro);
this.uint32_gbar_cnt = PBField.initUInt32(0);
this.bool_is_allow_mod_card = PBField.initBool(false);
this.bool_is_vip = PBField.initBool(false);
this.bool_is_year_vip = PBField.initBool(false);
this.bool_is_super_vip = PBField.initBool(false);
this.bool_is_super_qq = PBField.initBool(false);
this.uint32_vip_lev = PBField.initUInt32(0);
this.uint32_role = PBField.initUInt32(0);
this.bool_location_shared = PBField.initBool(false);
this.uint64_distance = PBField.initUInt64(0L);
this.uint32_concern_type = PBField.initUInt32(0);
this.bytes_special_title = PBField.initBytes(byteStringMicro);
this.uint32_special_title_expire_time = PBField.initUInt32(0);
this.msg_flower_entry = new tencent.im.group.group_member_info.FlowersEntry();
this.msg_team_entry = new group_member_info.TeamEntry();
this.bytes_phone_num = PBField.initBytes(byteStringMicro);
this.bytes_job = PBField.initBytes(byteStringMicro);
this.medal_id = PBField.initUInt32(0);
// this.qqstory_infocard = new group_member_info.RspGroupCardGetStory();
this.uint32_level = PBField.initUInt32(0);
this.msg_game_info = new tencent.im.group.group_member_info.MemberGameInfo();
this.bytes_group_honor = PBField.initBytes(byteStringMicro);
this.uint32_group_honor_bit = PBField.initUInt32(0);
}
}
public static class ReqBody extends MessageMicro<tencent.im.group.group_member_info.ReqBody> {
static final FieldMap __fieldMap__;
public final PBBoolField bool_new_client = PBField.initBool(false);
public final PBUInt32Field uint32_client_type = PBField.initUInt32(0);
public final PBUInt32Field uint32_rich_card_name_ver = PBField.initUInt32(0);
public final PBUInt64Field uint64_group_code = PBField.initUInt64(0L);
public final PBUInt64Field uint64_uin = PBField.initUInt64(0L);
static {
Long long_ = Long.valueOf(0L);
Integer integer = Integer.valueOf(0);
Boolean bool = Boolean.FALSE;
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 16, 24, 32, 40 }, new String[] { "uint64_group_code", "uint64_uin", "bool_new_client", "uint32_client_type", "uint32_rich_card_name_ver" }, new Object[] { long_, long_, bool, integer, integer }, tencent.im.group.group_member_info.ReqBody.class);
}
}
public static class RspBody extends MessageMicro<RspBody> {
static final FieldMap __fieldMap__;
public final PBBoolField bool_self_location_shared = PBField.initBool(false);
public MemberInfo msg_meminfo = new MemberInfo();
public final PBUInt32Field uint32_group_type = PBField.initUInt32(0);
public final PBUInt32Field uint32_self_role = PBField.initUInt32(0);
public final PBUInt64Field uint64_group_code = PBField.initUInt64(0L);
static {
Integer integer = Integer.valueOf(0);
Boolean bool = Boolean.FALSE;
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 16, 26, 32, 40 }, new String[] { "uint64_group_code", "uint32_self_role", "msg_meminfo", "bool_self_location_shared", "uint32_group_type" }, new Object[] { Long.valueOf(0L), integer, null, bool, integer }, tencent.im.group.group_member_info.RspBody.class);
}
}
public static class TeamEntry extends MessageMicro<TeamEntry> {
static final FieldMap __fieldMap__;
public final PBRepeatField<Long> rpt_uint64_depid;
public final PBRepeatField<Long> rpt_uint64_self_depid;
static {
Long long_ = Long.valueOf(0L);
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 16 }, new String[] { "rpt_uint64_depid", "rpt_uint64_self_depid" }, new Object[] { long_, long_ }, tencent.im.group.group_member_info.TeamEntry.class);
}
public TeamEntry() {
PBUInt64Field pBUInt64Field = (PBUInt64Field) PBUInt64Field.__repeatHelper__;
this.rpt_uint64_depid = PBField.initRepeat((PBField)pBUInt64Field);
this.rpt_uint64_self_depid = PBField.initRepeat((PBField)pBUInt64Field);
}
}
}

View File

@ -0,0 +1,51 @@
package tencent.im.oidb.cmd0x8a7;
import com.tencent.mobileqq.pb.ByteStringMicro;
import com.tencent.mobileqq.pb.MessageMicro;
import com.tencent.mobileqq.pb.PBBoolField;
import com.tencent.mobileqq.pb.PBBytesField;
import com.tencent.mobileqq.pb.PBField;
import com.tencent.mobileqq.pb.PBUInt32Field;
import com.tencent.mobileqq.pb.PBUInt64Field;
public class cmd0x8a7 {
public static class ReqBody
extends MessageMicro<ReqBody>
{
static final MessageMicro.FieldMap __fieldMap__;
public final PBUInt32Field uint32_limit_interval_type_for_group = PBField.initUInt32(0);
public final PBUInt32Field uint32_limit_interval_type_for_uin = PBField.initUInt32(0);
public final PBUInt32Field uint32_sub_cmd = PBField.initUInt32(0);
public final PBUInt64Field uint64_group_code = PBField.initUInt64(0L);
public final PBUInt64Field uint64_uin = PBField.initUInt64(0L);
static
{
Integer localInteger = Integer.valueOf(0);
Long localLong = Long.valueOf(0L);
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 16, 24, 32, 40 }, new String[] { "uint32_sub_cmd", "uint32_limit_interval_type_for_uin", "uint32_limit_interval_type_for_group", "uint64_uin", "uint64_group_code" }, new Object[] { localInteger, localInteger, localInteger, localLong, localLong }, ReqBody.class);
}
}
public static class RspBody
extends MessageMicro<RspBody>
{
static final MessageMicro.FieldMap __fieldMap__;
public final PBBoolField bool_can_at_all = PBField.initBool(false);
public final PBBoolField bool_show_at_all_lable = PBField.initBool(false);
public final PBBytesField bytes_prompt_msg_1 = PBField.initBytes(ByteStringMicro.EMPTY);
public final PBBytesField bytes_prompt_msg_2 = PBField.initBytes(ByteStringMicro.EMPTY);
public final PBUInt32Field uint32_remain_at_all_count_for_group = PBField.initUInt32(0);
public final PBUInt32Field uint32_remain_at_all_count_for_uin = PBField.initUInt32(0);
static
{
Integer localInteger = Integer.valueOf(0);
Boolean localBoolean = Boolean.valueOf(false);
ByteStringMicro localByteStringMicro1 = ByteStringMicro.EMPTY;
ByteStringMicro localByteStringMicro2 = ByteStringMicro.EMPTY;
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 16, 24, 34, 42, 48 }, new String[] { "bool_can_at_all", "uint32_remain_at_all_count_for_uin", "uint32_remain_at_all_count_for_group", "bytes_prompt_msg_1", "bytes_prompt_msg_2", "bool_show_at_all_lable" }, new Object[] { localBoolean, localInteger, localInteger, localByteStringMicro1, localByteStringMicro2, localBoolean }, RspBody.class);
}
}
}

View File

@ -0,0 +1,245 @@
package tencent.im.oidb.cmd0xeb7;
import com.tencent.mobileqq.pb.MessageMicro;
import com.tencent.mobileqq.pb.PBEnumField;
import com.tencent.mobileqq.pb.PBField;
import com.tencent.mobileqq.pb.PBFloatField;
import com.tencent.mobileqq.pb.PBInt32Field;
import com.tencent.mobileqq.pb.PBInt64Field;
import com.tencent.mobileqq.pb.PBRepeatField;
import com.tencent.mobileqq.pb.PBRepeatMessageField;
import com.tencent.mobileqq.pb.PBStringField;
public class oidb_0xeb7 {
public static class ReqBody extends MessageMicro<ReqBody> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18 }, new String[] { "signInStatusReq", "signInWriteReq" }, new Object[] { null, null }, ReqBody.class);
public StSignInStatusReq signInStatusReq = new StSignInStatusReq();
public StSignInWriteReq signInWriteReq = new StSignInWriteReq();
}
public static class RspBody extends MessageMicro<RspBody>
{
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18 }, new String[] { "signInStatusRsp", "signInWriteRsp" }, new Object[] { null, null }, RspBody.class);
public StSignInStatusRsp signInStatusRsp = new StSignInStatusRsp();
public StSignInWriteRsp signInWriteRsp = new StSignInWriteRsp();
}
public static class StSignInWriteRsp
extends MessageMicro<StSignInWriteRsp>
{
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 26 }, new String[] { "ret", "doneInfo", "groupScore" }, new Object[] { null, null, null }, StSignInWriteRsp.class);
public SignInStatusDoneInfo doneInfo = new SignInStatusDoneInfo();
public SignInStatusGroupScore groupScore = new SignInStatusGroupScore();
public Ret ret = new Ret();
}
public static class StSignInStatusRsp
extends MessageMicro<StSignInStatusRsp>
{
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 26, 34, 42, 50, 58, 66 }, new String[] { "ret", "base", "yesterday", "notInfo", "doneInfo", "groupScore", "mantleUrl", "backgroundUrl" }, new Object[] { null, null, null, null, null, null, "", "" }, StSignInStatusRsp.class);
public final PBStringField backgroundUrl = PBField.initString("");
public SignInStatusBase base = new SignInStatusBase();
public SignInStatusDoneInfo doneInfo = new SignInStatusDoneInfo();
public SignInStatusGroupScore groupScore = new SignInStatusGroupScore();
public final PBStringField mantleUrl = PBField.initString("");
public SignInStatusNotInfo notInfo = new SignInStatusNotInfo();
public Ret ret = new Ret();
public SignInStatusYesterdayFirst yesterday = new SignInStatusYesterdayFirst();
}
public static class SignInStatusYesterdayFirst
extends MessageMicro<SignInStatusYesterdayFirst>
{
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 26 }, new String[] { "yesterdayFirstUid", "yesterdayWord", "yesterdayNick" }, new Object[] { "", "", "" }, SignInStatusYesterdayFirst.class);
public final PBStringField yesterdayFirstUid = PBField.initString("");
public final PBStringField yesterdayNick = PBField.initString("");
public final PBStringField yesterdayWord = PBField.initString("");
}
public static class SignInStatusNotInfo
extends MessageMicro<SignInStatusNotInfo>
{
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 26 }, new String[] { "buttonWord", "signDescWordLeft", "signDescWordRight" }, new Object[] { "", "", "" }, SignInStatusNotInfo.class);
public final PBStringField buttonWord = PBField.initString("");
public final PBStringField signDescWordLeft = PBField.initString("");
public final PBStringField signDescWordRight = PBField.initString("");
}
public static class SignInStatusGroupScore
extends MessageMicro<SignInStatusGroupScore>
{
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18 }, new String[] { "groupScoreWord", "scoreUrl" }, new Object[] { "", "" }, SignInStatusGroupScore.class);
public final PBStringField groupScoreWord = PBField.initString("");
public final PBStringField scoreUrl = PBField.initString("");
}
public static class SignInStatusDoneInfo
extends MessageMicro<SignInStatusDoneInfo>
{
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 26, 34 }, new String[] { "leftTitleWrod", "rightDescWord", "belowPortraitWords", "recordUrl" }, new Object[] { "", "", "", "" }, SignInStatusDoneInfo.class);
public final PBRepeatField<String> belowPortraitWords = PBField.initRepeat(PBField.initString(""));
public final PBStringField leftTitleWrod = PBField.initString("");
public final PBStringField recordUrl = PBField.initString("");
public final PBStringField rightDescWord = PBField.initString("");
}
public static class StSignInRecordDaySigned extends MessageMicro<StSignInRecordDaySigned> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 13, 16, 26, 34 }, new String[] { "daySignedRatio", "dayTotalSignedUid", "daySignedPage", "daySignedUrl" }, new Object[] { Float.valueOf(0.0F), Integer.valueOf(0), null, "" }, StSignInRecordDaySigned.class);
public StDaySignedPage daySignedPage = new StDaySignedPage();
public final PBFloatField daySignedRatio = PBField.initFloat(0.0F);
public final PBStringField daySignedUrl = PBField.initString("");
public final PBInt32Field dayTotalSignedUid = PBField.initInt32(0);
}
public static class SignInStatusBase extends MessageMicro<SignInStatusBase> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 16 }, new String[] { "status", "currentTimeStamp" }, new Object[] { Integer.valueOf(0), Long.valueOf(0L) }, SignInStatusBase.class);
public final PBInt64Field currentTimeStamp = PBField.initInt64(0L);
public final PBEnumField status = PBField.initEnum(0);
}
public static class StSignInRecordRsp extends MessageMicro<StSignInRecordRsp> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 26, 34, 42, 50 }, new String[] { "ret", "base", "userRecord", "daySigned", "kingRecord", "level" }, new Object[] { null, null, null, null, null, null }, StSignInRecordRsp.class);
public SignInStatusBase base = new SignInStatusBase();
public StSignInRecordDaySigned daySigned = new StSignInRecordDaySigned();
public StSignInRecordKing kingRecord = new StSignInRecordKing();
public StViewGroupLevel level = new StViewGroupLevel();
public Ret ret = new Ret();
public StSignInRecordUser userRecord = new StSignInRecordUser();
}
public static class Ret extends MessageMicro<Ret> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 18 }, new String[] { "code", "msg" }, new Object[] { Integer.valueOf(0), "" }, Ret.class);
public final PBEnumField code = PBField.initEnum(0);
public final PBStringField msg = PBField.initString("");
}
public static class StSignInRecordUser extends MessageMicro<StSignInRecordUser> {
static final MessageMicro.FieldMap __fieldMap__;
public final PBInt64Field continueSignedDays = PBField.initInt64(0L);
public final PBInt64Field earliestSignedTimeStamp = PBField.initInt64(0L);
public final PBStringField groupName = PBField.initString("");
public final PBRepeatField<String> historySignedDays = PBField.initRepeat(PBField.initString(""));
public final PBInt32Field totalSignedDays = PBField.initInt32(0);
static {
Long long_ = Long.valueOf(0L);
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 16, 24, 32, 42, 50 }, new String[] { "totalSignedDays", "earliestSignedTimeStamp", "continueSignedDays", "historySignedDays", "groupName" }, new Object[] { Integer.valueOf(0), long_, long_, "", "" }, StSignInRecordUser.class);
}
}
public static class StDaySignedInfo extends MessageMicro<StDaySignedInfo> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 24, 32 }, new String[] { "uid", "uidGroupNick", "signedTimeStamp", "signInRank" }, new Object[] { "", "", Long.valueOf(0L), Integer.valueOf(0) }, StDaySignedInfo.class);
public final PBInt32Field signInRank = PBField.initInt32(0);
public final PBInt64Field signedTimeStamp = PBField.initInt64(0L);
public final PBStringField uid = PBField.initString("");
public final PBStringField uidGroupNick = PBField.initString("");
}
public static class StDaySignedPage extends MessageMicro<StDaySignedPage> {
static final MessageMicro.FieldMap __fieldMap__;
public final PBRepeatMessageField<StDaySignedInfo> infos = PBField.initRepeatMessage(StDaySignedInfo.class);
public final PBInt32Field offset = PBField.initInt32(0);
public final PBInt32Field total = PBField.initInt32(0);
static {
Integer integer = Integer.valueOf(0);
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 16, 24 }, new String[] { "infos", "offset", "total" }, new Object[] { null, integer, integer }, StDaySignedPage.class);
}
}
public static class StKingSignedInfo extends MessageMicro<StKingSignedInfo> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 24, 32 }, new String[] { "uid", "groupNick", "signedTimeStamp", "signedCount" }, new Object[] { "", "", Long.valueOf(0L), Integer.valueOf(0) }, StKingSignedInfo.class);
public final PBStringField groupNick = PBField.initString("");
public final PBInt32Field signedCount = PBField.initInt32(0);
public final PBInt64Field signedTimeStamp = PBField.initInt64(0L);
public final PBStringField uid = PBField.initString("");
}
public static class StSignInStatusReq extends MessageMicro<StSignInStatusReq> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 24, 34 }, new String[] { "uid", "groupId", "scene", "clientVersion" }, new Object[] { "", "", Integer.valueOf(0), "" }, StSignInStatusReq.class);
public final PBStringField clientVersion = PBField.initString("");
public final PBStringField groupId = PBField.initString("");
public final PBEnumField scene = PBField.initEnum(0);
public final PBStringField uid = PBField.initString("");
}
public static class StSignInWriteReq extends MessageMicro<StSignInWriteReq> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 26 }, new String[] { "uid", "groupId", "clientVersion" }, new Object[] { "", "", "" }, StSignInWriteReq.class);
public final PBStringField clientVersion = PBField.initString("");
public final PBStringField groupId = PBField.initString("");
public final PBStringField uid = PBField.initString("");
}
public static class StViewGroupLevel extends MessageMicro<StViewGroupLevel> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18 }, new String[] { "title", "url" }, new Object[] { "", "" }, StViewGroupLevel.class);
public final PBStringField title = PBField.initString("");
public final PBStringField url = PBField.initString("");
}
public static class StSignInRecordKing extends MessageMicro<StSignInRecordKing> {
static final MessageMicro.FieldMap __fieldMap__ = MessageMicro.initFieldMap(new int[] { 10, 18, 26, 34 }, new String[] { "yesterdayFirst", "topSignedTotal", "topSignedContinue", "kingUrl" }, new Object[] { null, null, null, "" }, StSignInRecordKing.class);
public final PBStringField kingUrl = PBField.initString("");
public final PBRepeatMessageField<StKingSignedInfo> topSignedContinue = PBField.initRepeatMessage(StKingSignedInfo.class);
public final PBRepeatMessageField<StKingSignedInfo> topSignedTotal = PBField.initRepeatMessage(StKingSignedInfo.class);
public StKingSignedInfo yesterdayFirst = new StKingSignedInfo();
}
}

View File

@ -0,0 +1,21 @@
package tencent.im.troop.honor;
import com.tencent.mobileqq.pb.MessageMicro;
import com.tencent.mobileqq.pb.PBField;
import com.tencent.mobileqq.pb.PBRepeatField;
import com.tencent.mobileqq.pb.PBUInt32Field;
public class troop_honor {
public static class GroupUserCardHonor extends MessageMicro<GroupUserCardHonor> {
static final FieldMap __fieldMap__;
public final PBRepeatField<Integer> id = PBField.initRepeat((PBField) PBUInt32Field.__repeatHelper__);
public final PBUInt32Field level = PBField.initUInt32(0);
static {
Integer integer = Integer.valueOf(0);
__fieldMap__ = MessageMicro.initFieldMap(new int[] { 8, 16 }, new String[] { "id", "level" }, new Object[] { integer, integer }, GroupUserCardHonor.class);
}
}
}

View File

@ -7,7 +7,7 @@ pluginManagement {
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()

View File

@ -8,12 +8,13 @@ plugins {
android {
namespace = "moe.fuqiuluo.xposed"
compileSdk = 33
compileSdk = 34
defaultConfig {
minSdk = 24
minSdk = 27
consumerProguardFiles("consumer-rules.pro")
@Suppress("UnstableApiUsage")
externalNativeBuild {
cmake {
cppFlags += ""

View File

@ -15,6 +15,8 @@ static std::vector<std::string> qemu_detect_props = {
static int (*backup_system_property_get)(const char *name, char *value);
static FILE* (*backup_fopen)(const char *filename, const char *mode);
static int (*backup_memcmp)(const void* __lhs, const void* __rhs, size_t __n);
//static const char* (*backup_strstr)(const char* h, const char* n);
//int fake_system_property_get(const char *name, char *value);
//FILE* fake_fopen(const char *filename, const char *mode);

View File

@ -71,6 +71,7 @@ int fake_system_property_get(const char *name, char *value) {
|| strstr(value, "unknown")
|| strstr(value, "emulator")
|| strstr(value, "vbox")
|| strstr(value, "nox") //部分NoxAppPlayer
|| strstr(value, "genymotion")
|| strstr(value, "goldfish")) {
strcpy(value, "qcom");
@ -84,22 +85,84 @@ int fake_system_property_get(const char *name, char *value) {
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;
const char* emuSpecFile[] = {
"libhoudini.so",
"libndk.so",
"libnoxd.so", //NoxAppPlayer
"libnoxspeedup.so", //NoxAppPlayer
"nox-prop", //NoxAppPlayer (MayUseless?)
"nox-vbox-sf", //NoxAppPlayer (MayUseless?)
"noxd", //NoxAppPlayer (MayUseless?)
"noxspeedup", //NoxAppPlayer (MayUseless?)
};
for (const char* keyword : emuSpecFile) {
if (strstr(filename, keyword)) {
LOGI("[Shamrock] bypass emu detection");
return nullptr;
}
}
if (strstr(filename, "libdobby.so")) {
LOGI("[Shamrock] bypass dobby detection");
return nullptr;
}
return backup_fopen(filename, mode);
}
char * __cdecl my_strstr(const char *lhs, const char *rhs) {
char *cur = (char *)lhs;
char *l;
char *r;
if (!*rhs) {
return ((char *)lhs);
}
while (*cur) {
l = cur;
r = (char *)rhs;
while (*r && !(*l - *r)) {
l++;
r++;
}
if (!*r) {
return cur;
}
cur++;
}
return nullptr;
}
int fake_memcmp(const void* __lhs, const void* __rhs, size_t __n) {
//if (my_strstr((const char*) __rhs, "lsposed")) {
//return -1;
//}
//if (my_strstr((const char*) __rhs, "xposed")) {
// return -1;
//}
if (my_strstr((const char*) __rhs, "shamrock")) {
if (backup_memcmp(__lhs, __rhs, __n) == 0) {
// 底层广播判断
return 0;
}
return -1;
}
if (my_strstr((const char*) __rhs, "riru")) {
return -1;
}
//if (my_strstr((const char*) __rhs, "zygisk")) {
// return -1;
//}
//if (my_strstr((const char*) __rhs, "magisk")) {
// return -1;
//}
return backup_memcmp(__lhs, __rhs, __n);
}
void on_library_loaded(const char *name, void *handle) {
}
@ -119,5 +182,8 @@ Java_moe_fuqiuluo_shamrock_xposed_actions_AntiDetection_antiNativeDetections(JNI
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);
//hook_function((void*) strstr, (void*) fake_strstr, (void**) &backup_strstr);
hook_function((void*) memcmp, (void*) fake_memcmp, (void**) &backup_memcmp);
return true;
}
}

View File

@ -61,7 +61,7 @@ class ProtoMap(
var curMap = value
tags.forEachIndexed { index, tag ->
if (index == tags.size - 1) {
return curMap[tag] ?: error("Tag $tag not found")
return curMap[tag] ?: error("pb[${tags.joinToString(", ")}][$index] Tag $tag not found")
}
curMap[tag]?.let { v ->
if (v is ProtoMap) {
@ -69,7 +69,7 @@ class ProtoMap(
} else {
return v
}
} ?: error("Tag $tag not found")
} ?: error("pb[${tags.joinToString(", ")}][$index] Tag $tag not found")
}
error("Instance is not ProtoMap for get(${tags.first()})")
}

View File

@ -109,7 +109,7 @@ internal object CardSvc: BaseSvc() {
val dataService = app
.getRuntimeService(IProfileDataService::class.java, "all")
val card = refreshCardLock.withLock {
suspendCancellableCoroutine<Card?> {
suspendCancellableCoroutine {
app.addObserver(object: ProfileCardObserver() {
override fun onGetProfileCard(success: Boolean, obj: Any) {
app.removeObserver(this)

View File

@ -2,6 +2,7 @@
package moe.fuqiuluo.qqinterface.servlet
import androidx.core.text.HtmlCompat
import com.tencent.common.app.AppInterface
import com.tencent.mobileqq.app.BusinessHandlerFactory
import com.tencent.mobileqq.app.QQAppInterface
@ -46,7 +47,11 @@ 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.TicketSvc.getLongUin
import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getUin
import moe.fuqiuluo.qqinterface.servlet.entries.GroupAtAllRemainInfo
import moe.fuqiuluo.qqinterface.servlet.entries.ProhibitedMemberInfo
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.service.data.EssenceMessage
@ -65,13 +70,19 @@ 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.utils.PlatformUtils
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import mqq.app.MobileQQ
import tencent.im.group.group_member_info
import tencent.im.oidb.cmd0x899.oidb_0x899
import tencent.im.oidb.cmd0x89a.oidb_0x89a
import tencent.im.oidb.cmd0x8a0.oidb_0x8a0
import tencent.im.oidb.cmd0x8a7.cmd0x8a7
import tencent.im.oidb.cmd0x8fc.Oidb_0x8fc
import tencent.im.oidb.cmd0xeb7.oidb_0xeb7
import tencent.im.oidb.oidb_sso
import tencent.im.troop.honor.troop_honor
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.nio.ByteBuffer
@ -94,6 +105,27 @@ internal object GroupSvc: BaseSvc() {
private lateinit var METHOD_REQ_TROOP_MEM_LIST: Method
private lateinit var METHOD_REQ_MODIFY_GROUP_NAME: Method
suspend fun getGroupRemainAtAllRemain (groupId: Long): Result<GroupAtAllRemainInfo> {
val buffer = sendOidbAW("OidbSvcTrpcTcp.0x8a7_0", 2215, 0, cmd0x8a7.ReqBody().apply {
uint32_sub_cmd.set(1)
uint32_limit_interval_type_for_uin.set(2)
uint32_limit_interval_type_for_group.set(1)
uint64_uin.set(getLongUin())
uint64_group_code.set(groupId)
}.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout"))
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(buffer.slice(4))
if(body.uint32_result.get() != 0) {
return Result.failure(RuntimeException(body.str_error_msg.get()))
}
val resp = cmd0x8a7.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
return Result.success(GroupAtAllRemainInfo(
canAtAll = resp.bool_can_at_all.get(),
remainAtAllCountForGroup = resp.uint32_remain_at_all_count_for_group.get(),
remainAtAllCountForUin = resp.uint32_remain_at_all_count_for_uin.get()
))
}
suspend fun getProhibitedMemberList(groupId: Long): Result<List<ProhibitedMemberInfo>> {
val buffer = sendOidbAW("OidbSvc.0x899_0", 2201, 0, oidb_0x899.ReqBody().apply {
uint64_group_code.set(groupId)
@ -147,7 +179,7 @@ internal object GroupSvc: BaseSvc() {
var troopList = service.allTroopList
if(refresh || !service.isTroopCacheInited || troopList == null) {
if(!requestGroupList(service)) {
if(!requestGroupInfo(service)) {
return Result.failure(Exception("获取群列表失败"))
} else {
troopList = service.allTroopList
@ -160,15 +192,14 @@ internal object GroupSvc: BaseSvc() {
val service = app
.getRuntimeService(ITroopInfoService::class.java, "all")
var groupInfo = getGroupInfo(groupId)
val groupInfo = getGroupInfo(groupId)
if(refresh || !service.isTroopCacheInited || groupInfo.troopuin.isNullOrBlank()) {
groupInfo = requestGroupList(service, groupId.toLong()).onFailure {
return Result.failure(it)
}.getOrThrow()
return if(refresh || !service.isTroopCacheInited || groupInfo.troopuin.isNullOrBlank()) {
requestGroupInfo(service, groupId.toLong())
} else {
Result.success(groupInfo)
}
return Result.success(groupInfo)
}
suspend fun setGroupUniqueTitle(groupId: String, userId: String, title: String) {
@ -177,7 +208,7 @@ internal object GroupSvc: BaseSvc() {
req.uint64_group_code.set(groupId.toLong())
val memberInfo = Oidb_0x8fc.MemberInfo()
memberInfo.uint64_uin.set(userId.toLong())
memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifBlank {
memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty {
localMemberInfo.troopremark.ifNullOrEmpty("")
}))
memberInfo.bytes_special_title.set(ByteStringMicro.copyFromUtf8(title))
@ -420,6 +451,36 @@ internal object GroupSvc: BaseSvc() {
}
}
}
try {
if (info != null && (info.alias == null || info.alias.isBlank())) {
val req = group_member_info.ReqBody()
req.uint64_group_code.set(groupId.toLong())
req.uint64_uin.set(uin.toLong())
req.bool_new_client.set(true)
req.uint32_client_type.set(1)
req.uint32_rich_card_name_ver.set(1)
val respBuffer = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray())
if (respBuffer != null) {
val rsp = group_member_info.RspBody()
rsp.mergeFrom(respBuffer.slice(4))
if (rsp.msg_meminfo.str_location.has()) {
info.alias = rsp.msg_meminfo.str_location.get().toStringUtf8()
}
if (rsp.msg_meminfo.uint32_age.has()) {
info.age = rsp.msg_meminfo.uint32_age.get().toByte()
}
if (rsp.msg_meminfo.bytes_group_honor.has()) {
val honorBytes = rsp.msg_meminfo.bytes_group_honor.get().toByteArray()
val honor = troop_honor.GroupUserCardHonor()
honor.mergeFrom(honorBytes)
info.level = honor.level.get()
// 10315: medal_id not real group level
}
}
}
} catch (err: Throwable) {
LogCenter.log(err.stackTraceToString(), Level.WARN)
}
return if (info != null) {
Result.success(info)
} else {
@ -499,7 +560,7 @@ internal object GroupSvc: BaseSvc() {
}
}
private suspend fun requestGroupList(
private suspend fun requestGroupInfo(
service: ITroopInfoService
): Boolean {
refreshTroopList()
@ -526,7 +587,7 @@ internal object GroupSvc: BaseSvc() {
throw RuntimeException("AppRuntime cannot cast to AppInterface")
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_LIST_HANDLER)
// void C(boolean foreRefresh, String groupId, String troopcode, int reqType); // RequestedTroopList/refreshMemberListFromServer
// void C(boolean forceRefresh, String groupId, String troopcode, int reqType); // RequestedTroopList/refreshMemberListFromServer
if (!GroupSvc::METHOD_REQ_TROOP_MEM_LIST.isInitialized) {
METHOD_REQ_TROOP_MEM_LIST = businessHandler.javaClass.declaredMethods.first {
it.parameterCount == 4
@ -592,9 +653,9 @@ internal object GroupSvc: BaseSvc() {
METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler, groupId.toString(), groupUin2GroupCode(groupId).toString(), arrayListOf(memberUin.toString()))
}
private suspend fun requestGroupList(dataService: ITroopInfoService, uin: Long): Result<TroopInfo> {
private suspend fun requestGroupInfo(dataService: ITroopInfoService, uin: Long): Result<TroopInfo> {
val strUin = uin.toString()
val list = withTimeoutOrNull(5000) {
val info = withTimeoutOrNull(5000) {
var troopInfo: TroopInfo?
do {
troopInfo = dataService.getTroopInfo(strUin)
@ -602,8 +663,8 @@ internal object GroupSvc: BaseSvc() {
} while (troopInfo == null || troopInfo.troopuin.isNullOrBlank())
return@withTimeoutOrNull troopInfo
}
return if (list != null) {
Result.success(list)
return if (info != null) {
Result.success(info)
} else {
Result.failure(Exception("获取群列表失败"))
}
@ -805,12 +866,13 @@ internal object GroupSvc: BaseSvc() {
senderId = obj["u"].asLong,
publishTime = obj["pubt"].asLong,
message = GroupAnnouncementMessage(
text = obj["msg"].asJsonObject["text"].asString,
images = obj["msg"].asJsonObject["pics"].asJsonArrayOrNull?.map {
// text = obj["msg"].asJsonObject["text"].asString,
text = fromHtml(obj["msg"].asJsonObject["text"].asString),
images = obj["msg"].asJsonObject["pics"].asJsonArrayOrNull?.map { pic ->
GroupAnnouncementMessageImage(
id = it.jsonObject["id"].asString,
width = it.jsonObject["w"].asString,
height = it.jsonObject["h"].asString,
id = pic.jsonObject["id"].asString,
width = pic.jsonObject["w"].asString,
height = pic.jsonObject["h"].asString,
)
} ?: ArrayList()
)
@ -821,6 +883,14 @@ internal object GroupSvc: BaseSvc() {
}
}
private fun fromHtml(htmlString: String): String {
return HtmlCompat
// 特殊处理&#10;目的是替换为换行符否则会被fromHtml忽略并移除
.fromHtml(htmlString.replace("&#10;", "[shamrockplaceholder]"), HtmlCompat.FROM_HTML_MODE_LEGACY)
.toString()
.replace("[shamrockplaceholder]", "\n")
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun uploadImageTroopNotice(image: String): Result<GroupAnnouncementMessageImage> {
val file = FileUtils.parseAndSave(image)
@ -907,4 +977,27 @@ internal object GroupSvc: BaseSvc() {
Result.failure(Exception(body.jsonObject["em"].asStringOrNull))
}
}
suspend fun groupSign(groupId: Long): Result<String> {
val req = oidb_0xeb7.ReqBody()
val signInWriteReq = oidb_0xeb7.StSignInWriteReq()
signInWriteReq.groupId.set(groupId.toString())
signInWriteReq.uid.set(getUin())
var version = PlatformUtils.getClientVersion(MobileQQ.getContext())
version = version.replace("android", "").trimStart()
signInWriteReq.clientVersion.set(version)
req.signInWriteReq.set(signInWriteReq)
val buffer = sendOidbAW("OidbSvc.0xeb7", 3767, 1, req.toByteArray())
return if (buffer == null) {
Result.failure(Exception("操作失败"))
} else {
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(buffer.slice(4))
val rsp = oidb_0xeb7.RspBody()
rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray())
val doneInfo = rsp.signInWriteRsp.doneInfo
LogCenter.log(rsp.toString(), Level.DEBUG)
Result.success("${doneInfo.leftTitleWrod.get()} ${doneInfo.rightDescWord.get()} ${doneInfo.belowPortraitWords.get().joinToString(" ")}")
}
}
}

View File

@ -31,10 +31,6 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
internal object MsgSvc: BaseSvc() {
fun uploadForwardMsg(): Result<String> {
return Result.failure(Exception("Not implemented"))
}
suspend fun prepareTempChatFromGroup(
groupId: String,
peerId: String
@ -176,9 +172,6 @@ internal object MsgSvc: BaseSvc() {
fromId: String = peedId,
retryCnt: Int = 3
): Result<Pair<Long, Int>> {
//LogCenter.log(message.toString(), Level.ERROR)
//callback.msgHash = result.second 什么垃圾代码万一cb比你快你不就寄了
// 主动临时消息
when (chatType) {
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
@ -188,13 +181,7 @@ internal object MsgSvc: BaseSvc() {
}
}
}
val result = MessageHelper.sendMessageWithoutMsgId(
chatType,
peedId,
message,
fromId,
MessageCallback(peedId, 0)
)
val result = MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
return if (result.isFailure
&& result.exceptionOrNull()?.javaClass == SendMsgException::class.java
&& retryCnt > 0) {

View File

@ -0,0 +1,436 @@
package moe.fuqiuluo.qqinterface.servlet
import android.graphics.BitmapFactory
import com.tencent.mobileqq.app.QQAppInterface
import com.tencent.mobileqq.transfile.HttpNetReq
import com.tencent.mobileqq.transfile.INetEngineListener
import com.tencent.mobileqq.transfile.NetReq
import com.tencent.mobileqq.transfile.NetResp
import com.tencent.mobileqq.transfile.ServerAddr
import com.tencent.mobileqq.transfile.api.IHttpEngineService
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.io.core.BytePacketBuilder
import kotlinx.io.core.readBytes
import kotlinx.io.core.writeFully
import moe.fuqiuluo.proto.protobufMapOf
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.utils.MD5
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import mqq.manager.TicketManager
import oicq.wlogin_sdk.request.Ticket
import oicq.wlogin_sdk.request.WtTicketPromise
import oicq.wlogin_sdk.tools.ErrMsg
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.ByteBuffer
import kotlin.coroutines.resume
/**
* QQ收藏相关接口
*/
internal object QFavSvc: BaseSvc() {
private val SERVER_LIST_COLLECTOR = listOf(ServerAddr().also {
it.isIpv6 = false
it.mIp = "collector.weiyun.com"
it.port = 80
})
private val SERVER_LIST_PICUP = listOf(ServerAddr().also {
it.isIpv6 = false
it.mIp = "pic.pieceup.qq.com"
it.port = 80
})
private const val VERSION = 12820
private const val APPID = 30244
private const val SUB_APPID = 538116905
private const val MAJOR_VERSION = 8
private const val MINOR_VERSION = 9
private var seq = 1
suspend fun getItemList(
category: Int,
startPos: Int,
pageSize: Int,
): Result<NetResp> {
val data = protobufMapOf {
it[1] = mapOf(
20000 to mapOf(
/**
* "type", "bid", "category", "start_time", "order_type", "start_pos", "page_size", "sync_policy", "req_source"
*/
1 to 0,
2 to 0,
3 to category,
//4 to System.currentTimeMillis() - 1000 * 60,
//4 to System.currentTimeMillis(),
4 to 0,
5 to 0,
6 to startPos,
7 to pageSize,
8 to 0,
9 to 0
)
)
}.toByteArray()
return sendWeiyunReq(20000, data)
}
suspend fun getItemContent(
id: String
): Result<NetResp> {
val data = protobufMapOf {
it[1] = mapOf(
20001 to mapOf(
1 to id
)
)
}.toByteArray()
return sendWeiyunReq(20001, data)
}
suspend fun addImageMsg(
uin: Long,
name: String,
groupId: Long = 0,
groupName: String = "",
picUrl: String,
pid: String,
width: Int, height: Int,
size: Long,
md5: String,
): Result<NetResp> {
val md5Bytes = md5.hex2ByteArray()
val data = protobufMapOf {
it[1] = mapOf(
20009 to mapOf(
1 to mapOf(
1 to 1, // bid
2 to 1, // category
3 to mapOf( // author
1 to if (groupId == 0L) 1 else 2, // type
2 to uin, // num_id
3 to name, // str_id
4 to groupId, // group_id
5 to groupName // group_name
),
4 to System.currentTimeMillis() - 2000, // create_time
5 to System.currentTimeMillis() - 1000, // sequence
7 to """{"recordAudioOnly":false,"audioOnly":false,"fileOnly":false}""",
9 to 0, // original_app_id
10 to 0 // custom_group_id
),
2 to mapOf(
1 to "",
3 to "[图片]",
4 to mapOf(
1 to picUrl,
2 to md5Bytes,
3 to md5,
6 to width,
7 to height,
8 to size,
9 to 0,
11 to pid
),
5 to 1
),
3 to mapOf(
2 to """<img src="$picUrl" />""",
4 to mapOf(
1 to picUrl,
2 to md5Bytes,
3 to md5,
6 to width,
7 to height,
8 to size,
9 to 0,
11 to pid
)
)
)
)
}.toByteArray()
return sendWeiyunReq(20009, data)
}
suspend fun applyUpImageMsg(
uin: Long,
name: String,
groupId: Long = 0,
groupName: String = "",
width: Int, height: Int,
image: File
): Result<NetResp> {
if (!image.exists()) {
return Result.failure(IllegalArgumentException("image file not exists"))
}
val md5 = MD5.genFileMd5(image.absolutePath)
val data = protobufMapOf {
it[1] = mapOf(
20010 to mapOf(
1 to mapOf(
2 to md5,
4 to md5.toHexString(),
10 to mapOf( // author
1 to if (groupId == 0L) 1 else 2, // type
2 to uin, // num_id
3 to name, // str_id
4 to groupId, // group_id
5 to groupName // group_name
),
6 to width, // width
7 to height,
8 to image.length(),
9 to 1, // type
11 to "/storage/emulated/0/DCIM/ShamrockUpload.jpeg" // pic_id
)
)
)
}.toByteArray()
return sendWeiyunReq(20010, data)
}
suspend fun addRichMediaMsg(
uin: Long,
name: String,
groupId: Long = 0,
groupName: String = "",
time: Long = System.currentTimeMillis(),
content: String
): Result<NetResp> {
val data = protobufMapOf {
it[1] = mapOf(
20009 to mapOf(
1 to mapOf(
/**
* 1 => bid
* 2 => category
* 3 => author
* 4 => create_time
* 5 => sequence
* 6 => biz_key
* 7 => biz_data_list
* 8 => share_url
* 9 => original_app_id
* 10 => custom_group_id
* 506 => modify_time
* 507 => qzone_ugc_key
*/
1 to 1, // bid
2 to 1, // category
3 to mapOf( // author
1 to if (groupId == 0L) 1 else 2, // type
2 to uin, // num_id
3 to name, // str_id
4 to groupId, // group_id
5 to groupName // group_name
),
4 to time - 2000, // create_time
5 to time - 1000, // sequence
9 to 0, // original_app_id
10 to 0 // custom_group_id
),
2 to mapOf(
/**
* 1 => title
* 2 => sub_title
* 3 => brief
* 4 => pic_list
* 5 => content_type
* 6 => original_uri
* 7 => publisher
* 8 => rich_media_version
*/
3 to content,
5 to 1
),
3 to mapOf(
/**
* 1 => rich_media
* 2 => raw_data
* 3 => biz_data_list
* 4 => pic_list
* 5 => file_list
*/
2 to content.textToHtml()
)
)
)
}.toByteArray()
return sendWeiyunReq(20009, data)
}
private fun String.textToHtml(): String {
return replace("\n", "<div><br/></div>")
}
suspend fun sendPicUpBlock(
fileSize: Long,
offset: Long,
block: ByteArray,
blockSize: Long,
sha: ByteArray,
pid: String,
outputStream: ByteArrayOutputStream = ByteArrayOutputStream(),
): Result<NetResp> {
return suspendCancellableCoroutine {
val httpNetReq = HttpNetReq()
httpNetReq.userData = null
httpNetReq.mCallback = object: INetEngineListener {
override fun onResp(netResp: NetResp) {
if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) {
netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"]
}
netResp.mRespData = outputStream.toByteArray().copyOf()
it.resume(Result.success(netResp))
}
override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {}
}
val vi = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getA2(app.currentAccountUin)
//LogCenter.log(pSKey)
httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST
httpNetReq.mSendData = BytePacketBuilder().apply {
writeInt(-1412589450)
writeInt(10000)
writeInt(0)
writeInt(sha.size + 16 + blockSize.toInt())
writeShort(0)
writeShort(sha.size.toShort())
writeFully(sha)
writeInt(fileSize.toInt())
writeInt(offset.toInt())
writeInt(blockSize.toInt())
writeFully(block)
}.build().readBytes()
httpNetReq.mOutStream = outputStream
httpNetReq.mStartDownOffset = 0L
httpNetReq.mReqProperties["Shamrock"] = "true"
httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;pid=%s;appid=%d", app.currentAccountUin, 8, vi, pid, APPID)
httpNetReq.mReqProperties["host"] = "pic.pieceup.qq.com"
httpNetReq.mReqProperties["Range"] = "bytes=0-"
httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString()
httpNetReq.mReqProperties["Accept-Encoding"] = "gzip"
httpNetReq.mReqProperties["Content-Encoding"] = "gzip"
httpNetReq.mPrioty = 1
httpNetReq.mReqUrl = "https://pic.pieceup.qq.com/"
httpNetReq.mServerList = SERVER_LIST_PICUP
val service = AppRuntimeFetcher.appRuntime
.getRuntimeService(IHttpEngineService::class.java, "qqfav")
service.sendReq(httpNetReq)
}
}
suspend fun sendWeiyunReq(
cmd: Int,
body: ByteArray,
outputStream: ByteArrayOutputStream = ByteArrayOutputStream(),
): Result<NetResp> {
return suspendCancellableCoroutine {
val httpNetReq = HttpNetReq()
httpNetReq.userData = null
httpNetReq.mCallback = object: INetEngineListener {
override fun onResp(netResp: NetResp) {
if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) {
netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"]
}
netResp.mRespData = outputStream.toByteArray().copyOf()
it.resume(Result.success(netResp))
}
override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {}
}
val pSKey = getWeiYunPSKey()
httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST
httpNetReq.mSendData = DeflateTools.gzip(packData(packHead(cmd, pSKey), body))
httpNetReq.mOutStream = outputStream
httpNetReq.mStartDownOffset = 0L
httpNetReq.mReqProperties["Shamrock"] = "true"
httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;appid=%d", app.currentAccountUin, 27, pSKey, APPID)
httpNetReq.mReqProperties["host"] = "collector.weiyun.com"
httpNetReq.mReqProperties["Range"] = "bytes=0-"
httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString()
httpNetReq.mReqProperties["Accept-Encoding"] = "gzip"
httpNetReq.mReqProperties["Content-Encoding"] = "gzip"
httpNetReq.mPrioty = 1
httpNetReq.mReqUrl = "https://collector.weiyun.com/collector.fcg"
httpNetReq.mServerList = SERVER_LIST_COLLECTOR
val service = AppRuntimeFetcher.appRuntime
.getRuntimeService(IHttpEngineService::class.java, "qqfav")
service.sendReq(httpNetReq)
}
}
private fun packHead(cmd: Int, pskey: String): ByteArray {
/**
* 1 => uin
* 2 => seq
* 3 => type
* 4 => cmd
* 5 => appid
* 6 => version
* 7 => nettype
* 8 => clientip
* 9 => encrypt
* 10 => keytype
* 11 => encryptkey
* 14 => major_version
* 15 => minor_version
* 101 => retcode
* 102 => retmsg
* 103 => promptmsg
* 111 => total_space
* 112 => used_space
*/
return protobufMapOf {
it[1] = app.longAccountUin
it[2] = seq++ // seq
it[3] = 1 // type
it[4] = cmd
it[5] = APPID
it[6] = VERSION // VERSION
it[7] = 3 // nettype
it[10] = 27 // keytype
it[11] = pskey
it[14] = MAJOR_VERSION
it[15] = MINOR_VERSION
}.toByteArray()
}
private fun packData(head: ByteArray, body: ByteArray): ByteArray {
val len = 16 + head.size + body.size
val buf = ByteBuffer.allocate(len)
buf.putInt(SUB_APPID)
buf.putShort(1)
buf.putInt(len)
buf.putInt(body.size)
buf.putShort(0)
buf.put(head)
buf.put(body)
return buf.array()
}
private fun getWeiYunPSKey(): String {
val pskey = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager)
.getPskey(app.currentAccountUin, 16L, arrayOf("weiyun.com"), WeiYunPSKeyPromise)
return if (pskey != null) pskey.getPSkey("weiyun.com") else ""
}
private object WeiYunPSKeyPromise: WtTicketPromise {
override fun Done(ticket: Ticket) {
LogCenter.log("Fav: getPskeyPromise: done", Level.DEBUG)
}
override fun Failed(errMsg: ErrMsg) {
LogCenter.log("Fav: getPskeyPromise: failed, $errMsg", Level.DEBUG)
}
override fun Timeout(errMsg: ErrMsg) {
LogCenter.log("Fav: getPskeyPromise: timeout, $errMsg", Level.DEBUG)
}
}
}

View File

@ -7,4 +7,11 @@ import kotlinx.serialization.Serializable
internal data class ProhibitedMemberInfo(
@SerialName("user_id") val memberUin: Long,
@SerialName("time") val shutuptimestap: Int
)
@Serializable
internal data class GroupAtAllRemainInfo(
@SerialName("can_at_all") val canAtAll: Boolean,
@SerialName("remain_at_all_count_for_group") val remainAtAllCountForGroup: Int,
@SerialName("remain_at_all_count_for_uin") val remainAtAllCountForUin: Int
)

View File

@ -274,7 +274,7 @@ internal object MessageMaker {
element.elementType = MsgConstant.KELEMTYPEREPLY
val reply = ReplyElement()
val msgHash = data["id"].asString.toInt()
val msgHash = data["id"].asInt
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
?: return Result.failure(Exception("不存在该消息映射,无法回复消息"))
@ -627,10 +627,16 @@ internal object MessageMaker {
else -> {
val info = GroupSvc.getTroopMemberInfoByUin(peerId, qq, true).onFailure {
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
}.getOrThrow()
at.content = "@${info.troopnick
.ifNullOrEmpty(info.friendnick)
.ifNullOrEmpty(qq)}"
}.getOrNull()
if (info != null) {
at.content = "@${
info.troopnick
.ifNullOrEmpty(info.friendnick)
.ifNullOrEmpty(qq)
}"
} else {
at.content = "@${data["name"].asStringOrNull.ifNullOrEmpty(qq)}"
}
at.atType = MsgConstant.ATTYPEONE
at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong())
}

View File

@ -53,6 +53,7 @@ internal object MessageConvert {
MsgConstant.KELEMTYPEREPLY to MessageElemConverter.ReplyConverter,
MsgConstant.KELEMTYPEGRAYTIP to MessageElemConverter.GrayTipsConverter,
MsgConstant.KELEMTYPEFILE to MessageElemConverter.FileConverter,
MsgConstant.KELEMTYPEMARKDOWN to MessageElemConverter.MarkdownConverter,
//MsgConstant.KELEMTYPEMULTIFORWARD to MessageElemConverter.XmlMultiMsgConverter,
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElemConverter.XmlLongMsgConverter,
)

View File

@ -324,14 +324,15 @@ internal sealed class MessageElemConverter: IMessageConvert {
val notify = tip.jsonGrayTipElement
when(notify.busiId) {
/* 新人入群 */ 17L, /* 群戳一戳 */1061L,
/* 群撤回 */1014L, /* 群设精消息 */2401L -> {}
/* 群撤回 */1014L, /* 群设精消息 */2401L,
/* 群头衔 */2407L -> {}
else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
}
}
MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
val notify = tip.xmlElement
when(notify.busiId) {
/* 群戳一戳 */1061L -> {}
/* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
}
}
@ -412,6 +413,22 @@ internal sealed class MessageElemConverter: IMessageConvert {
}
}
object MarkdownConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
element: MsgElement
): MessageSegment {
val markdown = element.markdownElement
return MessageSegment(
type = "markdown",
data = mapOf(
"content" to markdown.content
)
)
}
}
protected fun unknownChatType(chatType: Int) {
throw UnsupportedOperationException("Not supported chat type: $chatType")
}

View File

@ -10,7 +10,7 @@ import kotlin.coroutines.resume
internal object ContactHelper {
suspend fun getUinByUidAsync(uid: String): String {
if (uid.isBlank() || uid == "0") {
return "0"
return "-1"
}
val kernelService = NTServiceFetcher.kernelService
@ -20,7 +20,7 @@ internal object ContactHelper {
sessionService.uixConvertService.getUin(hashSetOf(uid)) {
continuation.resume(it)
}
}[uid]?.toString() ?: "0"
}[uid]?.toString() ?: "-1"
}
suspend fun getUidByUinAsync(peerId: Long): String {

View File

@ -9,6 +9,7 @@ import com.tencent.qqnt.msg.api.IMsgService
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
@ -38,7 +39,11 @@ internal object MessageHelper {
): Pair<Long, Int> {
val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, decodeCQCode(message)).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
if (it.second.isEmpty() && !it.first) {
error("消息合成失败,请查看日志或者检查输入。")
} else if (it.second.isEmpty()) {
return System.currentTimeMillis() to 0
}
}.second.filter {
it.elementType != -1
} as ArrayList<MsgElement>
@ -59,6 +64,12 @@ internal object MessageHelper {
}.second.filter {
it.elementType != -1
} as ArrayList<MsgElement>
// ActionMsg No Care
if (msg.isEmpty()) {
return Result.success(System.currentTimeMillis() to 0)
}
val totalSize = msg.filter {
it.elementType == MsgConstant.KELEMTYPEPIC ||
it.elementType == MsgConstant.KELEMTYPEPTT ||
@ -67,11 +78,11 @@ internal object MessageHelper {
(it.picElement?.fileSize ?: 0) + (it.pttElement?.fileSize
?: 0) + (it.videoElement?.fileSize ?: 0)
}.reduceOrNull { a, b -> a + b } ?: 0
val estimateTime = (totalSize / (300 * 1024)) * 1000 + 2000
val estimateTime = (totalSize / (300 * 1024)) * 1000 + 5000
lateinit var sendResultPair: Pair<Long, Int>
val sendRet = withTimeoutOrNull<Pair<Int, String>>(estimateTime) {
suspendCoroutine {
suspendCancellableCoroutine {
GlobalScope.launch {
sendResultPair = sendMessageWithoutMsgId(
chatType,

View File

@ -64,6 +64,7 @@ internal object HTTPServer {
guildAction()
testAction()
requestRouter()
fav()
if (ShamrockConfig.isDev()) {
qsign()
obtainProtocolData()

View File

@ -23,7 +23,7 @@ internal object ActionManager {
// UserActions
GetProfileCard, GetFriendList, SendLike, GetUid, GetUinByUid, ScanQRCode, SetProfileCard,
GetCookies, GetCSRF, GetCredentials, RestartMe, CleanCache, GetModelShow, SetModelShow,
GetModelShowList, GetOnlineClients, GetStrangerInfo, IsBlackListUin, GetHttpCookies,
GetModelShowList, GetOnlineClients, GetStrangerInfo, IsBlackListUin, GetHttpCookies, GetFriendSystemMsg,
// GroupInfo
GetTroopList, GetTroopInfo, GetTroopList, GetTroopMemberInfo, GetTroopMemberList,
@ -31,7 +31,8 @@ internal object ActionManager {
// GroupActions
ModifyTroopName, LeaveTroop, KickTroopMember, BanTroopMember, SetGroupWholeBan, SetGroupAdmin,
ModifyTroopMemberName, SetGroupUnique, GetTroopHonor, GroupPoke, SetEssenceMessage, DeleteEssenceMessage,
GetGroupSystemMsg, GetProhibitedMemberList, GetEssenceMessageList, GetGroupNotice, SendGroupNotice,
GetGroupSystemMsg, GetProhibitedMemberList, GetEssenceMessageList, GetGroupNotice, SendGroupNotice, SendGroupSign,
GetGroupRemainAtAllRemain,
// MSG ACTIONS
SendMessage, DeleteMessage, GetMsg, GetForwardMsg, SendPrivateForwardMessage, SendGroupMessage, SendPrivateMessage,
@ -42,17 +43,20 @@ internal object ActionManager {
DeleteGroupFile, GetGroupFileSystemInfo, GetGroupRootFiles, GetGroupSubFiles,
GetGroupFileUrl, UploadPrivateFile,
//REQUEST ACTION
// REQUEST ACTION
SetFriendAddRequest, SetGroupAddRequest,
// GUILD
GetGuildServiceProfile,
GetGuildServiceProfile, GetGuildList,
// WEATHER
GetWeatherCityCode, GetWeather,
// FAV
FavAddTextMsg, FavAddImageMsg, FavGetItemContent, FavGetItemList,
// OTHER
GetDeviceBattery, DownloadFile
GetDeviceBattery, DownloadFile, QuickOperation
).forEach {
it.alias.forEach { name ->
actionMap[name] = it

View File

@ -0,0 +1,155 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import android.graphics.BitmapFactory
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.utils.CryptTools
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.utils.FileUtils
internal object FavAddImageMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val uin = session.getLong("user_id")
val nickName = session.getString("nick")
val groupName = session.getStringOrNull("group_name") ?: ""
val groupId = session.getLongOrNull("group_id") ?: 0L
val file = session.getString("file")
return invoke(uin, nickName, file, groupName, groupId, session.echo)
}
suspend operator fun invoke(
uin: Long,
nickName: String,
fileText: String,
groupName: String = "",
groupId: Long = 0L,
echo: JsonElement = EmptyJsonString
): String {
val image = fileText.let {
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
if (md5.length == 32) {
FileUtils.getFile(it)
} else {
FileUtils.parseAndSave(it)
}
}
val options = BitmapFactory.Options()
BitmapFactory.decodeFile(image.absolutePath, options)
lateinit var picUrl: String
lateinit var picId: String
lateinit var itemId: String
lateinit var md5: String
QFavSvc.applyUpImageMsg(uin, nickName,
image = image,
groupName = groupName,
groupId = groupId,
width = options.outWidth,
height = options.outHeight
).onSuccess {
if (it.mHttpCode == 200 && it.mResult == 0) {
val readPacket = ByteReadPacket(DeflateTools.ungzip(it.mRespData))
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
}
val data = ByteArray(dataLength).also {
readPacket.readFully(it, 0, it.size)
}
val pb = ProtoUtils.decodeFromByteArray(data)
val resp = pb[2, 20010, 1, 2]
picUrl = resp[1].asUtf8String
picId = resp[11].asUtf8String
md5 = resp[4].asUtf8String
} else {
return logic(it.mErrDesc, echo)
}
}.onFailure {
return error(it.message ?: it.toString(), echo)
}
val sha = CryptTools
.getSHA1("/storage/emulated/0/Android/data/com.tencent.mobileqq/Tencent/QQ_Collection/pic/" + md5.uppercase() + "_0")
image.inputStream().use {
var offset = 0L
val block = ByteArray(131072)
var rest = image.length()
do {
val length = if (rest <= 131072) rest else 131072L
if(it.read(block, 0, length.toInt()) != -1) {
QFavSvc.sendPicUpBlock(
fileSize = image.length(),
offset = offset,
block = block,
blockSize = length,
pid = picId,
sha = sha
).onFailure {
return error(it.message ?: it.toString(), echo)
}
offset += length
rest -= length
} else {
rest = -1
}
} while (rest > 0)
}
QFavSvc.addImageMsg(
uin, nickName, groupId, groupName, picUrl, picId, options.outWidth, options.outHeight, image.length(), md5.uppercase()
).onFailure {
return error(it.message ?: it.toString(), echo)
}.onSuccess {
if (it.mHttpCode == 200 && it.mResult == 0) {
val readPacket = ByteReadPacket(DeflateTools.ungzip(it.mRespData))
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
}
val data = ByteArray(dataLength).also {
readPacket.readFully(it, 0, it.size)
}
val pb = ProtoUtils.decodeFromByteArray(data)
itemId = pb[2, 20009, 1].asUtf8String
}
System.gc()
}
return ok(PicInfo(
picUrl = picUrl,
picId = picId,
id = itemId
), echo)
}
override fun path(): String = "fav.add_image_msg"
override val requiredParams: Array<String> = arrayOf("user_id", "nick", "file")
@Serializable
private data class PicInfo(
@SerialName("pic_url") val picUrl: String,
@SerialName("pic_id") val picId: String,
@SerialName("id") val id: String
)
}

View File

@ -0,0 +1,77 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
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
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
internal object FavAddTextMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val uin = session.getLong("user_id")
val nickName = session.getString("nick")
val groupName = session.getStringOrNull("group_name") ?: ""
val groupId = session.getLongOrNull("group_id") ?: 0L
val time = session.getLongOrNull("time") ?: System.currentTimeMillis()
val content = session.getString("content")
return invoke(uin, nickName, time, content, groupName, groupId, session.echo)
}
suspend operator fun invoke(
uin: Long,
nickName: String,
time: Long = System.currentTimeMillis(),
content: String,
groupName: String = "",
groupId: Long = 0L,
echo: JsonElement = EmptyJsonString
): String {
QFavSvc.addRichMediaMsg(uin, nickName,
time = time,
content = content,
groupName = groupName,
groupId = groupId
).onSuccess {
return if (it.mHttpCode == 200 && it.mResult == 0) {
val readPacket = ByteReadPacket(DeflateTools.ungzip(it.mRespData))
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
}
val data = ByteArray(dataLength).also {
readPacket.readFully(it, 0, it.size)
}
val pb = ProtoUtils.decodeFromByteArray(data)
ok(data = QFavItem(
pb[2, 20009, 1].asUtf8String
), echo)
} else {
logic(it.mErrDesc, echo)
}
}
return ok("请求已提交", echo)
}
override fun path(): String = "fav.add_text_msg"
override val requiredParams: Array<String> = arrayOf("user_id", "nick", "content")
@Serializable
private data class QFavItem(
@SerialName("id") val id: String
)
}

View File

@ -0,0 +1,57 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
internal object FavGetItemContent: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val id = session.getString("id")
return invoke(id, session.echo)
}
suspend operator fun invoke(
id: String,
echo: JsonElement = EmptyJsonString
): String {
val respData = DeflateTools.ungzip(QFavSvc.getItemContent(id).onSuccess {
if (it.mHttpCode != 200 || it.mResult != 0) {
return logic(it.mErrDesc, echo)
}
}.getOrThrow().mRespData)
val readPacket = ByteReadPacket(respData)
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
}
val data = ByteArray(dataLength).also {
readPacket.readFully(it, 0, it.size)
}
val pb = ProtoUtils.decodeFromByteArray(data)
return ok(ItemContent(pb[2, 20001, 1, 8, 2].asUtf8String))
}
override fun path(): String = "fav.get_item_content"
override val requiredParams: Array<String> = arrayOf("id")
@Serializable
private data class ItemContent(
@SerialName("content") val content: String
)
}

View File

@ -0,0 +1,117 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asInt
import moe.fuqiuluo.proto.asList
import moe.fuqiuluo.proto.asLong
import moe.fuqiuluo.proto.asMap
import moe.fuqiuluo.proto.asULong
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
internal object FavGetItemList: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val category = session.getInt("category")
val startPos = session.getInt("start_pos")
val pageSize = session.getInt("page_size")
return invoke(category, startPos, pageSize, session.echo)
}
suspend operator fun invoke(
category: Int,
startPos: Int,
pageSize: Int,
echo: JsonElement = EmptyJsonString
): String {
if (pageSize <= 1) {
return logic("page_size must be greater than 1", echo)
}
val result = DeflateTools.ungzip(QFavSvc.getItemList(
category = category,
startPos = startPos,
pageSize = pageSize
).onSuccess {
if (it.mHttpCode != 200 || it.mResult != 0) {
return logic("fav.get_item_list failed", echo)
}
}.getOrThrow().mRespData)
val readPacket = ByteReadPacket(result)
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
}
val data = ByteArray(dataLength).also {
readPacket.readFully(it, 0, it.size)
}
val pb = ProtoUtils.decodeFromByteArray(data)
val itemList = arrayListOf<Item>()
val rawItemList = pb[2, 20000, 1].asList
rawItemList.value.forEach {
val item = it.asMap
val itemId = item[1].asUtf8String
val authorType = item[4, 1].asInt
val author = item[4, 2].asULong
val authorName = item[4, 3].asUtf8String
val groupName: String
val groupId: Long
if (authorType == 2) {
groupName = item[4, 5].asUtf8String
groupId = item[4, 4].asULong
} else {
groupName = ""
groupId = 0L
}
val clientVersion = item[7].asUtf8String
val time = item[9].asLong
itemList.add(Item(
id = itemId,
authorType = authorType,
author = author,
authorName = authorName,
groupName = groupName,
groupId = groupId,
clientVersion = clientVersion,
time = time
))
}
return ok(ItemList(itemList), echo)
}
override val requiredParams: Array<String> = arrayOf("category", "start_pos", "page_size")
override fun path(): String = "fav.get_item_list"
@Serializable
private data class ItemList(
val items: List<Item>
)
@Serializable
private data class Item(
@SerialName("id") val id: String,
@SerialName("author_type") val authorType: Int,
@SerialName("author") val author: Long,
@SerialName("author_name") val authorName: String,
@SerialName("group_name") val groupName: String,
@SerialName("group_id") val groupId: Long,
@SerialName("client_version") val clientVersion: String,
@SerialName("time") val time: Long
)
}

View File

@ -37,9 +37,9 @@ internal object GetForwardMsg: IActionHandler() {
realId = msg.msgSeq.toInt(),
sender = MessageSender(
msg.senderUin, msg.sendNickName
.ifBlank { msg.sendMemberName }
.ifBlank { msg.sendRemarkName }
.ifBlank { msg.peerName }, "unknown", 0, msg.senderUid
.ifEmpty { msg.sendMemberName }
.ifEmpty { msg.sendRemarkName }
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid
),
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
it.toJson()

View File

@ -0,0 +1,45 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.FriendSvc
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.remote.service.data.FriendRequest
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object GetFriendSystemMsg : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
return invoke(echo = session.echo)
}
suspend operator fun invoke(echo: JsonElement = EmptyJsonString): String {
val list = FriendSvc.requestFriendSystemMsgNew(20)
val msgs = list
// 13 是加别人好友
?.filter { it.msg.sub_type.get() != 13 }
?.map {
LogCenter.log(it.toString(), Level.WARN)
FriendRequest(
seq = it.msg_seq.get(),
userId = it.req_uin.get(),
name = it.msg.req_uin_nick.get(),
source = it.msg.msg_source.get(),
subId = it.msg.src_id.get(),
subSrcId = it.msg.sub_src_id.get(),
msg = it.msg.msg_additional.get(),
sourceGroupName = it.msg.group_name.get(),
sourceGroupCode = it.msg.group_code.get(),
flag = "${it.msg_seq.get()};${it.msg.src_id.get()};${it.msg.sub_src_id.get()};${it.req_uin.get()}",
sex = if (it.msg.req_uin_gender.get() == 1) "female" else "male",
age = it.msg.req_uin_age.get(),
msgDetail = it.msg.msg_detail.get(),
status = it.msg.msg_decided.get()
)
} ?: mutableListOf()
return ok(msgs, echo = echo)
}
override fun path(): String = "get_friend_system_msg"
}

View File

@ -0,0 +1,29 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object GetGroupRemainAtAllRemain: 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 result = GroupSvc.getGroupRemainAtAllRemain(groupId)
if (result.isFailure) {
return error(result.exceptionOrNull()?.message ?: "获取群 @全体成员 剩余次数失败", echo, arrayResult = true)
}
return ok(result.getOrThrow(), echo)
}
override val requiredParams: Array<String> = arrayOf("group_id")
override fun path(): String = "get_group_at_all_remain"
}

View File

@ -61,7 +61,5 @@ internal object GetGroupSystemMsg: IActionHandler() {
return ok(msgs, echo = echo)
}
override val requiredParams: Array<String> = arrayOf("group_id", "folder_id")
override fun path(): String = "get_group_files_by_folder"
override fun path(): String = "get_group_system_msg"
}

View File

@ -0,0 +1,20 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonArray
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object GetGuildList : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
return invoke(echo = session.echo)
}
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
// TODO: get_guild_list
return ok(EmptyJsonArray, echo, "此功能尚未实现")
}
override fun path(): String = "get_guild_list"
}

View File

@ -4,16 +4,19 @@ import com.tencent.mobileqq.qqguildsdk.api.IGPSService
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import mqq.app.MobileQQ
internal object GetGuildServiceProfile: IActionHandler() {
internal object GetGuildServiceProfile : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
TODO("Not yet implemented")
return invoke(echo = session.echo)
}
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
// TODO: get_guild_service_profile
return ok(EmptyJsonObject, echo, "此功能尚未实现")
val service = AppRuntimeFetcher.appRuntime
.getRuntimeService(IGPSService::class.java, "all")
if (!service.isGProSDKInitCompleted) {

View File

@ -85,9 +85,9 @@ internal object GetHistoryMsg: IActionHandler() {
realId = seq,
sender = MessageSender(
msg.senderUin, msg.sendNickName
.ifBlank { msg.sendMemberName }
.ifBlank { msg.sendRemarkName }
.ifBlank { msg.peerName }, "unknown", 0, msg.senderUid
.ifEmpty { msg.sendMemberName }
.ifEmpty { msg.sendRemarkName }
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid
),
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
it.toJson()

View File

@ -30,9 +30,9 @@ internal object GetMsg: IActionHandler() {
realId = seq,
sender = MessageSender(
msg.senderUin, msg.sendNickName
.ifBlank { msg.sendMemberName }
.ifBlank { msg.sendRemarkName }
.ifBlank { msg.peerName }, "unknown", 0, msg.senderUid
.ifEmpty { msg.sendMemberName }
.ifEmpty { msg.sendRemarkName }
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid
),
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
it.toJson()

View File

@ -29,7 +29,7 @@ internal object GetProfileCard: IActionHandler() {
uin = card.uin.toLong(),
name = card.strNick,
mail = card.strShowName ?: card.strEmail ?: "",
remark = card.strReMark.let { if (it.isNullOrBlank()) card.strAutoRemark else it },
remark = card.strReMark.let { if (it.isNullOrEmpty()) card.strAutoRemark else it },
findMethod = card.addSrcName,
displayName = card.strContactName,
maxVoteCnt = card.bAvailVoteCnt,

View File

@ -30,7 +30,7 @@ internal object GetTroopHonor: IActionHandler() {
GroupSvc.parseHonor(member.honorList).forEach {
val honor = nativeDecodeHonor(member.memberuin, it, member.mHonorRichFlag)
if (honor != null) {
honor.nick = member.troopnick.ifBlank { member.friendnick }
honor.nick = member.troopnick.ifEmpty { member.friendnick }
honorInfo.add(honor)
}
}

View File

@ -58,7 +58,9 @@ internal object GetTroopMemberInfo : IActionHandler() {
unfriendly = false,
title = info.mUniqueTitle ?: "",
titleExpireTime = info.mUniqueTitleExpire,
cardChangeable = GroupSvc.isAdmin(groupId)
cardChangeable = GroupSvc.isAdmin(groupId),
age = info.age.toInt(),
shutUpTimestamp = 0L
), echo
)
}

View File

@ -25,7 +25,9 @@ internal object GetTroopMemberList : IActionHandler() {
val memberList = GroupSvc.getGroupMemberList(groupId, refresh).onFailure {
return error(it.message ?: "unknown error", echo, arrayResult = true)
}.getOrThrow()
val prohibitedMemberList = GroupSvc.getProhibitedMemberList(groupId.toLong())
.getOrDefault(arrayListOf())
.associate { it.memberUin to it.shutuptimestap.toLong() }
return ok(arrayListOf<SimpleTroopMemberInfo>().apply {
memberList.forEach { info ->
if (info.memberuin != "0") {
@ -59,7 +61,9 @@ internal object GetTroopMemberList : IActionHandler() {
unfriendly = false,
title = info.mUniqueTitle ?: "",
titleExpireTime = info.mUniqueTitleExpire,
cardChangeable = GroupSvc.isAdmin(groupId)
cardChangeable = GroupSvc.isAdmin(groupId),
age = 0,
shutUpTimestamp = prohibitedMemberList[info.memberuin.toLong()] ?: 0L
)
)
}

View File

@ -0,0 +1,148 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.HttpService
import moe.fuqiuluo.shamrock.tools.asBoolean
import moe.fuqiuluo.shamrock.tools.asBooleanOrNull
import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asIntOrNull
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asLong
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.tools.jsonArray
internal object QuickOperation: IActionHandler() {
val actionMsgTypes = arrayOf(
"record", "voice", "video", "markdown"
)
override suspend fun internalHandle(session: ActionSession): String {
val botId = session.getLong("self_id")
if (botId != TicketSvc.getLongUin()) {
return logic("当前登录账号和输入的`self_id`不一致", session.echo)
}
val context = session.getObject("context")
//val msgType = context["message_type"].asString
val msgHash = context["message_id"].asInt
//val peerId = context[when(msgType) {
// "group" -> "group_id"
// "private" -> "user_id"
// else -> error("unknown message type: $msgType")
//}].asLong
val record = MsgSvc.getMsg(msgHash).getOrNull()
?: return logic("获取源消息失败", session.echo)
val operation = session.getObject("operation")
if (operation.containsKey("reply")) {
LogCenter.log({ "websocket quickly reply successfully" }, Level.DEBUG)
val autoEscape = operation["auto_escape"].asBooleanOrNull ?: false
val atSender = operation["at_sender"].asBooleanOrNull ?: false
val autoReply = operation["auto_reply"].asBooleanOrNull ?: true
val message = operation["reply"]
if (message is JsonPrimitive) {
if (autoEscape) {
val msgList = mutableSetOf<JsonElement>()
msgList.add(mapOf(
"type" to "text",
"data" to mapOf(
"text" to message.asString
)
).json)
quicklyReply(
record,
msgList.jsonArray,
msgHash,
atSender,
autoReply
)
} else {
val messageArray = MessageHelper.decodeCQCode(message.asString)
quicklyReply(
record,
messageArray,
msgHash,
atSender,
autoReply
)
}
} else if (message is JsonArray) {
quicklyReply(
record,
message,
msgHash,
atSender,
autoReply
)
}
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && operation.containsKey("delete") && operation["delete"].asBoolean) {
MsgSvc.recallMsg(msgHash)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && operation.containsKey("kick") && operation["kick"].asBoolean) {
GroupSvc.kickMember(record.peerUin, false, record.senderUin)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && operation.containsKey("ban") && operation["ban"].asBoolean) {
val banTime = operation["ban_duration"].asIntOrNull ?: (30 * 60)
if (banTime <= 0) return logic("禁言时间必须大于0", session.echo)
GroupSvc.banMember(record.peerUin, record.senderUin, banTime)
}
return logic("操作成功", session.echo)
}
override fun path(): String = ".handle_quick_operation_async"
override val requiredParams: Array<String> = arrayOf("context", "operation", "self_id")
suspend fun quicklyReply(
record: MsgRecord,
message: JsonArray,
msgHash: Int,
atSender: Boolean,
autoReply: Boolean
) {
val messageList = mutableListOf<JsonElement>()
message.filter {
it.asJsonObject["type"]?.asString in actionMsgTypes
}.let {
if (it.isNotEmpty()) {
it.map { listOf(it) }.forEach {
MsgSvc.sendToAio(record.chatType, record.peerUin.toString(), it.jsonArray)
}
return
}
}
if (autoReply) messageList.add(mapOf(
"type" to "reply",
"data" to mapOf(
"id" to msgHash
)
).json) // 添加回复
if (MsgConstant.KCHATTYPEGROUP == record.chatType && atSender) {
messageList.add(mapOf(
"type" to "at",
"data" to mapOf(
"qq" to record.senderUin
)
).json) // 添加@发送者
}
messageList.addAll(message)
MsgSvc.sendToAio(record.chatType, record.peerUin.toString(), JsonArray(messageList))
}
}

View File

@ -2,9 +2,7 @@ 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 kotlinx.serialization.json.*
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
@ -18,19 +16,6 @@ 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")
@ -39,31 +24,33 @@ internal object SendForwardMessage : IActionHandler() {
MessageHelper.obtainMessageTypeByDetailType(it)
} ?: run {
if (session.has("user_id")) {
MsgConstant.KCHATTYPEC2C
if (session.has("group_id")) {
MsgConstant.KCHATTYPETEMPC2CFROMGROUP
} else {
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
)
val peerId = when(chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo)
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> 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)
val fromId = when(chatType) {
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> 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")
}
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
invoke(chatType, peerId, messages, fromId, echo = session.echo)
} else {
logic("未知格式合并转发消息", session.echo)
}
return logic("未知格式合并转发消息", session.echo)
} catch (e: ParamsException) {
return noParam(e.message!!, session.echo)
} catch (e: Throwable) {
@ -74,7 +61,8 @@ internal object SendForwardMessage : IActionHandler() {
suspend operator fun invoke(
chatType: Int,
peerId: String,
message: JsonArray,
messages: JsonArray,
fromId: String = peerId,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
@ -83,63 +71,91 @@ internal object SendForwardMessage : IActionHandler() {
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)
val multiNodes = messages.map {
if (it.asJsonObject["type"].asStringOrNull != "node") {
LogCenter.log("包含非node类型节点", Level.WARN)
return@map null
}
}.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 ->
if (it.asJsonObject["data"] !is JsonObject) {
LogCenter.log("data字段错误", Level.WARN)
return@map null
}
it.asJsonObject["data"].asJsonObject.let { data ->
if (data.containsKey("id")) {
val record = MsgSvc.getMsg(data["id"].asInt).getOrNull()
if (record == null) {
LogCenter.log("合并转发消息节点消息获取失败:${data["id"]}", Level.WARN)
return@map null
} else {
record.peerName to record.toSegments().map { segment ->
segment.toJson()
}.json
)
}
} else if (data.containsKey("content")) {
(data["name"].asStringOrNull ?: "Anno") to when (val raw = data["content"]) {
is JsonObject -> raw.asJsonArray
is JsonArray -> raw.asJsonArray
else -> MessageHelper.decodeCQCode(raw.asString)
}
} else {
LogCenter.log("消息节点缺少id或content字段", Level.WARN)
return@map null
}
} 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)
}.let { node ->
val content = node.second.map { msg ->
when (msg.asJsonObject["type"].asStringOrNull ?: "text") {
"at" -> {
buildJsonObject {
put("type", "text")
putJsonObject("data") {
put(
"text", "@${
msg.asJsonObject["data"].asJsonObject["name"].asStringOrNull.ifNullOrEmpty(
msg.asJsonObject["data"].asJsonObject["qq"].asString
)
}"
)
}
}
}
"voice" -> {
buildJsonObject {
put("type", "text")
putJsonObject("data") {
put("text", "[语音]")
}
}
}
"node" -> {
LogCenter.log("合并转发消息暂时不支持嵌套", Level.WARN)
buildJsonObject {
put("type", "text")
putJsonObject("data") {
put("text", "[合并转发消息]")
}
}
}
else -> msg
}
}.json
val result = MessageHelper.sendMessageNoCb(MsgConstant.KCHATTYPEC2C, selfUin, content)
if (result.first != 0) {
LogCenter.log("合并转发消息节点消息发送失败", Level.WARN)
}
})
if (result.first != 0) {
LogCenter.log("合并转发消息节点消息发送失败", Level.WARN)
result.second to node.first
}
return@map result.second
}
}.filterNotNull()
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
val to = MessageHelper.generateContact(chatType, peerId)
val to = MessageHelper.generateContact(chatType, peerId, fromId)
val uniseq = MessageHelper.generateMsgId(chatType)
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
nodes.forEach { add(MultiMsgInfo(it, "Anno")) }
multiNodes.forEach { add(MultiMsgInfo(it.first, it.second)) }
}.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.first))
return ok(
@ -154,7 +170,7 @@ internal object SendForwardMessage : IActionHandler() {
return logic("合并转发消息失败(unknown error)", echo)
}
override val requiredParams: Array<String> = arrayOf("message")
override val requiredParams: Array<String> = arrayOf("messages")
override fun path(): String = "send_forward_msg"
}

View File

@ -9,7 +9,7 @@ internal object SendGroupForwardMessage: IActionHandler() {
val groupId = session.getString("group_id")
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId, messages, session.echo)
SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId, messages, echo = session.echo)
} else {
logic("未知格式合并转发消息", session.echo)
}

View File

@ -8,16 +8,18 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
internal object SendGroupMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getString("group_id")
val retryCnt = session.getIntOrNull("retry_cnt")
val recallDuration = session.getLongOrNull("recall_duration")
return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, message, autoEscape, echo = session.echo)
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, message, autoEscape, echo = session.echo, retryCnt = retryCnt ?: 3, recallDuration = recallDuration)
} else if (session.isObject("message")) {
val message = session.getObject("message")
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, listOf( message ).jsonArray, session.echo)
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, listOf( message ).jsonArray, session.echo, retryCnt = retryCnt ?: 3, recallDuration = recallDuration)
} else {
val message = session.getArray("message")
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, message, session.echo)
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, message, session.echo, retryCnt = retryCnt ?: 3, recallDuration = recallDuration)
}
}

View File

@ -0,0 +1,30 @@
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 SendGroupSign: 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 ret = GroupSvc.groupSign(groupId)
return if (ret.isSuccess) {
ok(ret.getOrNull() ?: "", echo)
} else {
logic(ret.exceptionOrNull()?.message ?: "", echo)
}
}
override val requiredParams: Array<String> = arrayOf("group_id")
override fun path(): String = "send_group_sign"
}

View File

@ -1,6 +1,10 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.helper.MessageHelper
@ -20,11 +24,15 @@ internal object SendMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type")
try {
var chatType = detailType?.let {
val chatType = detailType?.let {
MessageHelper.obtainMessageTypeByDetailType(it)
} ?: run {
if (session.has("user_id")) {
MsgConstant.KCHATTYPEC2C
if (session.has("group_id")) {
MsgConstant.KCHATTYPETEMPC2CFROMGROUP
} else {
MsgConstant.KCHATTYPEC2C
}
} else if (session.has("group_id")) {
MsgConstant.KCHATTYPEGROUP
} else {
@ -33,27 +41,26 @@ internal object SendMessage: IActionHandler() {
}
val peerId = when(chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo)
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id") ?: return noParam("user_id", session.echo)
else -> error("unknown chat type: $chatType")
}
val fromId = when(chatType) {
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> 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")
}
var fromId = peerId
if (chatType == MsgConstant.KCHATTYPEC2C) {
val groupId = session.getStringOrNull("group_id")
if (groupId != null) {
chatType = MsgConstant.KCHATTYPETEMPC2CFROMGROUP
fromId = groupId
}
}
val retryCnt = session.getIntOrNull("retry_cnt")
val recallDuration = session.getLongOrNull("recall_duration")
return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
invoke(chatType, peerId, message, autoEscape, echo = session.echo, fromId = fromId)
invoke(chatType, peerId, message, autoEscape, echo = session.echo, fromId = fromId, retryCnt = retryCnt ?: 3, recallDuration = recallDuration)
} else if (session.isArray("message")) {
val message = session.getArray("message")
invoke(chatType, peerId, message, session.echo, fromId = fromId)
invoke(chatType, peerId, message, session.echo, fromId = fromId, retryCnt ?: 3, recallDuration = recallDuration)
} else {
val message = session.getObject("message")
invoke(chatType, peerId, listOf( message ).jsonArray, session.echo, fromId = fromId)
invoke(chatType, peerId, listOf( message ).jsonArray, session.echo, fromId = fromId, retryCnt ?: 3, recallDuration = recallDuration)
}
} catch (e: ParamsException) {
return noParam(e.message!!, session.echo)
@ -69,6 +76,8 @@ internal object SendMessage: IActionHandler() {
message: String,
autoEscape: Boolean,
fromId: String = peerId,
retryCnt: Int,
recallDuration: Long?,
echo: JsonElement = EmptyJsonString
): String {
//if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
@ -89,7 +98,7 @@ internal object SendMessage: IActionHandler() {
LogCenter.log("CQ码不合法", Level.WARN)
return logic("CQCode is illegal", echo)
} else {
MsgSvc.sendToAio(chatType, peerId, msg, fromId = fromId)
MsgSvc.sendToAio(chatType, peerId, msg, fromId = fromId, retryCnt)
}
}
if (result.isFailure) {
@ -99,6 +108,7 @@ internal object SendMessage: IActionHandler() {
if (pair.first <= 0) {
return logic("send message failed", echo = echo)
}
recallDuration?.let { autoRecall(pair.second, it) }
return ok(MessageResult(
msgId = pair.second,
time = (pair.first * 0.001).toLong()
@ -107,12 +117,12 @@ internal object SendMessage: IActionHandler() {
// 消息段格式消息
suspend operator fun invoke(
chatType: Int, peerId: String, message: JsonArray, echo: JsonElement = EmptyJsonString, fromId: String = peerId
chatType: Int, peerId: String, message: JsonArray, echo: JsonElement = EmptyJsonString, fromId: String = peerId, retryCnt: Int, recallDuration: Long?,
): String {
//if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
// return logic("contact is not found", echo = echo)
//}
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId)
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt)
if (result.isFailure) {
return logic(result.exceptionOrNull()?.message ?: "", echo)
}
@ -120,12 +130,20 @@ internal object SendMessage: IActionHandler() {
if (pair.first <= 0) {
return logic("send message failed", echo = echo)
}
recallDuration?.let { autoRecall(pair.second, it) }
return ok(MessageResult(
msgId = pair.second,
time = (pair.first * 0.001).toLong()
), echo)
}
private fun autoRecall(msgHash: Int, duration: Long) {
GlobalScope.launch(Dispatchers.Default) {
delay(duration)
MsgSvc.recallMsg(msgHash)
}
}
override val requiredParams: Array<String> = arrayOf("message")
override fun path(): String = "send_message"

View File

@ -9,7 +9,7 @@ internal object SendPrivateForwardMessage : IActionHandler() {
val userId = session.getString("user_id")
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId, messages, session.echo)
SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId, messages, echo = session.echo)
} else {
logic("未知格式合并转发消息", session.echo)
}

View File

@ -10,6 +10,8 @@ internal object SendPrivateMessage: IActionHandler() {
val userId = session.getString("user_id")
val groupId = session.getStringOrNull("group_id")
val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
val retryCnt = session.getIntOrNull("retry_cnt")
val recallDuration = session.getLongOrNull("recall_duration")
return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
@ -19,7 +21,9 @@ internal object SendPrivateMessage: IActionHandler() {
message = message,
autoEscape = autoEscape,
echo = session.echo,
fromId = groupId ?: userId
fromId = groupId ?: userId,
retryCnt = retryCnt ?: 3,
recallDuration = recallDuration
)
} else if (session.isArray("message")) {
val message = session.getArray("message")
@ -28,7 +32,9 @@ internal object SendPrivateMessage: IActionHandler() {
peerId = userId,
message = message,
echo = session.echo,
fromId = groupId ?: userId
fromId = groupId ?: userId,
retryCnt = retryCnt ?: 3,
recallDuration = recallDuration
)
} else {
val message = session.getObject("message")
@ -37,7 +43,9 @@ internal object SendPrivateMessage: IActionHandler() {
peerId = userId,
message = listOf( message ).jsonArray,
echo = session.echo,
fromId = groupId ?: userId
fromId = groupId ?: userId,
retryCnt = retryCnt ?: 3,
recallDuration = recallDuration
)
}
}

View File

@ -25,7 +25,7 @@ internal object SetFriendAddRequest: IActionHandler() {
if (ts.toString().length < 13) {
// time but not seq, query seq again
val reqs = FriendSvc.requestFriendSystemMsgNew(20, 0, 0, 1)
val req = reqs?.first {
val req = reqs?.firstOrNull {
it.msg_time.get() == ts
}
// 好友请求seq貌似就是time*1000查不到直接*1000

View File

@ -27,10 +27,10 @@ internal object SetGroupAddRequest: IActionHandler() {
var reqs = GroupSvc.requestGroupSystemMsgNew(20, 1)
val riskReqs = GroupSvc.requestGroupSystemMsgNew(20, 2)
reqs = reqs + riskReqs
val req = reqs.first {
val req = reqs.firstOrNull {
it.msg_time.get() == ts
}
ts = req.msg_seq?.get() ?: return error("失败:未找到该请求", echo)
ts = req?.msg_seq?.get() ?: return error("失败:未找到该请求", echo)
}
} catch (err: Throwable) {
LogCenter.log(err.stackTraceToString(), Level.WARN)

View File

@ -0,0 +1,48 @@
package moe.fuqiuluo.shamrock.remote.api
import io.ktor.http.ContentType
import io.ktor.server.application.call
import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import moe.fuqiuluo.shamrock.remote.action.handlers.FavAddImageMsg
import moe.fuqiuluo.shamrock.remote.action.handlers.FavAddTextMsg
import moe.fuqiuluo.shamrock.remote.action.handlers.FavGetItemContent
import moe.fuqiuluo.shamrock.remote.action.handlers.FavGetItemList
import moe.fuqiuluo.shamrock.tools.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.getOrPost
// fav.add_rich_media_msg
fun Routing.fav() {
getOrPost("/fav/add_rich_media_msg") {
val uin = call.fetchOrThrow("user_id").toLong()
val nickName = call.fetchOrThrow("nick")
val time = call.fetchOrNull("time")?.toLong() ?: System.currentTimeMillis()
val content = call.fetchOrThrow("content")
val groupName = call.fetchOrNull("group_name") ?: ""
val groupId = call.fetchOrNull("group_id")?.toLong() ?: 0L
call.respondText(FavAddTextMsg(uin, nickName, time, content, groupName, groupId), ContentType.Application.Json)
}
getOrPost("/fav/add_image_msg") {
val uin = call.fetchOrThrow("user_id").toLong()
val nickName = call.fetchOrThrow("nick")
val file = call.fetchOrThrow("file")
val groupName = call.fetchOrNull("groupName") ?: ""
val groupId = call.fetchOrNull("group_id")?.toLong() ?: 0L
call.respondText(FavAddImageMsg(uin, nickName, file, groupName, groupId), ContentType.Application.Json)
}
getOrPost("/fav/get_item_content") {
val id = call.fetchOrThrow("id")
call.respondText(FavGetItemContent(id), ContentType.Application.Json)
}
getOrPost("/fav/get_item_list") {
val category = call.fetchOrThrow("category").toInt()
val startPos = call.fetchOrThrow("start_pos").toInt()
val pageSize = call.fetchOrThrow("page_size").toInt()
call.respondText(FavGetItemList(category, startPos, pageSize), ContentType.Application.Json)
}
}

View File

@ -8,6 +8,7 @@ import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import moe.fuqiuluo.shamrock.remote.action.handlers.GetFriendList
import moe.fuqiuluo.shamrock.remote.action.handlers.GetFriendSystemMsg
import moe.fuqiuluo.shamrock.remote.action.handlers.GetStrangerInfo
import moe.fuqiuluo.shamrock.remote.action.handlers.IsBlackListUin
import moe.fuqiuluo.shamrock.tools.fetchGetOrThrow
@ -30,4 +31,9 @@ fun Routing.friendAction() {
val uin = fetchOrThrow("user_id")
call.respondText(IsBlackListUin(uin), ContentType.Application.Json)
}
getOrPost("/get_friend_system_msg") {
call.respondText(GetFriendSystemMsg(), ContentType.Application.Json)
}
}

View File

@ -140,4 +140,14 @@ fun Routing.troopAction() {
call.respondText(SendGroupNotice(groupId, text, image), ContentType.Application.Json)
}
getOrPost("/send_group_sign") {
val groupId = fetchOrThrow("group_id").toLong()
call.respondText(SendGroupSign(groupId), ContentType.Application.Json)
}
getOrPost("/get_group_at_all_remain") {
val groupId = fetchOrThrow("group_id").toLong()
call.respondText(GetGroupRemainAtAllRemain(groupId), ContentType.Application.Json)
}
}

View File

@ -1,6 +1,7 @@
package moe.fuqiuluo.shamrock.remote.api
import io.ktor.http.ContentType
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.request.httpVersion
import io.ktor.server.response.respondText
@ -8,8 +9,12 @@ import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.util.pipeline.PipelineContext
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.shamrock.remote.HTTPServer
import moe.fuqiuluo.shamrock.remote.action.ActionManager
import moe.fuqiuluo.shamrock.remote.action.ActionSession
@ -18,12 +23,17 @@ import moe.fuqiuluo.shamrock.remote.entries.EmptyObject
import moe.fuqiuluo.shamrock.remote.entries.IndexData
import moe.fuqiuluo.shamrock.remote.entries.Status
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElement
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElementOrNull
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObjectOrNull
import moe.fuqiuluo.shamrock.tools.isJsonArray
import moe.fuqiuluo.shamrock.tools.isJsonData
import moe.fuqiuluo.shamrock.tools.isJsonObject
import moe.fuqiuluo.shamrock.tools.isJsonString
import moe.fuqiuluo.shamrock.tools.json
@ -39,6 +49,31 @@ data class OldApiResult<T>(
val data: T? = null
)
suspend fun PipelineContext<Unit, ApplicationCall>.handleAsJsonObject(data: JsonObject) {
val action = data["action"].asString
val echo = data["echo"] ?: EmptyJsonString
call.attributes.put(ECHO_KEY, echo)
val params = data["params"].asJsonObjectOrNull ?: EmptyJsonObject
val handler = ActionManager[action]
if (handler == null) {
respond(false, Status.UnsupportedAction, EmptyObject, "不支持的Action", echo = echo)
} else {
call.respondText(handler.handle(ActionSession(params, echo)), ContentType.Application.Json)
}
}
suspend fun PipelineContext<Unit, ApplicationCall>.handleAsJsonArray(data: JsonArray) {
data.forEach {
when (it) {
is JsonArray -> handleAsJsonArray(it)
is JsonObject -> handleAsJsonObject(it)
else -> handleAsJsonObject(it.jsonObject)
}
}
}
fun Routing.echoVersion() {
route("/") {
get {
@ -49,6 +84,15 @@ fun Routing.echoVersion() {
)
}
post {
fetchPostJsonElementOrNull()?.let {
if (it is JsonArray) {
handleAsJsonArray(it)
return@post
} else if (it is JsonObject) {
handleAsJsonObject(it)
return@post
}
}
val action = fetchOrThrow("action")
val echo = if (isJsonObject("echo") || isJsonArray("echo")) {
fetchPostJsonElement("echo")

View File

@ -29,7 +29,7 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
import moe.fuqiuluo.shamrock.tools.respond
fun Routing.messageAction() {
route("/send_group_forward_msg") {
route("/send_group_forward_(msg|message)".toRegex()) {
post {
val groupId = fetchPostOrNull("group_id")
val messages = fetchPostJsonArray("messages")
@ -40,7 +40,7 @@ fun Routing.messageAction() {
}
}
route("/send_private_forward_msg") {
route("/send_private_forward_(msg|message)".toRegex()) {
post {
val userId = fetchPostOrNull("user_id")
val messages = fetchPostJsonArray("messages")
@ -51,6 +51,18 @@ fun Routing.messageAction() {
}
}
route("/send_forward_(msg|message)".toRegex()) {
post {
val userId = fetchPostOrNull("user_id")
val groupId = fetchPostOrNull("group_id")
val messages = fetchPostJsonArray("messages")
call.respondText(SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: groupId?: "", messages), ContentType.Application.Json)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
getOrPost("/get_forward_msg") {
val id = fetchOrThrow("id")
call.respondText(GetForwardMsg(id), ContentType.Application.Json)
@ -101,27 +113,34 @@ fun Routing.messageAction() {
get {
val msgType = fetchGetOrThrow("message_type")
val message = fetchGetOrThrow("message")
val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 3
val autoEscape = fetchGetOrNull("auto_escape")?.toBooleanStrict() ?: false
val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
val userId = fetchGetOrNull("user_id")
val groupId = fetchGetOrNull("group_id")
val recallDuration = fetchGetOrNull("recall_duration")?.toLongOrNull()
call.respondText(SendMessage(
chatType = chatType,
peerId = if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!,
message = message,
autoEscape = autoEscape,
fromId = groupId ?: userId ?: ""
fromId = groupId ?: userId ?: "",
retryCnt = retryCnt,
recallDuration = recallDuration
), ContentType.Application.Json)
}
post {
val msgType = fetchPostOrThrow("message_type")
val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 3
val userId = fetchPostOrNull("user_id")
val groupId = fetchPostOrNull("group_id")
val peerId = if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!
val recallDuration = fetchPostOrNull("recall_duration")?.toLongOrNull()
call.respondText(if (isJsonData() && !isJsonString("message")) {
if (isJsonObject("message")) {
@ -129,14 +148,18 @@ fun Routing.messageAction() {
chatType = chatType,
peerId = peerId,
message = listOf(fetchPostJsonObject("message")).jsonArray,
fromId = groupId ?: userId ?: ""
fromId = groupId ?: userId ?: "",
retryCnt = retryCnt,
recallDuration = recallDuration
)
} else {
SendMessage(
chatType = chatType,
peerId = peerId,
message = fetchPostJsonArray("message"),
fromId = groupId ?: userId ?: ""
fromId = groupId ?: userId ?: "",
retryCnt = retryCnt,
recallDuration = recallDuration
)
}
} else {
@ -147,7 +170,9 @@ fun Routing.messageAction() {
peerId = peerId,
message = fetchPostOrThrow("message"),
autoEscape = autoEscape,
fromId = groupId ?: userId ?: ""
fromId = groupId ?: userId ?: "",
retryCnt = retryCnt,
recallDuration = recallDuration
)
}, ContentType.Application.Json)
}
@ -157,34 +182,43 @@ fun Routing.messageAction() {
get {
val groupId = fetchGetOrThrow("group_id")
val message = fetchGetOrThrow("message")
val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 3
val autoEscape = fetchGetOrNull("auto_escape")?.toBooleanStrict() ?: false
call.respondText(SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, message, autoEscape))
val recallDuration = fetchGetOrNull("recall_duration")?.toLongOrNull()
call.respondText(SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, message, autoEscape, retryCnt = retryCnt, recallDuration = recallDuration), ContentType.Application.Json)
}
post {
val groupId = fetchPostOrThrow("group_id")
val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 3
val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false
val recallDuration = fetchPostOrNull("recall_duration")?.toLongOrNull()
val result = if (isJsonData()) {
if (isJsonString("message")) {
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, fetchPostJsonString("message"), autoEscape)
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, fetchPostJsonString("message"), autoEscape, retryCnt = retryCnt, recallDuration = recallDuration)
} else {
if (isJsonObject("message")) {
SendMessage(
chatType = MsgConstant.KCHATTYPEGROUP,
peerId = groupId,
message = listOf(fetchPostJsonObject("message")).jsonArray
message = listOf(fetchPostJsonObject("message")).jsonArray,
retryCnt = retryCnt,
recallDuration = recallDuration
)
} else {
SendMessage(
chatType = MsgConstant.KCHATTYPEGROUP,
peerId = groupId,
message = fetchPostJsonArray("message")
message = fetchPostJsonArray("message"),
retryCnt = retryCnt,
recallDuration = recallDuration
)
}
}
} else {
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, fetchPostOrThrow("message"), autoEscape)
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId, fetchPostOrThrow("message"), autoEscape, retryCnt = retryCnt, recallDuration = recallDuration)
}
call.respondText(result, ContentType.Application.Json)
@ -196,23 +230,27 @@ fun Routing.messageAction() {
val userId = fetchGetOrThrow("user_id")
val groupId = fetchGetOrNull("group_id")
val message = fetchGetOrThrow("message")
val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 3
val autoEscape = fetchGetOrNull("auto_escape")?.toBooleanStrict() ?: false
val recallDuration = fetchGetOrNull("recall_duration")?.toLongOrNull()
call.respondText(SendMessage(
chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
peerId = userId,
message = message,
autoEscape = autoEscape,
fromId = groupId ?: userId
fromId = groupId ?: userId,
retryCnt = retryCnt, recallDuration = recallDuration
), ContentType.Application.Json)
}
post {
val userId = fetchPostOrThrow("user_id")
val groupId = fetchPostOrNull("group_id")
val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 3
val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false
val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
val fromId = groupId ?: userId
val recallDuration = fetchPostOrNull("recall_duration")?.toLongOrNull()
val result = if (isJsonData()) {
if (isJsonString("message")) {
@ -221,7 +259,8 @@ fun Routing.messageAction() {
peerId = userId,
message = fetchPostJsonString("message"),
autoEscape = autoEscape,
fromId = fromId
fromId = fromId,
retryCnt = retryCnt, recallDuration = recallDuration
)
} else {
if (isJsonObject("message")) {
@ -229,14 +268,16 @@ fun Routing.messageAction() {
chatType = chatType,
peerId = userId,
message = listOf(fetchPostJsonObject("message")).jsonArray,
fromId = fromId
fromId = fromId,
retryCnt = retryCnt, recallDuration = recallDuration
)
} else {
SendMessage(
chatType = chatType,
peerId = userId,
message = fetchPostJsonArray("message"),
fromId = fromId
fromId = fromId,
retryCnt = retryCnt, recallDuration = recallDuration
)
}
}
@ -246,7 +287,8 @@ fun Routing.messageAction() {
peerId = userId,
message = fetchPostOrThrow("message"),
autoEscape = autoEscape,
fromId = fromId
fromId = fromId,
retryCnt = retryCnt, recallDuration = recallDuration
)
}

View File

@ -5,6 +5,8 @@ import moe.fuqiuluo.shamrock.helper.ErrorTokenException
import io.ktor.server.application.createApplicationPlugin
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.fetchOrNull
import java.net.URLDecoder
import java.nio.charset.Charset
private suspend fun ApplicationCall.checkToken() {
val token = ShamrockConfig.getToken()
@ -12,10 +14,17 @@ private suspend fun ApplicationCall.checkToken() {
return
}
var accessToken = request.headers["Authorization"]
?: fetchOrNull("ticket")
?: fetchOrNull("access_token")
?: fetchOrNull("ticket")?.let {
URLDecoder.decode(it)
}
?: fetchOrNull("access_token")?.let {
URLDecoder.decode(it)
}
?: fetchOrNull("token")?.let {
URLDecoder.decode(it)
}
?: throw ErrorTokenException
if (accessToken.startsWith("Bearer ")) {
if (accessToken.startsWith("Bearer ", ignoreCase = true)) {
accessToken = accessToken.substring(7)
}
if (token != accessToken) {

View File

@ -7,6 +7,9 @@ import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.server.application.call
import io.ktor.server.response.respondText
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
@ -14,6 +17,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import moe.fuqiuluo.qqinterface.servlet.msg.*
import moe.fuqiuluo.shamrock.remote.service.api.HttpTransmitServlet
@ -21,13 +25,15 @@ import moe.fuqiuluo.shamrock.remote.service.data.push.*
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.action.ActionManager
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.handlers.QuickOperation.quicklyReply
import moe.fuqiuluo.shamrock.remote.config.ECHO_KEY
import moe.fuqiuluo.shamrock.remote.entries.EmptyObject
import moe.fuqiuluo.shamrock.remote.entries.Status
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
internal object HttpService: HttpTransmitServlet() {
private val actionMsgTypes = arrayOf(
"record", "voice", "video", "markdown"
)
private val jobList = arrayListOf<Job>()
override fun submitFlowJob(job: Job) {
@ -66,99 +72,83 @@ internal object HttpService: HttpTransmitServlet() {
private suspend fun handleQuicklyReply(record: MsgRecord, msgHash: Int, jsonText: String) {
try {
val data = Json.parseToJsonElement(jsonText).asJsonObject
if (data.containsKey("reply")) {
LogCenter.log({ "quickly reply successfully" }, Level.DEBUG)
val autoEscape = data["auto_escape"].asBooleanOrNull ?: false
val atSender = data["at_sender"].asBooleanOrNull ?: false
val autoReply = data["auto_reply"].asBooleanOrNull ?: true
val message = data["reply"]
if (message is JsonPrimitive) {
if (autoEscape) {
val msgList = mutableSetOf<JsonElement>()
msgList.add(mapOf(
"type" to "text",
"data" to mapOf(
"text" to message.asString
val data = Json.parseToJsonElement(jsonText)
if (data is JsonObject) {
if (data.containsKey("reply")) {
LogCenter.log({ "quickly reply successfully" }, Level.DEBUG)
val autoEscape = data["auto_escape"].asBooleanOrNull ?: false
val atSender = data["at_sender"].asBooleanOrNull ?: false
val autoReply = data["auto_reply"].asBooleanOrNull ?: true
val message = data["reply"]
if (message is JsonPrimitive) {
if (autoEscape) {
val msgList = mutableSetOf<JsonElement>()
msgList.add(mapOf(
"type" to "text",
"data" to mapOf(
"text" to message.asString
)
).json)
quicklyReply(
record,
msgList.jsonArray,
msgHash,
atSender,
autoReply
)
).json)
} else {
val messageArray = MessageHelper.decodeCQCode(message.asString)
quicklyReply(
record,
messageArray,
msgHash,
atSender,
autoReply
)
}
} else if (message is JsonArray) {
quicklyReply(
record,
msgList.jsonArray,
msgHash,
atSender,
autoReply
)
} else {
val messageArray = MessageHelper.decodeCQCode(message.asString)
quicklyReply(
record,
messageArray,
message,
msgHash,
atSender,
autoReply
)
}
} else if (message is JsonArray) {
quicklyReply(
record,
message,
msgHash,
atSender,
autoReply
)
}
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("delete") && data["delete"].asBoolean) {
MsgSvc.recallMsg(msgHash)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("kick") && data["kick"].asBoolean) {
GroupSvc.kickMember(record.peerUin, false, record.senderUin)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("ban") && data["ban"].asBoolean) {
val banTime = data["ban_duration"].asIntOrNull ?: (30 * 60)
if (banTime <= 0) return
GroupSvc.banMember(record.peerUin, record.senderUin, banTime)
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("delete") && data["delete"].asBoolean) {
MsgSvc.recallMsg(msgHash)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("kick") && data["kick"].asBoolean) {
GroupSvc.kickMember(record.peerUin, false, record.senderUin)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("ban") && data["ban"].asBoolean) {
val banTime = data["ban_duration"].asIntOrNull ?: (30 * 60)
if (banTime <= 0) return
GroupSvc.banMember(record.peerUin, record.senderUin, banTime)
}
} else if (data is JsonArray) {
data.forEach {
handleQuicklyActions(it.asJsonObject)
}
}
} catch (e: Throwable) {
LogCenter.log("处理快速操作错误: $e", Level.WARN)
}
}
private suspend fun quicklyReply(
record: MsgRecord,
message: JsonArray,
msgHash: Int,
atSender: Boolean,
autoReply: Boolean
) {
val messageList = mutableListOf<JsonElement>()
message.filter {
it.asJsonObject["type"]?.asString in actionMsgTypes
}.let {
if (it.isNotEmpty()) {
it.map { listOf(it) }.forEach {
MsgSvc.sendToAio(record.chatType, record.peerUin.toString(), it.jsonArray)
}
return
}
}
private suspend fun handleQuicklyActions(data: JsonObject) {
val action = data["action"].asString
val echo = data["echo"] ?: EmptyJsonString
if (autoReply) messageList.add(mapOf(
"type" to "reply",
"data" to mapOf(
"id" to msgHash
)
).json) // 添加回复
if (MsgConstant.KCHATTYPEGROUP == record.chatType && atSender) {
messageList.add(mapOf(
"type" to "at",
"data" to mapOf(
"qq" to record.senderUin
)
).json) // 添加@发送者
val params = data["params"].asJsonObjectOrNull ?: EmptyJsonObject
val handler = ActionManager[action]
if (handler == null) {
LogCenter.log("HTTP快速操作不支持的Action: $action", Level.WARN)
} else {
handler.handle(ActionSession(params, echo))
}
messageList.addAll(message)
MsgSvc.sendToAio(record.chatType, record.peerUin.toString(), JsonArray(messageList))
}
}

View File

@ -18,6 +18,11 @@ internal class WebSocketClientService(
) : WebSocketClientServlet(address, heartbeatInterval, wsHeaders) {
private val eventJobList = mutableSetOf<Job>()
init {
startHeartbeatTimer()
initTransmitter()
}
override fun submitFlowJob(job: Job) {
eventJobList.add(job)
}

View File

@ -66,7 +66,7 @@ internal class WebSocketService(
.ifNullOrEmpty(handshake.getFieldValue("ticket"))
.ifNullOrEmpty(handshake.getFieldValue("Authorization"))
?: throw ErrorTokenException
if (accessToken.startsWith("Bearer ")) {
if (accessToken.startsWith("Bearer ", ignoreCase = true)) {
accessToken = accessToken.substring(7)
}
val tokenList = token.split(",", "|", "")

View File

@ -25,6 +25,7 @@ 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.remote.service.data.push.SignDetail
import moe.fuqiuluo.shamrock.tools.ShamrockDsl
import moe.fuqiuluo.shamrock.tools.json
import java.util.ArrayList
@ -58,7 +59,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
elements: ArrayList<MsgElement>,
rawMsg: String,
msgHash: Int,
postType: PostType = PostType.Msg
postType: PostType
): Boolean {
val uin = app.longAccountUin
transMessageEvent(record,
@ -81,10 +82,10 @@ internal object GlobalEventTransmitter: BaseSvc() {
sender = Sender(
userId = record.senderUin,
nickname = record.sendNickName
.ifBlank { record.sendRemarkName }
.ifBlank { record.sendMemberName }
.ifBlank { record.peerName },
card = record.sendMemberName.ifBlank { record.sendNickName },
.ifEmpty { record.sendRemarkName }
.ifEmpty { record.sendMemberName }
.ifEmpty { record.peerName },
card = record.sendMemberName,
role = when (record.senderUin) {
GroupSvc.getOwner(record.peerUin.toString()) -> MemberRole.Owner
in GroupSvc.getAdminList(record.peerUin.toString()) -> MemberRole.Admin
@ -106,12 +107,12 @@ internal object GlobalEventTransmitter: BaseSvc() {
elements: ArrayList<MsgElement>,
rawMsg: String,
msgHash: Int,
postType: PostType = PostType.Msg,
postType: PostType,
tempSource: MessageTempSource = MessageTempSource.Unknown
): Boolean {
val botUin = app.longAccountUin
var nickName = record.sendNickName
if (nickName.isNullOrBlank()) {
if (nickName.isNullOrEmpty()) {
CardSvc.getProfileCard(record.senderUin.toString()).onSuccess {
nickName = it.strNick ?: record.peerName
}
@ -222,6 +223,24 @@ internal object GlobalEventTransmitter: BaseSvc() {
* 群聊通知 通知器
*/
object GroupNoticeTransmitter {
suspend fun transGroupSign(time: Long, target: Long, action: String?, rankImg: String?, groupCode: Long): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.Notify,
subType = NoticeSubType.Sign,
userId = target,
groupId = groupCode,
target = target,
signDetail = SignDetail(
rankImg = rankImg,
action = action
)
))
return true
}
suspend fun transGroupPoke(time: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?, groupCode: Long): Boolean {
pushNotice(NoticeEvent(
time = time,
@ -245,8 +264,10 @@ internal object GlobalEventTransmitter: BaseSvc() {
suspend fun transGroupMemberNumChanged(
time: Long,
target: Long,
targetUid: String,
groupCode: Long,
operation: Long,
operator: Long,
operatorUid: String,
noticeType: NoticeType,
noticeSubType: NoticeSubType
): Boolean {
@ -256,11 +277,14 @@ internal object GlobalEventTransmitter: BaseSvc() {
postType = PostType.Notice,
type = noticeType,
subType = noticeSubType,
operatorId = operation,
operatorId = operator,
userId = target,
senderId = operation,
senderId = operator,
target = target,
groupId = groupCode
groupId = groupCode,
targetUid = targetUid,
operatorUid = operatorUid,
userUid = targetUid
))
return true
}
@ -268,6 +292,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
suspend fun transGroupAdminChanged(
msgTime: Long,
target: Long,
targetUid: String,
groupCode: Long,
setAdmin: Boolean
): Boolean {
@ -279,6 +304,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
subType = if (setAdmin) NoticeSubType.Set else NoticeSubType.UnSet,
operatorId = 0,
target = target,
targetUid = targetUid,
groupId = groupCode
))
return true
@ -286,8 +312,11 @@ internal object GlobalEventTransmitter: BaseSvc() {
suspend fun transGroupBan(
msgTime: Long,
operation: Long,
subType: NoticeSubType,
operator: Long,
operatorUid: String,
target: Long,
targetUid: String,
groupCode: Long,
duration: Int
): Boolean {
@ -296,13 +325,15 @@ internal object GlobalEventTransmitter: BaseSvc() {
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.GroupBan,
subType = if (duration == 0) NoticeSubType.LiftBan else NoticeSubType.Ban,
operatorId = operation,
subType = subType,
operatorId = operator,
userId = target,
senderId = operation,
senderId = operator,
target = target,
groupId = groupCode,
duration = duration
duration = duration,
operatorUid = operatorUid,
targetUid = targetUid
))
return true
}

View File

@ -30,7 +30,7 @@ internal abstract class HttpTransmitServlet : BaseTransmitServlet {
if (!allowTransmit()) return null
try {
if (address.startsWith("http://") || address.startsWith("https://")) {
return GlobalClient.post(address) {
val response = GlobalClient.post(address) {
contentType(ContentType.Application.Json)
setBody(body)
@ -44,6 +44,11 @@ internal abstract class HttpTransmitServlet : BaseTransmitServlet {
header("X-Client-Role", "Universal")
header("Sec-WebSocket-Protocol", "11.Shamrock")
}
return if (response.status.value == 204) {
null
} else {
response
}
} else {
LogCenter.log("HTTP推送地址错误: ${address}", Level.ERROR)
}

View File

@ -35,24 +35,22 @@ import java.net.URI
import kotlin.concurrent.timer
internal abstract class WebSocketClientServlet(
url: String,
private val url: String,
private val heartbeatInterval: Long,
wsHeaders: Map<String, String>
private val wsHeaders: Map<String, String>
) : BaseTransmitServlet, WebSocketClient(URI(url), wsHeaders) {
init {
if (connectedClients.containsKey(url)) {
throw RuntimeException("WebSocketClient已存在: $url")
}
}
private val sendLock = Mutex()
override fun allowTransmit(): Boolean {
return ShamrockConfig.openWebSocketClient()
}
override fun onOpen(handshakedata: ServerHandshake?) {
LogCenter.log("WebSocketClient onOpen: ${handshakedata?.httpStatus}, ${handshakedata?.httpStatusMessage}")
startHeartbeatTimer()
pushMetaLifecycle()
initTransmitter()
}
override fun onMessage(message: String) {
GlobalScope.launch {
handleMessage(message)
@ -84,14 +82,34 @@ internal abstract class WebSocketClientServlet(
respond?.let { send(it) }
}
override fun onOpen(handshakedata: ServerHandshake?) {
LogCenter.log("WebSocketClient onOpen: ${handshakedata?.httpStatus}, ${handshakedata?.httpStatusMessage}")
connectedClients[url] = this
//startHeartbeatTimer()
pushMetaLifecycle()
//initTransmitter()
}
override fun onClose(code: Int, reason: String?, remote: Boolean) {
if (code == 403) {
if (wsHeaders.containsKey("authorization")) {
val token = wsHeaders["authorization"]!!.substring(7)
LogCenter.log("WebSocketClient连接被拒绝, token: $token 失效", Level.WARN)
} else {
LogCenter.log("WebSocketClient连接被拒绝, 未设置token", Level.WARN)
}
}
LogCenter.log("WebSocketClient onClose: $code, $reason, $remote")
cancelFlowJobs()
connectedClients.remove(url)
}
override fun onError(ex: Exception?) {
LogCenter.log("WebSocketClient onError: ${ex?.message}")
cancelFlowJobs()
connectedClients.remove(url)
}
protected suspend inline fun <reified T> pushTo(body: T) {
@ -105,11 +123,14 @@ internal abstract class WebSocketClientServlet(
}
}
private fun startHeartbeatTimer() {
if (heartbeatInterval <= 0) return
fun startHeartbeatTimer() {
if (heartbeatInterval <= 0) {
LogCenter.log("被动WebSocket心跳间隔为0不启动心跳", Level.WARN)
return
}
timer(
name = "heartbeat",
initialDelay = 0,
initialDelay = heartbeatInterval,
period = heartbeatInterval,
) {
if (isClosed || isClosing || !isOpen) {
@ -117,6 +138,7 @@ internal abstract class WebSocketClientServlet(
return@timer
}
val runtime = AppRuntimeFetcher.appRuntime
LogCenter.log("WebSocketClient心跳: ${app.longAccountUin}", Level.DEBUG)
send(
GlobalJson.encodeToString(
PushMetaEvent(
@ -131,7 +153,7 @@ internal abstract class WebSocketClientServlet(
status = "正常",
good = true
),
interval = 1000L * 15
interval = heartbeatInterval
)
)
)
@ -157,4 +179,8 @@ internal abstract class WebSocketClientServlet(
)
}
}
companion object {
private val connectedClients = mutableMapOf<String, WebSocketClientServlet>()
}
}

View File

@ -61,6 +61,7 @@ internal abstract class WebSocketTransmitServlet(
timer("heartbeat", true, 0, heartbeatInterval) {
val runtime = AppRuntimeFetcher.appRuntime
val curUin = runtime.currentAccountUin
LogCenter.log("WebSocket心跳: $curUin", Level.DEBUG)
broadcastAnyEvent(
PushMetaEvent(
time = System.currentTimeMillis() / 1000,
@ -78,6 +79,8 @@ internal abstract class WebSocketTransmitServlet(
)
)
}
} else {
LogCenter.log("主动WebSocket心跳间隔为0不启动心跳", Level.WARN)
}
}
@ -86,7 +89,13 @@ internal abstract class WebSocketTransmitServlet(
if (path != "/api") {
eventReceivers.remove(conn)
}
LogCenter.log({ "WSServer断开(${conn.remoteSocketAddress.address.hostAddress}:${conn.remoteSocketAddress.port}$path): $code,$reason,$remote" }, Level.WARN)
runCatching {
conn.remoteSocketAddress.address.hostAddress to conn.remoteSocketAddress.port
}.onSuccess {
LogCenter.log({ "WSServer断开(${it.first}:${it.second}$path): $code,$reason,$remote" }, Level.WARN)
}.onFailure {
LogCenter.log({ "WSServer断开($path): $code,$reason,$remote" }, Level.WARN)
}
}
override fun onMessage(conn: WebSocket, message: String) {

View File

@ -76,6 +76,7 @@ internal object ShamrockConfig {
putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息
putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息
putBoolean("isInit", true)
}
@ -101,6 +102,10 @@ internal object ShamrockConfig {
return Config.rules?.privateRule
}
fun enableSyncMsgAsSentMsg(): Boolean {
return mmkv.getBoolean("enable_sync_msg_as_sent_msg", false)
}
fun enableSelfMsg(): Boolean {
return mmkv.getBoolean("enable_self_msg", false)
}

View File

@ -16,3 +16,22 @@ internal data class FriendEntry(
@SerialName("term_type") val termType: Int,
)
@Serializable
internal data class FriendRequest(
@SerialName("request_id") val seq: Long = 0,
@SerialName("requester_uin") val userId: Long = 0,
@SerialName("requester_nick") val name: String?,
val source: String?,
@SerialName("sub_id") val subId: Int?,
@SerialName("sub_src_id") val subSrcId: Int?,
@SerialName("message") val msg: String?,
@SerialName("source_group_name") val sourceGroupName: String?,
@SerialName("source_group_id") val sourceGroupCode: Long?,
val flag: String,
val sex: String?,
val age: Int?,
@SerialName("msg_detail") val msgDetail: String?,
val status: String?,
)

View File

@ -26,6 +26,7 @@ internal data class SimpleTroopMemberInfo(
@SerialName("group_id") val groupId: Long,
@SerialName("user_name") val name: String,
@SerialName("sex") val sex: String,
@SerialName("age") val age: Int,
@SerialName("title") val title: String,
@SerialName("title_expire_time") val titleExpireTime: Int,
@SerialName("nickname") val nick: String,
@ -42,6 +43,7 @@ internal data class SimpleTroopMemberInfo(
@SerialName("role") val role: MemberRole,
@SerialName("unfriendly") val unfriendly: Boolean,
@SerialName("card_changeable") val cardChangeable: Boolean,
@SerialName("shut_up_timestamp") val shutUpTimestamp: Long?,
)
@Serializable

View File

@ -20,7 +20,7 @@ internal enum class NoticeType {
@Serializable
internal enum class RequestType {
@SerialName("friend ") Friend,
@SerialName("friend") Friend,
@SerialName("group") Group,
}
@ -42,6 +42,8 @@ internal enum class NoticeSubType {
@SerialName("kick_me") KickMe,
@SerialName("poke") Poke,
@SerialName("sign") Sign,
@SerialName("title") Title,
@SerialName("delete") Delete,
@ -65,14 +67,20 @@ internal data class NoticeEvent(
@SerialName("post_type") val postType: PostType,
@SerialName("notice_type") val type: NoticeType,
@SerialName("sub_type") val subType: NoticeSubType = NoticeSubType.None,
@SerialName("group_id") val groupId: Long = 0,
@SerialName("operator_id") val operatorId: Long = 0,
@SerialName("user_id") val userId: Long = 0,
@SerialName("sender_id") val senderId: Long = 0,
@SerialName("duration") val duration: Int = 0,
@SerialName("message_id") val msgId: Int = 0,
@SerialName("group_id") val groupId: Long = Long.MIN_VALUE,
@SerialName("operator_id") val operatorId: Long = Long.MIN_VALUE,
@SerialName("operator_uid") val operatorUid: String = "",
@SerialName("user_id") val userId: Long = Long.MIN_VALUE,
@SerialName("user_uid") val userUid: String = "",
@SerialName("sender_id") val senderId: Long = Long.MIN_VALUE,
@SerialName("duration") val duration: Int = Int.MIN_VALUE,
@SerialName("message_id") val msgId: Int = Int.MIN_VALUE,
@SerialName("tip_text") val tip: String = "",
@SerialName("target_id") val target: Long = 0,
@SerialName("target_id") val target: Long = Long.MIN_VALUE,
@SerialName("target_uid") val targetUid: String = "",
@SerialName("file") val file: GroupFileMsg? = null,
@SerialName("private_file") val privateFile: PrivateFileMsg? = null,
@SerialName("flag") val flag: String? = null,
@ -87,7 +95,10 @@ internal data class NoticeEvent(
// 戳一戳
@SerialName("poke_detail") val pokeDetail: PokeDetail? = null,
)
// 群打卡
@SerialName("sign_detail") val signDetail: SignDetail? = null,
)
/**
* 不要使用继承的方式实现通用字段,那样会很难维护!
@ -99,8 +110,8 @@ internal data class RequestEvent(
@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("group_id") val groupId: Long = -1,
@SerialName("user_id") val userId: Long = -1,
@SerialName("comment") val comment: String = "",
@SerialName("flag") val flag: String? = null,
)
@ -131,4 +142,11 @@ internal data class PokeDetail (
val suffix: String? = "",
@SerialName("action_img_url")
val actionImg: String? = "https://tianquan.gtimg.cn/nudgeaction/item/0/expression.jpg",
)
)
@Serializable
internal data class SignDetail (
val action: String? = "今日第1个打卡",
@SerialName("rank_img")
val rankImg: String? = "",
)

View File

@ -1,13 +1,12 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.shamrock.remote.service.listener
import android.os.Build
import moe.fuqiuluo.shamrock.helper.MessageHelper
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
@ -19,12 +18,11 @@ 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
import java.util.ArrayList
import java.util.Collections
import kotlin.collections.HashMap
internal object AioListener: IKernelMsgListener {
internal object AioListener : IKernelMsgListener {
// 通过MSG SEQ临时监听器
internal val messageLessListenerMap = Collections.synchronizedMap(HashMap<Long, MsgRecord.() -> Unit>())
@ -43,13 +41,12 @@ internal object AioListener: IKernelMsgListener {
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return // TODO: 频道消息暂不处理
messageLessListenerMap.firstNotNullOfOrNull {
if(it.key == record.msgSeq) it else null
if (it.key == record.msgSeq) it else null
}?.let {
it.value(record)
messageLessListenerMap.remove(it.key)
}
if (record.msgSeq < 0) return
if (record.msgSeq < 0) return
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
@ -67,9 +64,14 @@ internal object AioListener: IKernelMsgListener {
if (rawMsg.isEmpty()) return
if (ShamrockConfig.aliveReply() && rawMsg == "ping") {
MessageHelper.sendMessageWithoutMsgId(record.chatType, record.peerUin.toString(), "pong", { _, _ -> })
MessageHelper.sendMessageWithoutMsgId(record.chatType, record.peerUin.toString(), "pong", { _, _ -> })
}
val postType = if (record.senderUin == TicketSvc.getLongUin() && ShamrockConfig.enableSyncMsgAsSentMsg()) {
PostType.MsgSent
} else PostType.Msg
//if (rawMsg.contains("forward")) {
// LogCenter.log(record.extInfoForUI.decodeToString(), Level.WARN)
//}
@ -83,7 +85,7 @@ internal object AioListener: IKernelMsgListener {
}
if(!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(
record, record.elements, rawMsg, msgHash
record, record.elements, rawMsg, msgHash, postType
)) {
LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN)
}
@ -96,7 +98,7 @@ internal object AioListener: IKernelMsgListener {
}
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
record, record.elements, rawMsg, msgHash
record, record.elements, rawMsg, msgHash, postType
)) {
LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN)
}
@ -112,8 +114,8 @@ internal object AioListener: IKernelMsgListener {
}
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
record, record.elements, rawMsg, msgHash, tempSource = MessageTempSource.Group
)) {
record, record.elements, rawMsg, msgHash, tempSource = MessageTempSource.Group, postType = postType
)) {
LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
}
}
@ -158,7 +160,8 @@ internal object AioListener: IKernelMsgListener {
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return@forEach// TODO: 频道消息暂不处理
if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED
|| record.sendStatus == MsgConstant.KSENDSTATUSSENDING) {
|| record.sendStatus == MsgConstant.KSENDSTATUSSENDING
) {
return@forEach
}
@ -182,8 +185,10 @@ internal object AioListener: IKernelMsgListener {
.updateMsgSeqByMsgHash(msgHash, record.msgSeq.toInt())
}
if (!ShamrockConfig.enableSelfMsg() || record.senderUin != TicketSvc.getLongUin())
return@launch
if (!ShamrockConfig.enableSelfMsg()
|| record.senderUin != TicketSvc.getLongUin()
|| record.peerUin == TicketSvc.getLongUin()
) return@launch
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString())
if (rawMsg.isEmpty()) return@launch
@ -191,24 +196,37 @@ internal object AioListener: IKernelMsgListener {
when (record.chatType) {
MsgConstant.KCHATTYPEGROUP -> {
if(!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 -> {
if(!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
if(!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}")
}
}
@ -305,7 +323,7 @@ internal object AioListener: IKernelMsgListener {
override fun onFileMsgCome(arrayList: ArrayList<MsgRecord>?) {
arrayList?.forEach { record ->
GlobalScope.launch {
when(record.chatType) {
when (record.chatType) {
MsgConstant.KCHATTYPEGROUP -> onGroupFileMsg(record)
MsgConstant.KCHATTYPEC2C -> onC2CFileMsg(record)
else -> LogCenter.log("不支持该来源的文件上传事件:${record}", Level.WARN)
@ -330,8 +348,9 @@ internal object AioListener: IKernelMsgListener {
val fileSubId = fileMsg.fileSubId ?: ""
val url = RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
if(!GlobalEventTransmitter.FileNoticeTransmitter
.transPrivateFileEvent(record.msgTime, userId, fileId, fileSubId, fileName, fileSize, expireTime, url)) {
if (!GlobalEventTransmitter.FileNoticeTransmitter
.transPrivateFileEvent(record.msgTime, userId, fileId, fileSubId, fileName, fileSize, expireTime, url)
) {
LogCenter.log("私聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
}
}
@ -353,8 +372,9 @@ internal object AioListener: IKernelMsgListener {
val url = RichProtoSvc.getGroupFileDownUrl(record.peerUin, uuid, bizId)
if(!GlobalEventTransmitter.FileNoticeTransmitter
.transGroupFileEvent(record.msgTime, userId, groupId, uuid, fileName, fileSize, bizId, url)) {
if (!GlobalEventTransmitter.FileNoticeTransmitter
.transGroupFileEvent(record.msgTime, userId, groupId, uuid, fileName, fileSize, bizId, url)
) {
LogCenter.log("群聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
}
}

View File

@ -12,17 +12,11 @@ import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
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
import moe.fuqiuluo.proto.asLong
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asByteArray
import moe.fuqiuluo.proto.asList
import moe.fuqiuluo.proto.asULong
import moe.fuqiuluo.proto.*
import moe.fuqiuluo.qqinterface.servlet.FriendSvc.requestFriendSystemMsgNew
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew
import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeSubType
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeType
@ -54,7 +48,6 @@ internal object PrimitiveListener {
if (
!pb.has(1, 3)
|| !pb.has(1, 2)
// || !pb.has(1, 2, 2)
|| !pb.has(1, 2, 6)
) return
val msgType = pb[1, 2, 1].asInt
@ -63,28 +56,32 @@ internal object PrimitiveListener {
subType = pb[1, 2, 2].asInt
}
val msgTime = pb[1, 2, 6].asLong
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) {
35 -> onFriendApply(msgTime, pb)
39 -> onCardChange(msgTime, pb)
// invite
68 -> onGroupApply(msgTime, pb)
138 -> onC2CRecall(msgTime, pb)
290 -> onC2cPoke(msgTime, pb)
}
try {
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) {
35 -> onFriendApply(msgTime, pb)
39 -> onCardChange(msgTime, pb)
// invite
68 -> onGroupApply(msgTime, pb)
138 -> onC2CRecall(msgTime, pb)
290 -> onC2cPoke(msgTime, pb)
}
732 -> when (subType) {
12 -> onGroupBan(msgTime, pb)
16 -> onGroupTitleChange(msgTime, pb)
17 -> onGroupRecall(msgTime, pb)
20 -> onGroupPoke(msgTime, pb)
21 -> onEssenceMessage(msgTime, pb)
732 -> when (subType) {
12 -> onGroupBan(msgTime, pb)
16 -> onGroupTitleChange(msgTime, pb)
17 -> onGroupRecall(msgTime, pb)
20 -> onGroupPokeAndGroupSign(msgTime, pb)
21 -> onEssenceMessage(msgTime, pb)
}
}
} catch (e: Exception) {
LogCenter.log("onMsgPush(msgType: $msgType, subType: $subType): "+e.stackTraceToString(), Level.WARN)
}
}
@ -153,8 +150,21 @@ internal object PrimitiveListener {
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 detail = pb[1, 3, 2]
if (detail !is ProtoMap) {
try {
val readPacket = ByteReadPacket(detail.asByteArray)
readPacket.readBuf32Long()
readPacket.discardExact(1)
detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
readPacket.release()
} catch (e: Exception) {
LogCenter.log("onCardChange error: ${e.stackTraceToString()}", Level.WARN)
}
}
val targetId = detail[1, 13, 2].asUtf8String
val newCardList = detail[1, 13, 3].asList
var newCard = ""
newCardList
.value
@ -163,7 +173,7 @@ internal object PrimitiveListener {
newCard = it[2].asUtf8String
}
}
val groupId = pb[1, 3, 2, 1, 13, 4].asLong
val groupId = detail[1, 13, 4].asLong
var oldCard = ""
val targetQQ = ContactHelper.getUinByUidAsync(targetId).toLong()
LogCenter.log("群组[$groupId]成员$targetQQ 群名片变动 -> $newCard")
@ -181,20 +191,35 @@ internal object PrimitiveListener {
}
private suspend fun onGroupTitleChange(msgTime: Long, pb: ProtoMap) {
val groupCode = pb[1, 1, 1].asULong
var detail = pb[1, 3, 2]
if (detail !is ProtoMap) {
try {
val readPacket = ByteReadPacket(detail.asByteArray)
readPacket.readBuf32Long()
readPacket.discardExact(1)
detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
readPacket.release()
} catch (e: Exception) {
LogCenter.log("onGroupTitleChange error: ${e.stackTraceToString()}", Level.WARN)
}
}
var groupId:Long
try {
groupId = detail[4].asULong
}catch (e: ClassCastException){
groupId = detail[4].asList.value[0].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]
detail = if (detail[5] is ProtoList) {
(detail[5] as ProtoList).value[0]
} else {
detail[5]
}
val targetUin = detail[5, 5].asLong
val groupId = detail[4].asLong
val targetUin = detail[5].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
val titleChangeInfo = detail[2].asUtf8String
if (titleChangeInfo.indexOf("群主授予") == -1) {
return
}
@ -212,15 +237,25 @@ internal object PrimitiveListener {
}
private suspend fun onEssenceMessage(msgTime: Long, pb: ProtoMap) {
val groupCode = pb[1, 1, 1].asULong
var detail = pb[1, 3, 2]
if (detail !is ProtoMap) {
try {
val readPacket = ByteReadPacket(detail.asByteArray)
readPacket.readBuf32Long()
readPacket.discardExact(1)
detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
readPacket.release()
} catch (e: Exception) {
LogCenter.log("onEssenceMessage error: ${e.stackTraceToString()}", Level.WARN)
}
}
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
var groupId:Long
try {
groupId = detail[4].asULong
}catch (e: ClassCastException){
groupId = detail[4].asList.value[0].asULong
}
val mesSeq = detail[37].asInt
val senderUin = detail[33, 5].asLong
val operatorUin = detail[33, 6].asLong
@ -254,32 +289,38 @@ internal object PrimitiveListener {
}
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 groupCode2 = readPacket.readBuf32Long()
var detail = if (groupCode2 == groupCode1) {
groupCode = groupCode2
readPacket.discardExact(1)
ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
} else pb[1, 3, 2]
private suspend fun onGroupPokeAndGroupSign(time: Long, pb: ProtoMap) {
var detail = pb[1, 3, 2]
if (detail !is ProtoMap) {
groupCode = groupCode2
readPacket.discardExact(1)
detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
try {
val readPacket = ByteReadPacket(detail.asByteArray)
readPacket.discardExact(4)
readPacket.discardExact(1)
detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
readPacket.release()
} catch (e: Exception) {
LogCenter.log("onGroupPokeAndGroupSign error: ${e.stackTraceToString()}", Level.WARN)
}
}
val groupId = try {
detail[4].asULong
}catch (e: ClassCastException){
detail[4].asList.value[0].asULong
}
detail = if (detail[26] is ProtoList) {
(detail[26] as ProtoList).value[0]
} else {
detail[26]
}
readPacket.release()
lateinit var target: String
lateinit var operation: String
var action: String? = null
var suffix: String? = null
var actionImg: String? = null
detail[26][7]
var rankImg: String? = null
detail[7]
.asList
.value
.forEach {
@ -287,18 +328,42 @@ internal object PrimitiveListener {
when (it[1].asUtf8String) {
"uin_str1" -> operation = value
"uin_str2" -> target = value
// "nick_str1" -> operation_nick = value
// "nick_str2" -> operation_nick = value
"action_str" -> action = value
"alt_str1" -> action = value
"suffix_str" -> suffix = value
"action_img_url" -> actionImg = value
"mqq_uin" -> target = value
// "mqq_nick" -> operation_nick = value
"user_sign" -> action = value
"rank_img" -> rankImg = value
// "sign_word" -> 我也要打卡
}
}
when (detail[2].asInt) {
1061 -> {
LogCenter.log("群戳一戳($groupId): $operation $action $target $suffix")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupId)
) {
LogCenter.log("群戳一戳推送失败!", Level.WARN)
}
}
LogCenter.log("群戳一戳($groupCode): $operation $action $target $suffix")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupCode)
) {
LogCenter.log("群戳一戳推送失败!", Level.WARN)
1068 -> {
LogCenter.log("群打卡($groupId): $action $target")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupSign(time, target.toLong(), action, rankImg, groupId)
) {
LogCenter.log("群打卡推送失败!", Level.WARN)
}
}
else -> {
LogCenter.log("onGroupPokeAndGroupSign unknown type ${detail[2].asInt}", Level.WARN)
}
}
}
@ -326,13 +391,21 @@ internal object PrimitiveListener {
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()
GroupSvc.getGroupMemberList(groupCode.toString(), true).onFailure {
LogCenter.log("新成员加入刷新群成员列表失败: $groupCode", Level.WARN)
}.onSuccess {
LogCenter.log("新成员加入刷新群成员列表成功,群成员数量: ${it.size}", Level.INFO)
}
val operatorUid = pb[1, 3, 2, 5].asUtf8String
val operator = ContactHelper.getUinByUidAsync(operatorUid).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) {
time, target, targetUid, groupCode, operator, operatorUid, NoticeType.GroupMemIncrease, when (type) {
130 -> NoticeSubType.Approve
131 -> NoticeSubType.Invite
else -> NoticeSubType.Approve
@ -347,8 +420,19 @@ internal object PrimitiveListener {
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 operatorUid = try {
pb[1, 3, 2, 5, 1, 1].asUtf8String
} catch (e: Throwable) {
pb[1, 3, 2, 5].asUtf8String
}
GroupSvc.getGroupMemberList(groupCode.toString(), true).onFailure {
LogCenter.log("新成员加入刷新群成员列表失败: $groupCode", Level.WARN)
}.onSuccess {
LogCenter.log("新成员加入刷新群成员列表成功,群成员数量: ${it.size}", Level.INFO)
}
val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong()
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
val subtype = when (type) {
130 -> NoticeSubType.Leave
@ -362,7 +446,7 @@ internal object PrimitiveListener {
LogCenter.log("群成员减少($groupCode): $target, type = $subtype ($type)")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMemberNumChanged(time, target, groupCode, operation, NoticeType.GroupMemDecrease, subtype)
.transGroupMemberNumChanged(time, target, targetUid, groupCode, operator, operatorUid, NoticeType.GroupMemDecrease, subtype)
) {
LogCenter.log("群成员减少推送失败!", Level.WARN)
}
@ -383,40 +467,55 @@ internal object PrimitiveListener {
LogCenter.log("群管理员变动($groupCode): $target, isSetAdmin = $isSetAdmin")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupAdminChanged(msgTime, target, groupCode, isSetAdmin)
.transGroupAdminChanged(msgTime, target, targetUid, groupCode, isSetAdmin)
) {
LogCenter.log("群管理员变动推送失败!", Level.WARN)
}
}
private suspend fun onGroupBan(msgTime: Long, pb: ProtoMap) {
val groupCode = pb[1, 1, 1].asULong
val groupCode = pb[1, 3, 2, 1].asULong
val operatorUid = pb[1, 3, 2, 4].asUtf8String
val targetUid = pb[1, 3, 2, 5, 3, 1].asUtf8String
val duration = pb[1, 3, 2, 5, 3, 2].asInt
val operation = ContactHelper.getUinByUidAsync(operatorUid).toLong()
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群禁言($groupCode): $operation -> $target, 时长 = ${duration}s")
val wholeBan = !pb.has(1, 3, 2, 5, 3, 1)
val targetUid = if (wholeBan) "" else pb[1, 3, 2, 5, 3, 1].asUtf8String
val rawDuration = pb[1, 3, 2, 5, 3, 2].asInt
val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong()
val duration = if (wholeBan) -1 else rawDuration
val target = if (wholeBan) 0 else ContactHelper.getUinByUidAsync(targetUid).toLong()
val subType = if (rawDuration == 0) NoticeSubType.LiftBan else NoticeSubType.Ban
if (wholeBan) {
LogCenter.log("群全员禁言($groupCode): $operator -> ${if (subType == NoticeSubType.Ban) "开启" else "关闭"}")
} else {
LogCenter.log("群禁言($groupCode): $operator -> $target, 时长 = ${duration}s")
}
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupBan(msgTime, operation, target, groupCode, duration)
.transGroupBan(msgTime, subType, operator, operatorUid, target, targetUid, 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)
try {
/**
* 真是不理解这个傻呗设计有些群是正常的Protobuf有些群要去掉7字节
*/
val detail = if (readPacket.readBuf32Long() == groupCode) {
readPacket.discardExact(1)
ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
} else pb[1, 3, 2]
var detail = pb[1, 3, 2]
if (detail !is ProtoMap) {
try {
val readPacket = ByteReadPacket(detail.asByteArray)
readPacket.discardExact(4)
readPacket.discardExact(1)
detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
readPacket.release()
} catch (e: Exception) {
LogCenter.log("onGroupRecall error: ${e.stackTraceToString()}", Level.WARN)
}
}
var groupCode:Long
try {
groupCode = detail[4].asULong
}catch (e: ClassCastException){
groupCode = detail[4].asList.value[0].asULong
}
val operatorUid = detail[11, 1].asUtf8String
val targetUid = detail[11, 3, 6].asUtf8String
val msgSeq = detail[11, 3, 1].asLong
@ -436,9 +535,6 @@ internal object PrimitiveListener {
) {
LogCenter.log("群消息撤回推送失败!", Level.WARN)
}
} finally {
readPacket.release()
}
}
private suspend fun onGroupApply(time: Long, pb: ProtoMap) {
@ -448,16 +544,18 @@ internal object PrimitiveListener {
val applierUid = pb[1, 3, 2, 3].asUtf8String
val reason = pb[1, 3, 2, 5].asUtf8String
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == getLongUin()) {
return
}
LogCenter.log("入群申请($groupCode) $applier: \"$reason\"")
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(10, 2)
val riskReqs = requestGroupSystemMsgNew(5, 2)
reqs = reqs + riskReqs
val req = reqs.first {
val req = reqs.firstOrNull() {
it.msg_time.get() == time
}
val seq = req.msg_seq?.get()
val seq = req?.msg_seq?.get() ?: time
"$seq;$groupCode;$applier"
} catch (err: Throwable) {
"$time;$groupCode;$applier"
@ -473,6 +571,9 @@ internal object PrimitiveListener {
val groupCode = pb[1, 3, 2, 2, 3].asULong
val applierUid = pb[1, 3, 2, 2, 5].asUtf8String
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == getLongUin()) {
return
}
if (pb[1, 3, 2, 2, 1].asInt < 3) {
// todo
return
@ -480,17 +581,17 @@ internal object PrimitiveListener {
LogCenter.log("邀请入群申请($groupCode): $applier")
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(10, 2)
val riskReqs = requestGroupSystemMsgNew(5, 2)
reqs = reqs + riskReqs
val req = reqs.first {
val req = reqs.firstOrNull() {
it.msg_time.get() == time
}
val seq = req.msg_seq?.get()
val seq = req?.msg_seq?.get() ?: time
"$seq;$groupCode;$applier"
} catch (err: Throwable) {
"$time;$groupCode;$applierUid"
"$time;$groupCode;$applier"
}
if (GlobalEventTransmitter.RequestTransmitter
if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, "", groupCode, flag, RequestSubType.Add)
) {
LogCenter.log("邀请入群申请推送失败!", Level.WARN)
@ -509,19 +610,19 @@ internal object PrimitiveListener {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(10, 2)
reqs = reqs + riskReqs
val req = reqs.first {
val req = reqs.firstOrNull() {
it.msg_time.get() == time
}
val seq = req.msg_seq?.get()
val seq = req?.msg_seq?.get() ?: time
"$seq;$groupCode;$uin"
} catch (err: Throwable) {
"$time;$groupCode;$uin"
}
if (GlobalEventTransmitter.RequestTransmitter
if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, invitor, "", groupCode, flag, RequestSubType.Invite)
) {
LogCenter.log("邀请入群推送失败!", Level.WARN)
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More