From a06708bf95962b4298650a760a3a034c380b424e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Fri, 8 Mar 2024 01:11:24 +0800 Subject: [PATCH 01/20] `Shamrock`: build for kritor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- app/build.gradle.kts | 7 +- app/src/main/cpp/shamrock.cpp | 2 +- .../moe/fuqiuluo/shamrock/MainActivity.kt | 73 +- .../fuqiuluo/shamrock/app/config/ConfigKey.kt | 2 + .../shamrock/app/config/ShamrockConfig.kt | 33 + .../shamrock/ui/app/ShamrockConfig.kt | 395 ------ .../shamrock/ui/fragment/DashboardFragment.kt | 248 +--- .../shamrock/ui/fragment/LabFragment.kt | 104 +- .../ui/service/DashboardInitializer.kt | 108 -- .../ui/service/handlers/FetchPortHandler.kt | 15 - .../ui/service/handlers/InitHandler.kt | 23 +- .../ui/service/handlers/SwitchStatus.kt | 49 + .../internal/MultifunctionalProvider.kt | 6 +- .../moe/fuqiuluo/shamrock/ui/tools/tabs.kt | 106 +- .../ksp/impl/OneBotHandlerProcessor.kt | 85 -- .../fuqiuluo/ksp/impl/ProtobufProcessor.kt | 2 +- .../fuqiuluo/ksp/impl/XposedHookProcessor.kt | 22 +- .../OneBotHandlerProcessorProvider.kt | 17 - protobuf/build.gradle.kts | 2 +- xposed/build.gradle.kts | 11 - .../xposed/ipc/bytedata/IByteData.aidl | 8 - .../xposed/ipc/bytedata/IByteDataSign.aidl | 4 - .../shamrock/xposed/ipc/qsign/IQSign.aidl | 4 - .../shamrock/xposed/ipc/qsign/IQSigner.aidl | 14 - xposed/src/main/cpp/clover.cpp | 2 +- .../fuqiuluo/qqinterface/servlet/BaseSvc.kt | 201 --- .../fuqiuluo/qqinterface/servlet/CardSvc.kt | 135 -- .../fuqiuluo/qqinterface/servlet/ChatSvc.kt | 20 - .../fuqiuluo/qqinterface/servlet/FileSvc.kt | 248 ---- .../fuqiuluo/qqinterface/servlet/FriendSvc.kt | 121 -- .../fuqiuluo/qqinterface/servlet/GProSvc.kt | 361 ------ .../fuqiuluo/qqinterface/servlet/GroupSvc.kt | 1152 ----------------- .../fuqiuluo/qqinterface/servlet/LbsSvc.kt | 57 - .../fuqiuluo/qqinterface/servlet/MsgSvc.kt | 524 -------- .../fuqiuluo/qqinterface/servlet/PacketSvc.kt | 112 -- .../fuqiuluo/qqinterface/servlet/QFavSvc.kt | 402 ------ .../fuqiuluo/qqinterface/servlet/QSafeSvc.kt | 33 - .../fuqiuluo/qqinterface/servlet/TicketSvc.kt | 179 --- .../qqinterface/servlet/VisitorSvc.kt | 95 -- .../qqinterface/servlet/ark/ArkMsgSvc.kt | 94 -- .../qqinterface/servlet/ark/LightAppSvc.kt | 45 - .../qqinterface/servlet/ark/WeatherSvc.kt | 74 -- .../servlet/ark/data/ArkAppInfo.kt | 40 - .../qqinterface/servlet/ark/data/Region.kt | 10 - .../qqinterface/servlet/msg/Converter.kt | 112 -- .../qqinterface/servlet/msg/MessageSegment.kt | 34 - .../servlet/msg/MessageTempHandler.kt | 34 - .../servlet/msg/converter/ElemConverter.kt | 593 --------- .../msg/converter/NtMsgElementConverter.kt | 608 --------- .../servlet/msg/maker/ElemMaker.kt | 717 ---------- .../servlet/msg/maker/NtMsgElementMaker.kt | 1103 ---------------- .../qqinterface/servlet/structures/Files.kt | 53 - .../qqinterface/servlet/structures/Group.kt | 30 - .../qqinterface/servlet/structures/Guild.kt | 79 -- .../servlet/structures/RichMedia.kt | 20 - .../servlet/transfile/FileTransfer.kt | 190 --- .../servlet/transfile/NtV2RichMediaSvc.kt | 526 -------- .../transfile/RichMediaUploadHandler.kt | 27 - .../servlet/transfile/RichProtoSvc.kt | 431 ------ .../qqinterface/servlet/transfile/Transfer.kt | 142 -- .../servlet/transfile/data/Contact.kt | 29 - .../servlet/transfile/data/Resource.kt | 31 - .../servlet/transfile/data/RichMedia.kt | 13 - .../moe/fuqiuluo/shamrock/config/ActiveRPC.kt | 7 + .../fuqiuluo/shamrock/config/AliveReply.kt | 6 + .../fuqiuluo/shamrock/config/AntiJvmTrace.kt | 7 + .../moe/fuqiuluo/shamrock/config/B2Mode.kt | 6 + .../moe/fuqiuluo/shamrock/config/ConfigKey.kt | 12 + .../moe/fuqiuluo/shamrock/config/DebugMode.kt | 7 + .../fuqiuluo/shamrock/config/EnableOldBDH.kt | 6 + .../shamrock/config/EnableSelfMessage.kt | 6 + .../fuqiuluo/shamrock/config/ForceTablet.kt | 7 + .../fuqiuluo/shamrock/config/PassiveRPC.kt | 7 + .../fuqiuluo/shamrock/config/RPCAddress.kt | 7 + .../moe/fuqiuluo/shamrock/config/RPCPort.kt | 8 + .../fuqiuluo/shamrock/config/ResourceGroup.kt | 7 + .../shamrock/config/ShamrockConfig.kt | 52 + .../fuqiuluo/shamrock/helper/ContactHelper.kt | 25 +- .../fuqiuluo/shamrock/helper/Exceptions.kt | 8 +- .../shamrock/helper/LocalCacheHelper.kt | 21 - .../moe/fuqiuluo/shamrock/helper/LogCenter.kt | 24 +- .../fuqiuluo/shamrock/helper/MessageHelper.kt | 431 ------ .../fuqiuluo/shamrock/helper/MusicHelper.kt | 97 -- .../shamrock/helper/TroopHonorHelper.kt | 10 +- .../helper => internals}/NTServiceFetcher.kt | 14 +- .../fuqiuluo/shamrock/remote/HTTPServer.kt | 174 --- .../shamrock/remote/action/ActionManager.kt | 177 --- .../remote/action/handlers/AdaptShareJson.kt | 44 - .../remote/action/handlers/BanTroopMember.kt | 34 - .../remote/action/handlers/CleanCache.kt | 29 - .../remote/action/handlers/ClearMsgs.kt | 32 - .../action/handlers/CreateGroupFileFolder.kt | 28 - .../remote/action/handlers/CreateGuildRole.kt | 42 - .../action/handlers/DeleteEssenceMessage.kt | 34 - .../remote/action/handlers/DeleteGroupFile.kt | 27 - .../action/handlers/DeleteGroupFolder.kt | 26 - .../remote/action/handlers/DeleteGuildRole.kt | 24 - .../remote/action/handlers/DeleteMessage.kt | 23 - .../remote/action/handlers/DownloadFile.kt | 140 -- .../remote/action/handlers/FavAddImageMsg.kt | 159 --- .../remote/action/handlers/FavAddTextMsg.kt | 74 -- .../action/handlers/FavGetItemContent.kt | 65 - .../remote/action/handlers/FavGetItemList.kt | 113 -- .../remote/action/handlers/GetCSRF.kt | 31 - .../remote/action/handlers/GetCookies.kt | 32 - .../remote/action/handlers/GetCredentials.kt | 40 - .../action/handlers/GetDeviceBattery.kt | 19 - .../action/handlers/GetEssenceMessageList.kt | 29 - .../remote/action/handlers/GetForwardMsg.kt | 30 - .../remote/action/handlers/GetFriendList.kt | 37 - .../action/handlers/GetFriendSystemMsg.kt | 45 - .../action/handlers/GetGProChannelList.kt | 38 - .../action/handlers/GetGroupFileSystemInfo.kt | 22 - .../remote/action/handlers/GetGroupFileUrl.kt | 24 - .../action/handlers/GetGroupMsgHistory.kt | 29 - .../remote/action/handlers/GetGroupNotice.kt | 25 - .../handlers/GetGroupRemainAtAllRemain.kt | 29 - .../action/handlers/GetGroupRootFiles.kt | 25 - .../action/handlers/GetGroupSubFiles.kt | 26 - .../action/handlers/GetGroupSystemMsg.kt | 65 - .../remote/action/handlers/GetGuildFeeds.kt | 38 - .../remote/action/handlers/GetGuildList.kt | 35 - .../action/handlers/GetGuildMemberList.kt | 79 -- .../action/handlers/GetGuildMemberProfile.kt | 78 -- .../action/handlers/GetGuildMetaByGuest.kt | 59 - .../remote/action/handlers/GetGuildRoles.kt | 61 - .../action/handlers/GetGuildServiceProfile.kt | 39 - .../remote/action/handlers/GetHistoryMsg.kt | 133 -- .../remote/action/handlers/GetHttpCookies.kt | 29 - .../remote/action/handlers/GetImage.kt | 73 -- .../remote/action/handlers/GetLatestEvents.kt | 17 - .../remote/action/handlers/GetLoginInfo.kt | 33 - .../remote/action/handlers/GetModelShow.kt | 31 - .../action/handlers/GetModelShowList.kt | 115 -- .../shamrock/remote/action/handlers/GetMsg.kt | 56 - .../action/handlers/GetNotJoinedGroupInfo.kt | 25 - .../action/handlers/GetOnlineClients.kt | 38 - .../remote/action/handlers/GetProfileCard.kt | 78 -- .../handlers/GetProhibitedMemberList.kt | 29 - .../remote/action/handlers/GetRecord.kt | 42 - .../remote/action/handlers/GetSelfInfo.kt | 24 - .../remote/action/handlers/GetStatus.kt | 23 - .../remote/action/handlers/GetStrangerInfo.kt | 160 --- .../action/handlers/GetSupportedActions.kt | 15 - .../remote/action/handlers/GetTroopHonor.kt | 58 - .../remote/action/handlers/GetTroopInfo.kt | 41 - .../remote/action/handlers/GetTroopList.kt | 46 - .../action/handlers/GetTroopMemberInfo.kt | 67 - .../action/handlers/GetTroopMemberList.kt | 79 -- .../shamrock/remote/action/handlers/GetUid.kt | 29 - .../remote/action/handlers/GetUinByUid.kt | 28 - .../remote/action/handlers/GetVersionInfo.kt | 30 - .../remote/action/handlers/GetWeather.kt | 36 - .../action/handlers/GetWeatherCityCode.kt | 29 - .../remote/action/handlers/GroupPoke.kt | 24 - .../remote/action/handlers/IsBlackListUin.kt | 41 - .../remote/action/handlers/KickTroopMember.kt | 27 - .../remote/action/handlers/LeaveTroop.kt | 26 - .../action/handlers/ModifyTroopMemberName.kt | 30 - .../remote/action/handlers/ModifyTroopName.kt | 29 - .../action/handlers/ModifyTroopRemark.kt | 25 - .../remote/action/handlers/QuickOperation.kt | 154 --- .../action/handlers/RenameGroupFolder.kt | 27 - .../handlers/RequestUploadGroupImage.kt | 29 - .../remote/action/handlers/RestartMe.kt | 18 - .../remote/action/handlers/ScanQRCode.kt | 18 - .../action/handlers/SendForwardMessage.kt | 83 -- .../handlers/SendGroupForwardMessage.kt | 28 - .../action/handlers/SendGroupMessage.kt | 36 - .../remote/action/handlers/SendGroupNotice.kt | 38 - .../remote/action/handlers/SendGroupSign.kt | 35 - .../action/handlers/SendGuildMessage.kt | 112 -- .../remote/action/handlers/SendLike.kt | 29 - .../remote/action/handlers/SendMessage.kt | 187 --- .../remote/action/handlers/SendMsgByResid.kt | 59 - .../handlers/SendPrivateForwardMessage.kt | 30 - .../action/handlers/SendPrivateMessage.kt | 47 - .../action/handlers/SetEssenceMessage.kt | 34 - .../action/handlers/SetFriendAddRequest.kt | 46 - .../action/handlers/SetGroupAddRequest.kt | 57 - .../remote/action/handlers/SetGroupAdmin.kt | 26 - .../action/handlers/SetGroupCommentFace.kt | 30 - .../remote/action/handlers/SetGroupUnique.kt | 28 - .../action/handlers/SetGroupWholeBan.kt | 24 - .../action/handlers/SetGuildMemberRole.kt | 42 - .../remote/action/handlers/SetModelShow.kt | 35 - .../remote/action/handlers/SetProfileCard.kt | 45 - .../remote/action/handlers/SwitchAccount.kt | 35 - .../remote/action/handlers/TestHandler.kt | 31 - .../remote/action/handlers/UpdateGuildRole.kt | 28 - .../action/handlers/UploadFileToShamrock.kt | 59 - .../remote/action/handlers/UploadGroupFile.kt | 163 --- .../action/handlers/UploadMultiMessage.kt | 82 -- .../action/handlers/UploadNtResource.kt | 82 -- .../action/handlers/UploadPrivateFile.kt | 154 --- .../fuqiuluo/shamrock/remote/api/BDHWorker.kt | 50 - .../shamrock/remote/api/FavouriteWeiyun.kt | 48 - .../shamrock/remote/api/FriendAction.kt | 40 - .../shamrock/remote/api/GenerateQSign.kt | 437 ------- .../shamrock/remote/api/GetFrameworkInfo.kt | 27 - .../shamrock/remote/api/GroupAction.kt | 174 --- .../shamrock/remote/api/GuildAction.kt | 171 --- .../shamrock/remote/api/LogExplorer.kt | 17 - .../fuqiuluo/shamrock/remote/api/MainRoute.kt | 111 -- .../shamrock/remote/api/MessageAction.kt | 385 ------ .../shamrock/remote/api/OtherAction.kt | 147 --- .../shamrock/remote/api/ProfileAction.kt | 86 -- .../shamrock/remote/api/ProtocolAction.kt | 79 -- .../shamrock/remote/api/RequestAction.kt | 39 - .../shamrock/remote/api/ResourceAction.kt | 101 -- .../shamrock/remote/api/TestAction.kt | 54 - .../shamrock/remote/api/TicketAction.kt | 81 -- .../shamrock/remote/api/UserAction.kt | 56 - .../shamrock/remote/api/WeatherAction.kt | 26 - .../remote/config/ContentNegotiation.kt | 17 - .../shamrock/remote/config/StatusPages.kt | 77 -- .../fuqiuluo/shamrock/remote/plugin/Auth.kt | 53 - .../shamrock/remote/service/HttpService.kt | 144 --- .../shamrock/remote/service/PacketReceiver.kt | 69 - .../remote/service/WebSocketClientService.kt | 54 - .../remote/service/WebSocketService.kt | 99 -- .../remote/service/api/BaseTransmitServlet.kt | 24 - .../service/api/GlobalEventTransmitter.kt | 592 --------- .../remote/service/api/HttpTransmitServlet.kt | 65 - .../service/api/WebSocketClientServlet.kt | 192 --- .../service/api/WebSocketTransmitServlet.kt | 157 --- .../shamrock/remote/service/config/Config.kt | 41 - .../remote/service/config/ShamrockConfig.kt | 284 ---- .../shamrock/remote/service/data/BotStatus.kt | 30 - .../remote/service/data/FriendData.kt | 37 - .../remote/service/data/GroupAnnouncement.kt | 25 - .../remote/service/data/GroupHonor.kt | 39 - .../shamrock/remote/service/data/Message.kt | 70 - .../shamrock/remote/service/data/Platform.kt | 68 - .../remote/service/data/ResourceData.kt | 9 - .../remote/service/data/TicketData.kt | 17 - .../shamrock/remote/service/data/TroopData.kt | 70 - .../remote/service/data/VersionInfo.kt | 18 - .../shamrock/remote/service/data/VipData.kt | 22 - .../service/data/profile/ProfileCard.kt | 37 - .../data/profile/ProfileProtocolConst.java | 41 - .../remote/service/data/push/MessageEvent.kt | 106 -- .../remote/service/data/push/NoticeEvent.kt | 153 --- .../remote/service/data/push/PushMetaEvent.kt | 32 - .../remote/service/listener/AioListener.kt | 582 --------- .../service/listener/GroupEventListener.kt | 136 -- .../service/listener/KernelGuildListener.kt | 753 ----------- .../service/listener/NetworkListener.kt | 11 - .../service/listener/PrimitiveListener.kt | 625 --------- .../remote/structures/CommonResult.kt | 56 - .../remote/structures/CurrentAccount.kt | 17 - .../shamrock/remote/structures/ErrorCatch.kt | 9 - .../shamrock/remote/structures/IndexData.kt | 11 - .../shamrock/remote/structures/Protocol.kt | 43 - .../remote/structures/SendMsgResult.kt | 8 - .../moe/fuqiuluo/shamrock/tools/AndroidX.kt | 16 + .../java/moe/fuqiuluo/shamrock/tools/Json.kt | 10 - .../moe/fuqiuluo/shamrock/tools/KtorServer.kt | 366 ------ .../fuqiuluo/shamrock/tools/MutBroadcast.kt | 14 - .../moe/fuqiuluo/shamrock/tools/Version.kt | 10 +- .../moe/fuqiuluo/shamrock/utils/AudioUtils.kt | 2 +- .../fuqiuluo/shamrock/xposed/XposedEntry.kt | 37 +- .../{hooks => actions}/AntiDetection.kt | 11 +- .../xposed/actions/DynamicBroadcast.kt | 44 + .../xposed/{hooks => actions}/FetchService.kt | 4 +- .../FixAudioLibraryLoader.kt} | 6 +- .../xposed/{hooks => actions}/ForceTablet.kt | 12 +- .../xposed/{hooks => actions}/IAction.kt | 2 +- .../xposed/actions/InitRemoteService.kt | 15 + .../ListenShamrockUpdate.kt | 2 +- .../xposed/{hooks => actions}/NoBackGround.kt | 2 +- .../shamrock/xposed/actions/PullConfig.kt | 14 + .../xposed/actions/interacts/IInteract.kt | 7 + .../shamrock/xposed/actions/interacts/Init.kt | 24 + .../xposed/actions/interacts/SwitchStatus.kt | 19 + .../shamrock/xposed/helper/AppTalker.kt | 13 +- .../xposed/{loader => helper}/KeepAlive.kt | 22 +- .../shamrock/xposed/helper/PacketHandler.kt | 59 - .../xposed/helper/internal/DataRequester.kt | 113 -- .../xposed/helper/internal/DynamicReceiver.kt | 78 -- .../shamrock/xposed/hooks/DataReceiver.kt | 67 - .../shamrock/xposed/hooks/GuidLock.kt | 75 -- .../shamrock/xposed/hooks/HookForDebug.kt | 73 -- .../shamrock/xposed/hooks/HookWrapperCodec.kt | 139 -- .../xposed/hooks/InitRemoteService.kt | 133 -- .../shamrock/xposed/hooks/IpcService.kt | 44 - .../shamrock/xposed/hooks/PullConfig.kt | 95 -- .../shamrock/xposed/ipc/ShamrockIpc.kt | 45 - .../shamrock/xposed/ipc/bytedata/ByteData.kt | 41 - .../shamrock/xposed/ipc/qsign/QSign.kt | 78 -- .../service/QQInterfaces.kt} | 12 +- .../qq/service/contact/LocalCacheHelper.kt | 23 + .../service/internals}/NTServiceHelper.kt | 2 +- 293 files changed, 680 insertions(+), 24540 deletions(-) create mode 100644 app/src/main/java/moe/fuqiuluo/shamrock/app/config/ConfigKey.kt create mode 100644 app/src/main/java/moe/fuqiuluo/shamrock/app/config/ShamrockConfig.kt delete mode 100644 app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt delete mode 100644 app/src/main/java/moe/fuqiuluo/shamrock/ui/service/DashboardInitializer.kt delete mode 100644 app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/FetchPortHandler.kt create mode 100644 app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/SwitchStatus.kt delete mode 100644 processor/src/main/java/moe/fuqiuluo/ksp/impl/OneBotHandlerProcessor.kt delete mode 100644 processor/src/main/java/moe/fuqiuluo/ksp/providers/OneBotHandlerProcessorProvider.kt delete mode 100644 xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteData.aidl delete mode 100644 xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteDataSign.aidl delete mode 100644 xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSign.aidl delete mode 100644 xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSigner.aidl delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ChatSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FileSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FriendSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GProSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/LbsSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QFavSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QSafeSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/VisitorSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/ArkMsgSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/LightAppSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/WeatherSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/ArkAppInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/Region.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageTempHandler.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Files.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Group.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Guild.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/RichMedia.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/FileTransfer.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/NtV2RichMediaSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichMediaUploadHandler.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/Transfer.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/Contact.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/Resource.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/RichMedia.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/ActiveRPC.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/AliveReply.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/AntiJvmTrace.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/B2Mode.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/ConfigKey.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/DebugMode.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableSelfMessage.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/ForceTablet.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/PassiveRPC.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/RPCAddress.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/RPCPort.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/ResourceGroup.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MusicHelper.kt rename xposed/src/main/java/moe/fuqiuluo/shamrock/{xposed/helper => internals}/NTServiceFetcher.kt (88%) delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/HTTPServer.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/ActionManager.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/AdaptShareJson.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/BanTroopMember.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CleanCache.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ClearMsgs.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CreateGroupFileFolder.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CreateGuildRole.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteEssenceMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGroupFile.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGroupFolder.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGuildRole.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DownloadFile.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavAddImageMsg.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavAddTextMsg.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavGetItemContent.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavGetItemList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCSRF.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCookies.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCredentials.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetDeviceBattery.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetEssenceMessageList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetFriendList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetFriendSystemMsg.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGProChannelList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupFileSystemInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupFileUrl.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupMsgHistory.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupNotice.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRemainAtAllRemain.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRootFiles.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupSubFiles.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupSystemMsg.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildFeeds.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMemberList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMemberProfile.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMetaByGuest.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildRoles.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildServiceProfile.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHttpCookies.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetImage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetLatestEvents.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetLoginInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetModelShow.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetModelShowList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetNotJoinedGroupInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetOnlineClients.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetProfileCard.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetProhibitedMemberList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetRecord.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetSelfInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetStatus.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetStrangerInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetSupportedActions.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopHonor.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopMemberInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopMemberList.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetUid.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetUinByUid.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetVersionInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetWeather.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetWeatherCityCode.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GroupPoke.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/IsBlackListUin.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/KickTroopMember.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/LeaveTroop.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopMemberName.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopName.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopRemark.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/QuickOperation.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RenameGroupFolder.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RequestUploadGroupImage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RestartMe.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ScanQRCode.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupNotice.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupSign.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGuildMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendLike.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMsgByResid.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetEssenceMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetFriendAddRequest.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupAddRequest.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupAdmin.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupCommentFace.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupUnique.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupWholeBan.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGuildMemberRole.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetModelShow.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetProfileCard.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SwitchAccount.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/TestHandler.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UpdateGuildRole.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadFileToShamrock.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadGroupFile.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadNtResource.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadPrivateFile.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/BDHWorker.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/FavouriteWeiyun.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/FriendAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GenerateQSign.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GetFrameworkInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GroupAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GuildAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/LogExplorer.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MainRoute.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/OtherAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ProfileAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ProtocolAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/RequestAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ResourceAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TestAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TicketAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/UserAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/WeatherAction.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/config/ContentNegotiation.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/config/StatusPages.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/plugin/Auth.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/HttpService.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/PacketReceiver.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketClientService.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketService.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/BaseTransmitServlet.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/HttpTransmitServlet.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/WebSocketClientServlet.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/WebSocketTransmitServlet.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/Config.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/BotStatus.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/FriendData.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/GroupAnnouncement.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/GroupHonor.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Platform.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/ResourceData.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/TicketData.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/TroopData.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/VersionInfo.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/VipData.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/profile/ProfileCard.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/profile/ProfileProtocolConst.java delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/MessageEvent.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/NoticeEvent.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/PushMetaEvent.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/GroupEventListener.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/KernelGuildListener.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/NetworkListener.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/CommonResult.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/CurrentAccount.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/ErrorCatch.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/IndexData.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/Protocol.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/SendMsgResult.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/tools/AndroidX.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/tools/KtorServer.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/tools/MutBroadcast.kt rename xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/{hooks => actions}/AntiDetection.kt (96%) create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/DynamicBroadcast.kt rename xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/{hooks => actions}/FetchService.kt (92%) rename xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/{hooks/FixLibraryLoad.kt => actions/FixAudioLibraryLoader.kt} (85%) rename xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/{hooks => actions}/ForceTablet.kt (87%) rename xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/{hooks => actions}/IAction.kt (69%) create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt rename xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/{hooks => actions}/ListenShamrockUpdate.kt (96%) rename xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/{hooks => actions}/NoBackGround.kt (96%) create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PullConfig.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/IInteract.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/Init.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/SwitchStatus.kt rename xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/{loader => helper}/KeepAlive.kt (91%) delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/PacketHandler.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/internal/DataRequester.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/internal/DynamicReceiver.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/DataReceiver.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/GuidLock.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/HookForDebug.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/HookWrapperCodec.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/InitRemoteService.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/IpcService.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/PullConfig.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/ShamrockIpc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/ByteData.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/qsign/QSign.kt rename xposed/src/main/java/{moe/fuqiuluo/shamrock/xposed/helper/AppRuntimeFetcher.kt => qq/service/QQInterfaces.kt} (51%) create mode 100644 xposed/src/main/java/qq/service/contact/LocalCacheHelper.kt rename xposed/src/main/java/{moe/fuqiuluo/shamrock/xposed/helper => qq/service/internals}/NTServiceHelper.kt (93%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 24c3666..64fffd2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { minSdk = 27 targetSdk = 34 versionCode = getVersionCode() - versionName = "1.0.9" + ".r${getGitCommitCount()}." + getVersionName() + versionName = "1.1.0" + ".r${getGitCommitCount()}." + getVersionName() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -201,11 +201,6 @@ dependencies { implementation("io.coil-kt:coil-compose:2.4.0") implementation(kotlinx("io-jvm", "0.1.16")) - implementation(ktor("server", "core")) - implementation(ktor("server", "host-common")) - implementation(ktor("server", "status-pages")) - implementation(ktor("server", "netty")) - implementation(ktor("server", "content-negotiation")) implementation(ktor("client", "core")) implementation(ktor("client", "content-negotiation")) implementation(ktor("client", "cio")) diff --git a/app/src/main/cpp/shamrock.cpp b/app/src/main/cpp/shamrock.cpp index 9e90b5c..ce6a499 100644 --- a/app/src/main/cpp/shamrock.cpp +++ b/app/src/main/cpp/shamrock.cpp @@ -12,7 +12,7 @@ extern "C" JNIEXPORT jstring JNICALL -Java_moe_fuqiuluo_shamrock_xposed_hooks_PullConfig_testNativeLibrary(JNIEnv *env, jobject thiz) { +Java_moe_fuqiuluo_shamrock_xposed_actions_interacts_Init_testNativeLibrary(JNIEnv *env, jobject thiz) { return env->NewStringUTF("加载Shamrock库成功~"); } diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt b/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt index 5b0b155..83abc6a 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt @@ -4,6 +4,7 @@ package moe.fuqiuluo.shamrock import android.annotation.SuppressLint import android.os.Bundle +import android.os.Handler import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -64,7 +65,9 @@ import androidx.compose.ui.unit.sp import androidx.core.view.WindowCompat import com.google.accompanist.systemuicontroller.rememberSystemUiController import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import moe.fuqiuluo.shamrock.tools.GlobalUi import moe.fuqiuluo.shamrock.ui.app.AppRuntime import moe.fuqiuluo.shamrock.ui.app.Logger import moe.fuqiuluo.shamrock.ui.app.RuntimeState @@ -85,8 +88,16 @@ import moe.fuqiuluo.shamrock.ui.tools.getShamrockVersion class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContent { + LaunchedEffect(Unit) { + while (true) { + delay(15_000) // Delay in milliseconds + broadcastToModule { + putExtra("__cmd", "switch_status") + } + } + } + CompositionLocalProvider( LocalIndication provides NoIndication ) { @@ -96,8 +107,9 @@ class MainActivity : ComponentActivity() { isAppearanceLightStatusBars = true } WindowCompat.setDecorFitsSystemWindows(window, true) - broadcastToModule { putExtra("__cmd", "fetchPort") } } + + GlobalUi = Handler(mainLooper) } } @@ -153,7 +165,7 @@ private fun AppMainView() { } val ctx = LocalContext.current - LaunchedEffect(isFined.value) { + LaunchedEffect(isFined) { if (isFined.value) { AppRuntime.log(LocalString.logCentralLoadSuccessfully) Toast.makeText(ctx, LocalString.frameworkYes, Toast.LENGTH_SHORT).show() @@ -284,58 +296,11 @@ private fun AnimatedTab( val lastSelectedState = remember { mutableIntStateOf(0) } - val enter = remember { - scaleIn(animationSpec = TweenSpec(150, easing = FastOutLinearInEasing)) - } - val exit = remember { - scaleOut(animationSpec = TweenSpec(150, easing = FastOutSlowInEasing)) - } val defaultConst = SELECTED_TABLE[index * 2] val selectedConst = SELECTED_TABLE[(index * 2) + 1] val isFirst: Boolean = (lastSelectedState.value and defaultConst) != defaultConst - var icon: @Composable (() -> Unit)? = null - var text: @Composable (() -> Unit)? = null - - if (curSelected) { - text = { - AnimatedVisibility(visibleState = MutableTransitionState(false).also { - it.targetState = - isFirst || lastSelectedState.value and selectedConst == selectedConst - }, enter = enter, exit = exit, modifier = Modifier) { - Text( - text = titleWithIcon.first, - color = GlobalColor.TabItem, - fontSize = 15.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier - .padding(bottom = 5.dp) - .indication( - remember { MutableInteractionSource() }, - rememberRipple(color = Color.Transparent) - ) - ) - } - } - } else { - icon = { - Icon( - painter = painterResource(id = titleWithIcon.second), - contentDescription = titleWithIcon.first, - tint = Color.Unspecified, - modifier = Modifier - .height(24.dp) - .width(24.dp) - .padding(bottom = 5.dp) - .indication( - remember { MutableInteractionSource() }, - rememberRipple(color = Color.Transparent) - ) - ) - } - } - ShamrockTab( selected = curSelected, onClick = { @@ -343,11 +308,13 @@ private fun AnimatedTab( state.scrollToPage(index, 0f) } }, - text = text, - icon = icon, selectedContentColor = Color.Transparent, unselectedContentColor = Color.Transparent, - indication = null + indication = null, + titleWithIcon = titleWithIcon, + visibleState = MutableTransitionState(false).also { + it.targetState = isFirst || lastSelectedState.value and selectedConst == selectedConst + } ) lastSelectedState.value.let { var tmp = it diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ConfigKey.kt b/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ConfigKey.kt new file mode 100644 index 0000000..40ad8b0 --- /dev/null +++ b/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ConfigKey.kt @@ -0,0 +1,2 @@ +package moe.fuqiuluo.shamrock.app.config + diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ShamrockConfig.kt b/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ShamrockConfig.kt new file mode 100644 index 0000000..21c2a2d --- /dev/null +++ b/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ShamrockConfig.kt @@ -0,0 +1,33 @@ +package moe.fuqiuluo.shamrock.app.config + +import android.content.Context +import moe.fuqiuluo.shamrock.config.ConfigKey +import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule + +object ShamrockConfig { + internal fun getConfigPref(ctx: Context) = ctx.getSharedPreferences("config", 0) + + internal inline operator fun get(ctx: Context, key: ConfigKey): Type { + val preferences = getConfigPref(ctx) + return when(Type::class) { + Int::class -> preferences.getInt(key.name(), key.default() as Int) as Type + Long::class -> preferences.getLong(key.name(), key.default() as Long) as Type + String::class -> preferences.getString(key.name(), key.default() as String) as Type + Boolean::class -> preferences.getBoolean(key.name(), key.default() as Boolean) as Type + else -> throw IllegalArgumentException("Unsupported type") + } + } + + internal inline operator fun set(ctx: Context, key: ConfigKey, value: Type) { + val preferences = getConfigPref(ctx) + val editor = preferences.edit() + when(Type::class) { + Int::class -> editor.putInt(key.name(), value as Int) + Long::class -> editor.putLong(key.name(), value as Long) + String::class -> editor.putString(key.name(), value as String) + Boolean::class -> editor.putBoolean(key.name(), value as Boolean) + else -> throw IllegalArgumentException("Unsupported type") + } + editor.apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt deleted file mode 100644 index f12999b..0000000 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt +++ /dev/null @@ -1,395 +0,0 @@ -package moe.fuqiuluo.shamrock.ui.app - -import android.content.Context -import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule - -object ShamrockConfig { - fun getSSLKeyPath(ctx: Context): String { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getString("key_store", "")!! - } - - fun setSSLKeyPath(ctx: Context, path: String) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putString("key_store", path).apply() - pushUpdate(ctx) - } - - fun getSSLPort(ctx: Context): Int { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getInt("ssl_port", 5701) - } - - fun setSSLPort(ctx: Context, port: Int) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putInt("ssl_port", port).apply() - pushUpdate(ctx) - } - - fun getSSLAlias(ctx: Context): String { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getString("ssl_alias", "")!! - } - - fun setSSLAlias(ctx: Context, alias: String) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putString("ssl_alias", alias).apply() - pushUpdate(ctx) - } - - fun getSSLPwd(ctx: Context): String { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getString("ssl_pwd", "")!! - } - - fun setSSLPwd(ctx: Context, alias: String) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putString("ssl_pwd", alias).apply() - pushUpdate(ctx) - } - - fun getSSLPrivatePwd(ctx: Context): String { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getString("ssl_private_pwd", "")!! - } - - fun setSSLPrivatePwd(ctx: Context, alias: String) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putString("ssl_private_pwd", alias).apply() - pushUpdate(ctx) - } - - fun getHttpAddr(ctx: Context): String { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getString("http_addr", "")!! - } - - fun setHttpAddr(ctx: Context, v: String) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putString("http_addr", v).apply() - pushUpdate(ctx) - } - - fun isPro(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("pro_api", false) - } - - fun setPro(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("pro_api", v).apply() - ctx.broadcastToModule { - putExtra("type", "restart") - putExtra("__cmd", "change_port") - } - pushUpdate(ctx) - } - - fun getToken(ctx: Context): String { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getString("token", null) ?: "" - } - - fun setToken(ctx: Context, v: String?) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putString("token", v).apply() - pushUpdate(ctx) - } - - fun isWs(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("ws", false) - } - - fun setWs(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("ws", v).apply() - pushUpdate(ctx) - } - - fun isWsClient(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("ws_client", false) - } - - fun setWsClient(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("ws_client", v).apply() - pushUpdate(ctx) - } - - fun isTablet(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("tablet", false) - } - - fun setTablet(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("tablet", v).apply() - pushUpdate(ctx) - } - - fun isUseCQCode(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("use_cqcode", false) - } - - fun setUseCQCode(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("use_cqcode", v).apply() - pushUpdate(ctx) - } - - fun isWebhook(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("webhook", false) - } - - fun setWebhook(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("webhook", v).apply() - pushUpdate(ctx) - } - - fun getWsAddr(ctx: Context): String { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getString("ws_addr", "")!! - } - - fun setWsAddr(ctx: Context, v: String) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putString("ws_addr", v).apply() - pushUpdate(ctx) - } - - fun getUploadResourceGroup(ctx: Context): String { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getString("up_res_group", "100000000")!! - } - - fun setUploadResourceGroup(ctx: Context, v: String) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putString("up_res_group", v).apply() - pushUpdate(ctx) - } - - fun getHttpPort(ctx: Context): Int { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getInt("port", 5700) - } - - fun setHttpPort(ctx: Context, v: Int) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putInt("port", v).apply() - ctx.broadcastToModule { - putExtra("type", "port") - putExtra("port", v) - putExtra("__cmd", "change_port") - } - pushUpdate(ctx) - } - - fun getWsPort(ctx: Context): Int { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getInt("ws_port", 5800) - } - - fun setWsPort(ctx: Context, v: Int) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putInt("ws_port", v).apply() - ctx.broadcastToModule { - putExtra("type", "ws_port") - putExtra("port", v) - putExtra("__cmd", "change_port") - } - pushUpdate(ctx) - } - - fun is2B(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("2B", false) - } - - fun set2B(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("2B", v).apply() - } - - fun setAutoClean(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("auto_clear", v).apply() - } - - fun isAutoClean(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("auto_clear", false) - } - - fun isDebug(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("debug", false) - } - - fun setDebug(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("debug", v).apply() - } - - fun isAntiTrace(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("anti_qq_trace", true) - } - - fun isForbidUselessProcess(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("forbid_useless_process", false) - } - - fun setForbidUselessProcess(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("forbid_useless_process", v).apply() - } - - fun setAntiTrace(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("anti_qq_trace", v).apply() - } - - fun isInjectPacket(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("inject_packet", false) - } - - fun setInjectPacket(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("inject_packet", v).apply() - } - - fun enableAutoStart(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("enable_auto_start", false) - } - - fun disableAutoSyncSetting(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("disable_auto_sync_setting", false) - } - - fun enableAliveReply(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("alive_reply", false) - } - - fun allowShell(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("shell", false) - } - - fun setAutoStart(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("enable_auto_start", v).apply() - } - - fun setDisableAutoSyncSetting(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("disable_auto_sync_setting", v).apply() - } - - fun setAliveReply(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("alive_reply", v).apply() - } - - fun setShellStatus(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("shell", v).apply() - } - - fun enableSelfMsg(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("enable_self_msg", false) - } - - fun enableOldBDH(ctx: Context): Boolean { - val preferences = ctx.getSharedPreferences("config", 0) - return preferences.getBoolean("enable_old_bdh", false) - } - - fun setEnableOldBDH(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("enable_old_bdh", v).apply() - } - - 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 setEnableSyncMsgAsSentMsg(ctx: Context, v: Boolean) { - val preferences = ctx.getSharedPreferences("config", 0) - preferences.edit().putBoolean("enable_sync_msg_as_sent_msg", v).apply() - } - - fun getConfigMap(ctx: Context): Map { - val preferences = ctx.getSharedPreferences("config", 0) - return mapOf( - "tablet" to preferences.getBoolean("tablet", false), - "port" to preferences.getInt("port", 5700), - "ws" to preferences.getBoolean("ws", false), - "ws_port" to preferences.getInt("ws_port", 5800), - "ssl_port" to preferences.getInt("ssl_port", 5701), - "http" to preferences.getBoolean("webhook", false), - "http_addr" to preferences.getString("http_addr", ""), - "ws_client" to preferences.getBoolean("ws_client", false), - "use_cqcode" to preferences.getBoolean("use_cqcode", false), - "ws_addr" to preferences.getString("ws_addr", ""), - "ssl_alias" to preferences.getString("ssl_alias", ""), - "pro_api" to preferences.getBoolean("pro_api", false), - "token" to preferences.getString("token", null), - "ssl_pwd" to preferences.getString("ssl_pwd", ""), - "inject_packet" to preferences.getBoolean("inject_packet", false), - "debug" to preferences.getBoolean("debug", false), - "anti_qq_trace" to preferences.getBoolean("anti_qq_trace", true), - "ssl_private_pwd" to preferences.getString("ssl_private_pwd", ""), - "key_store" to preferences.getString("key_store", ""), - "enable_self_msg" to preferences.getBoolean("enable_self_msg", false), - "echo_number" to preferences.getBoolean("echo_number", false), - "shell" to preferences.getBoolean("shell", false), - "alive_reply" to preferences.getBoolean("alive_reply", false), - "enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false), - "disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false), - "forbid_useless_process" to preferences.getBoolean("forbid_useless_process", false), - "enable_old_bdh" to preferences.getBoolean("enable_old_bdh", false), - "up_res_group" to preferences.getString("up_res_group", ""), - ) - } - - fun pushUpdate(ctx: Context) { - ctx.broadcastToModule { - getConfigMap(ctx).forEach { (key, value) -> - if (value == null) { - val v: String? = null - this.putExtra(key, v) - } else { - when (value) { - is Int -> this.putExtra(key, value) - is Long -> this.putExtra(key, value) - is Short -> this.putExtra(key, value) - is Byte -> this.putExtra(key, value) - is String -> this.putExtra(key, value) - is ByteArray -> this.putExtra(key, value) - is Boolean -> this.putExtra(key, value) - is Float -> this.putExtra(key, value) - is Double -> this.putExtra(key, value) - } - } - } - putExtra("__cmd", "push_config") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt index 9d195c2..68daf10 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt @@ -5,7 +5,6 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.os.Build -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -25,6 +24,7 @@ import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,10 +45,12 @@ import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import coil.size.Size import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import moe.fuqiuluo.shamrock.R import moe.fuqiuluo.shamrock.ui.app.AppRuntime import moe.fuqiuluo.shamrock.ui.app.Level -import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig +import moe.fuqiuluo.shamrock.app.config.ShamrockConfig +import moe.fuqiuluo.shamrock.config.* import moe.fuqiuluo.shamrock.ui.theme.GlobalColor import moe.fuqiuluo.shamrock.ui.theme.LocalString import moe.fuqiuluo.shamrock.ui.theme.ThemeColor @@ -72,110 +74,6 @@ fun DashboardFragment( InformationCard(ctx) APIInfoCard(ctx) FunctionCard(scope, ctx, LocalString.functionSetting) - SSLCard(ctx) - } -} - -@Composable -private fun SSLCard(ctx: Context) { - ActionBox( - modifier = Modifier.padding(top = 12.dp), - painter = painterResource(id = R.drawable.baseline_security_24), - title = LocalString.sslSetting - ) { - Column { - Divider( - modifier = Modifier, - color = GlobalColor.Divider, - thickness = 0.2.dp - ) - - val sslPort = remember { mutableStateOf(ShamrockConfig.getSSLPort(ctx).toString()) } - TextItem( - title = "SSL端口", - desc = "端口范围在0~65565,并确保可用。", - text = sslPort, - hint = "请输入端口号", - error = "端口范围应在0~65565", - checker = { - it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false) - }, - confirm = { - val newPort = sslPort.value.toInt() - ShamrockConfig.setSSLPort(ctx, newPort) - AppRuntime.log("设置SSL(HTTP)端口为$newPort,立即生效尝试中。") - } - ) - - val keyStore = remember { mutableStateOf(ShamrockConfig.getSSLKeyPath(ctx)) } - TextItem( - title = "SSL证书", - desc = "BKS签名的证书。", - text = keyStore, - hint = "输入证书路径", - error = "证书路径不合法或不存在", - checker = { - it.isNotBlank() - }, - confirm = { - val new = keyStore.value - ShamrockConfig.setSSLKeyPath(ctx, new) - AppRuntime.log("设置SSL证书为[$new]。") - } - ) - - val alias = remember { mutableStateOf(ShamrockConfig.getSSLAlias(ctx)) } - TextItem( - title = "SSL别名", - desc = "BKS签名的别名,确保大小写区分正确。", - text = alias, - hint = "输入签名别名", - error = "别名不合法", - checker = { - it.isNotBlank() - }, - confirm = { - val new = alias.value - ShamrockConfig.setSSLAlias(ctx, new) - AppRuntime.log("设置SSL别名为[$new]。") - } - ) - - val sslPwd = remember { mutableStateOf(ShamrockConfig.getSSLPwd(ctx)) } - TextItem( - title = "SSL密码", - desc = "BKS签名的密码。", - text = sslPwd, - hint = "输入签名密码", - error = "密码不合法", - checker = { - it.isNotBlank() - }, - confirm = { - val new = sslPwd.value - ShamrockConfig.setSSLPwd(ctx, new) - AppRuntime.log("设置SSL密码为[$new]。") - } - ) - - val sslPrivatePwd = remember { mutableStateOf(ShamrockConfig.getSSLPrivatePwd(ctx)) } - TextItem( - title = "SSL Private密码", - desc = "BKS签名的Private密码。", - text = sslPrivatePwd, - hint = "输入Private密码", - error = "密码不合法", - checker = { - it.isNotBlank() - }, - confirm = { - val new = sslPrivatePwd.value - ShamrockConfig.setSSLPrivatePwd(ctx, new) - AppRuntime.log("设置SSL Private密码为[$new]。") - } - ) - - } } } @@ -195,93 +93,37 @@ private fun APIInfoCard( thickness = 0.2.dp ) - val wsPort = remember { mutableStateOf(ShamrockConfig.getWsPort(ctx).toString()) } - val port = remember { mutableStateOf(ShamrockConfig.getHttpPort(ctx).toString()) } + val rpcPort = remember { mutableStateOf(ShamrockConfig[ctx, RPCPort].toString()) } TextItem( - title = "主动HTTP端口", + title = "RPC服务端口", desc = "端口范围在0~65565,并确保可用。", - text = port, + text = rpcPort, hint = "请输入端口号", error = "端口范围应在0~65565", checker = { - it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false) && wsPort.value != it + it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 } + .getOrDefault(false) && rpcPort.value != it }, confirm = { - val newPort = port.value.toInt() - ShamrockConfig.setHttpPort(ctx, newPort) + val newPort = rpcPort.value.toInt() + ShamrockConfig[ctx, RPCPort] = newPort AppRuntime.log("设置主动HTTP监听端口为$newPort,立即生效尝试中。") } ) + val rpcAddress = remember { mutableStateOf(ShamrockConfig[ctx, RPCAddress]) } TextItem( - title = "主动WebSocket端口", - desc = "端口范围在0~65565,并确保可用。", - text = wsPort, - hint = "请输入端口号", - error = "端口范围应在0~65565", - checker = { - it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false) && it != port.value - }, - confirm = { - val newPort = wsPort.value.toInt() - ShamrockConfig.setWsPort(ctx, newPort) - AppRuntime.log("设置主动WebSocket监听端口为$newPort。") - } - ) - - val webHookAddress = remember { mutableStateOf(ShamrockConfig.getHttpAddr(ctx)) } - TextItem( - title = "回调HTTP地址", - desc = "例如:http://shamrock.moe:80。", - text = webHookAddress, + title = "回调RPC地址", + desc = "例如:kritor.support:8081", + text = rpcAddress, hint = "请输入回调地址", error = "输入的地址不合法", checker = { - it.isNotBlank() + it.isEmpty() || it.contains(":") }, confirm = { - if (it.startsWith("http://") || it.startsWith("https://")) { - ShamrockConfig.setHttpAddr(ctx, webHookAddress.value) - AppRuntime.log("设置回调HTTP地址为[${webHookAddress.value}]。") - } else { - Toast.makeText(ctx, "回调地址不合法", Toast.LENGTH_SHORT).show() - webHookAddress.value = "" - } - } - ) - - val wsAddress = remember { mutableStateOf(ShamrockConfig.getWsAddr(ctx)) } - TextItem( - title = "被动WebSocket地址", - desc = "例如:ws://shamrock.moe:81,多个使用逗号分隔。", - text = wsAddress, - hint = "请输入被动地址", - error = "输入的地址不合法", - checker = { - true - }, - confirm = { - if (it.startsWith("ws://") || it.startsWith("wss://") || it.isBlank()) { - ShamrockConfig.setWsAddr(ctx, wsAddress.value) - AppRuntime.log("设置被动WebSocket地址为[${wsAddress.value}]。") - } else { - Toast.makeText(ctx, "被动WebSocket地址不合法", Toast.LENGTH_SHORT).show() - wsAddress.value = "" - } - } - ) - - val authToken = remember { mutableStateOf(ShamrockConfig.getToken(ctx)) } - TextItem( - title = "鉴权Token", - desc = "用于鉴权的Token。", - text = authToken, - hint = "请填写鉴权token", - error = "输入的参数不合法", - checker = { true }, - confirm = { - ShamrockConfig.setToken(ctx, authToken.value) - AppRuntime.log("设置鉴权Token为[${authToken.value}]。") + ShamrockConfig[ctx, RPCAddress] = rpcAddress.value + AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。") } ) @@ -314,50 +156,32 @@ private fun FunctionCard( Function( title = "强制平板模式", desc = "强制QQ使用平板模式,实现共存登录。", - isSwitch = ShamrockConfig.isTablet(ctx) + isSwitch = ShamrockConfig[ctx, ForceTablet] ) { - ShamrockConfig.setTablet(ctx, it) + ShamrockConfig[ctx, ForceTablet] = it return@Function true } Function( - title = "HTTP回调", - desc = "OneBot标准的HTTPAPI回调,Shamrock作为Client。", - isSwitch = ShamrockConfig.isWebhook(ctx) + title = "主动RPC", + desc = "Kritor协议实现RPC", + isSwitch = ShamrockConfig[ctx, ActiveRPC] ) { - ShamrockConfig.setWebhook(ctx, it) + ShamrockConfig[ctx, ActiveRPC] = it return@Function true } Function( - title = "消息格式为CQ码", - desc = "HTTPAPI回调的消息格式,关闭则为消息段。", - isSwitch = ShamrockConfig.isUseCQCode(ctx) + title = "被动RPC", + desc = "Kritor协议实现RPC", + isSwitch = ShamrockConfig[ctx, PassiveRPC] ) { - ShamrockConfig.setUseCQCode(ctx, it) - return@Function true - } - - Function( - title = "主动WebSocket", - desc = "OneBot标准WebSocket,Shamrock作为Server。", - isSwitch = ShamrockConfig.isWs(ctx) - ) { - ShamrockConfig.setWs(ctx, it) - return@Function true - } - - Function( - title = "被动WebSocket", - desc = "OneBot标准WebSocket,Shamrock作为Client。", - isSwitch = ShamrockConfig.isWsClient(ctx) - ) { - ShamrockConfig.setWsClient(ctx, it) + ShamrockConfig[ctx, PassiveRPC] = it return@Function true } run { - val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig.getUploadResourceGroup(ctx)) } + val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig[ctx, ResourceGroup]) } Column( modifier = Modifier .absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp) @@ -380,23 +204,11 @@ private fun FunctionCard( }, confirm = { val groupId = uploadResourceGroup.value - ShamrockConfig.setUploadResourceGroup(ctx, groupId) + ShamrockConfig[ctx, ResourceGroup] = groupId AppRuntime.log("设置接受资源群聊为[$groupId]。") } ) } - - /* - Function( - title = "专业级接口", - desc = "如果你不知道你在做什么,请不要开启本功能。", - descColor = Color.Red, - isSwitch = ShamrockConfig.isPro(ctx) - ) { - ShamrockConfig.setPro(ctx, it) - AppRuntime.log("专业级API = $it", Level.WARN) - return@Function true - }*/ } } } diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt index 3b59952..3df5d16 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt @@ -23,7 +23,14 @@ import androidx.compose.ui.unit.sp import moe.fuqiuluo.shamrock.R import moe.fuqiuluo.shamrock.ui.app.AppRuntime import moe.fuqiuluo.shamrock.ui.app.Level -import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig +import moe.fuqiuluo.shamrock.app.config.ShamrockConfig +import moe.fuqiuluo.shamrock.config.AliveReply +import moe.fuqiuluo.shamrock.config.AntiJvmTrace +import moe.fuqiuluo.shamrock.config.B2Mode +import moe.fuqiuluo.shamrock.config.DebugMode +import moe.fuqiuluo.shamrock.config.EnableOldBDH +import moe.fuqiuluo.shamrock.config.EnableSelfMessage +import moe.fuqiuluo.shamrock.ui.service.handlers.InitHandler import moe.fuqiuluo.shamrock.ui.theme.GlobalColor import moe.fuqiuluo.shamrock.ui.theme.LocalString import moe.fuqiuluo.shamrock.ui.tools.NoticeTextDialog @@ -68,9 +75,9 @@ fun LabFragment() { title = LocalString.b2Mode, desc = LocalString.b2ModeDesc, descColor = it, - isSwitch = ShamrockConfig.is2B(ctx) + isSwitch = ShamrockConfig[ctx, B2Mode] ) { - ShamrockConfig.set2B(ctx, it) + ShamrockConfig[ctx, B2Mode] = it scope.toast(ctx, LocalString.restartToast) return@Function true } @@ -79,10 +86,10 @@ fun LabFragment() { title = LocalString.showDebugLog, desc = LocalString.showDebugLogDesc, descColor = it, - isSwitch = ShamrockConfig.isDebug(ctx) + isSwitch = ShamrockConfig[ctx, DebugMode] ) { - ShamrockConfig.setDebug(ctx, it) - ShamrockConfig.pushUpdate(ctx) + ShamrockConfig[ctx, DebugMode] = it + InitHandler.update(ctx) return@Function true } } @@ -100,54 +107,13 @@ fun LabFragment() { thickness = 0.2.dp ) - Function( - title = "禁止无用进程", - desc = "禁止QQ生成无用进程浪费内存,可能造成部分功能闪退。", - descColor = color, - isSwitch = ShamrockConfig.isForbidUselessProcess(ctx) - ) { - ShamrockConfig.setForbidUselessProcess(ctx, it) - ShamrockConfig.pushUpdate(ctx) - return@Function true - } - Function( title = "自回复测试", desc = "发送[ping],机器人发送一个具有调试信息的返回。", descColor = color, - isSwitch = ShamrockConfig.enableAliveReply(ctx) + isSwitch = ShamrockConfig[ctx, AliveReply] ) { - ShamrockConfig.setAliveReply(ctx, it) - return@Function true - } - - Function( - title = "开启Shell接口", - desc = "可能导致设备被入侵,请勿随意开启。", - descColor = color, - isSwitch = ShamrockConfig.allowShell(ctx) - ) { - ShamrockConfig.setShellStatus(ctx, it) - return@Function true - } - - Function( - title = "自动唤醒QQ", - desc = "QQ进程死亡时重新打开QQ进程,前提本进程存活。", - descColor = color, - isSwitch = ShamrockConfig.enableAutoStart(ctx) - ) { - ShamrockConfig.setAutoStart(ctx, it) - return@Function true - } - - Function( - title = "禁止Shamrock同步设置", - desc = "禁止Shamrock同步设置,防止恢复手动修改后的配置文件。", - descColor = color, - isSwitch = ShamrockConfig.disableAutoSyncSetting(ctx) - ) { - ShamrockConfig.setDisableAutoSyncSetting(ctx, it) + ShamrockConfig[ctx, AliveReply] = it return@Function true } @@ -194,25 +160,14 @@ fun LabFragment() { thickness = 0.2.dp ) - Function( - title = LocalString.injectPacket, - desc = LocalString.injectPacketDesc, - descColor = color, - isSwitch = ShamrockConfig.isInjectPacket(ctx) - ) { - ShamrockConfig.setInjectPacket(ctx, it) - ShamrockConfig.pushUpdate(ctx) - return@Function true - } - Function( title = LocalString.antiTrace, desc = LocalString.antiTraceDesc, descColor = color, - isSwitch = ShamrockConfig.isAntiTrace(ctx) + isSwitch = ShamrockConfig[ctx, AntiJvmTrace] ) { - ShamrockConfig.setAntiTrace(ctx, it) - ShamrockConfig.pushUpdate(ctx) + ShamrockConfig[ctx, AntiJvmTrace] = it + scope.toast(ctx, LocalString.restartToast) return@Function true } @@ -277,21 +232,10 @@ fun LabFragment() { title = "自发消息推送", desc = "推送Bot发送的消息,未做特殊处理请勿打开。", descColor = it, - isSwitch = ShamrockConfig.enableSelfMsg(ctx) + isSwitch = ShamrockConfig[ctx, EnableSelfMessage] ) { - ShamrockConfig.setEnableSelfMsg(ctx, it) - ShamrockConfig.pushUpdate(ctx) - return@Function true - } - - Function( - title = "同步消息推送类型异换", - desc = "推送来自同号异设备消息,将同步消息作为自发消息推送。", - descColor = it, - isSwitch = ShamrockConfig.enableSyncMsgAsSentMsg(ctx) - ) { - ShamrockConfig.setEnableSyncMsgAsSentMsg(ctx, it) - ShamrockConfig.pushUpdate(ctx) + ShamrockConfig[ctx, EnableSelfMessage] = it + InitHandler.update(ctx) return@Function true } @@ -299,10 +243,10 @@ fun LabFragment() { title = "启用旧版资源上传系统", desc = "如果NT内核无法上传资源,请打开本开关。", descColor = it, - isSwitch = ShamrockConfig.enableOldBDH(ctx) + isSwitch = ShamrockConfig[ctx, EnableOldBDH] ) { - ShamrockConfig.setEnableOldBDH(ctx, it) - ShamrockConfig.pushUpdate(ctx) + ShamrockConfig[ctx, EnableOldBDH] = it + InitHandler.update(ctx) return@Function true } } diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/DashboardInitializer.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/DashboardInitializer.kt deleted file mode 100644 index ef906d6..0000000 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/DashboardInitializer.kt +++ /dev/null @@ -1,108 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) -package moe.fuqiuluo.shamrock.ui.service - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.core.content.ContextCompat.startActivity -import io.ktor.client.request.get -import io.ktor.client.request.parameter -import io.ktor.client.request.url -import io.ktor.client.statement.bodyAsText -import io.ktor.http.HttpStatusCode -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json -import moe.fuqiuluo.shamrock.remote.structures.CommonResult -import moe.fuqiuluo.shamrock.remote.structures.CurrentAccount -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.tools.GlobalClient -import moe.fuqiuluo.shamrock.ui.app.AppRuntime.AccountInfo -import moe.fuqiuluo.shamrock.ui.app.AppRuntime.log -import moe.fuqiuluo.shamrock.ui.app.AppRuntime.state -import moe.fuqiuluo.shamrock.ui.app.Level -import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig -import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule -import java.net.ConnectException -import java.util.Timer -import kotlin.concurrent.timer - -object DashboardInitializer { - private var servicePort: Int = 0 - private lateinit var heartbeatTimer: Timer - - operator fun invoke(context: Context, port: Int) { - servicePort = port - initHeartbeat(true, context) - } - - private fun initHeartbeat(reload: Boolean, context: Context) { - if (::heartbeatTimer.isInitialized && !reload) { - return - } - if (::heartbeatTimer.isInitialized) { - heartbeatTimer.cancel() - } - heartbeatTimer = timer("heartbeat", false, 0, 1000L * 30) { - checkService(context) - } - } - - private fun checkService(context: Context) { - GlobalScope.launch { - try { - GlobalClient.get { - url("http://127.0.0.1:$servicePort/get_account_info") - val token = ShamrockConfig.getToken(context) - if (token.isNotBlank()) { - //header("Authorization", "Bearer $token") - parameter("access_token", token) - } - }.let { - if (it.status == HttpStatusCode.OK) { - val result: CommonResult = Json.decodeFromString(it.bodyAsText()) - state.isFined.value = result.retcode == 0 - if (result.retcode == Status.InternalHandlerError.code) { - log("账号未登录。", Level.WARN) - } else if (result.retcode != 0) { - log("尝试从接口获取账号信息失败,未知错误。", Level.ERROR) - } else { - AccountInfo.let { account -> - account.uin.value = result.data.uin.toString() - account.nick.value = result.data.nick - } - } - } else { - state.isFined.value = false - log("尝试从接口获取账号信息失败,服务运行异常。", Level.ERROR) - } - } - } catch (e: ConnectException) { - state.isFined.value = false - context.broadcastToModule { - putExtra("__cmd", "checkAndStartService") - } - - if (ShamrockConfig.enableAutoStart(context)) { - log("检测到Service死亡,正在尝试重新启动!") - GlobalScope.launch(Dispatchers.Main) { - val packageName = "com.tencent.mobileqq" - val className = "com.tencent.mobileqq.activity.SplashActivity" - - val intent = Intent() - intent.component = ComponentName(packageName, className) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - intent.putExtra("from", "shamrock") - startActivity(context, intent, Bundle.EMPTY) - } - } - } catch (e: Throwable) { - state.isFined.value = false - log(e.stackTraceToString(), Level.ERROR) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/FetchPortHandler.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/FetchPortHandler.kt deleted file mode 100644 index 5b8f9a1..0000000 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/FetchPortHandler.kt +++ /dev/null @@ -1,15 +0,0 @@ -package moe.fuqiuluo.shamrock.ui.service.handlers - -import android.content.ContentValues -import android.content.Context -import moe.fuqiuluo.shamrock.ui.app.AppRuntime -import moe.fuqiuluo.shamrock.ui.service.DashboardInitializer - -object FetchPortHandler: ModuleHandler() { - override val cmd: String = "success" - - override fun onReceive(callbackId: Int, values: ContentValues, context: Context) { - AppRuntime.state.supportVoice.value = values.getAsBoolean("voice") - DashboardInitializer(context, values.getAsInteger("port")) - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt index 1147cb2..148b6c4 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt @@ -3,13 +3,32 @@ package moe.fuqiuluo.shamrock.ui.service.handlers import android.content.ContentValues import android.content.Context import moe.fuqiuluo.shamrock.ui.app.AppRuntime -import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig +import moe.fuqiuluo.shamrock.app.config.ShamrockConfig +import moe.fuqiuluo.shamrock.config.* internal object InitHandler: ModuleHandler() { override val cmd: String = "init" override fun onReceive(callbackId: Int, values: ContentValues, context: Context) { + update(context) + } + + fun update(context: Context) { AppRuntime.log("推送QQ进程初始化设置数据包成功...") - callback(context, callbackId, ShamrockConfig.getConfigMap(context)) + + val maps = hashMapOf() + + RPCPort.update(context, maps) + RPCAddress.update(context, maps) + ForceTablet.update(context, maps) + ActiveRPC.update(context, maps) + PassiveRPC.update(context, maps) + ResourceGroup.update(context, maps) + + callback(context, 1, maps) + } + + private inline fun ConfigKey.update(context: Context, map: HashMap) { + map[name()] = ShamrockConfig[context, this] } } \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/SwitchStatus.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/SwitchStatus.kt new file mode 100644 index 0000000..52b24f8 --- /dev/null +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/SwitchStatus.kt @@ -0,0 +1,49 @@ +package moe.fuqiuluo.shamrock.ui.service.handlers + +import android.content.ContentValues +import android.content.Context +import android.widget.Toast +import moe.fuqiuluo.shamrock.tools.GlobalUi +import moe.fuqiuluo.shamrock.ui.app.AppRuntime +import java.util.Timer +import kotlin.concurrent.timer +import kotlin.concurrent.timerTask + +object SwitchStatus: ModuleHandler() { + override val cmd: String + get() = "switch_status" + + private var lastActiveTime = 0L + private var timer: Timer? = null + + override fun onReceive(callbackId: Int, values: ContentValues, context: Context) { + val voiceSwitch = values.getAsBoolean("voice") + val nickname = values.getAsString("nickname") + val account = values.getAsString("account") + if (lastActiveTime == 0L) GlobalUi.post { + Toast.makeText(context, "激活成功", Toast.LENGTH_SHORT).show() + } + AppRuntime.state.apply { + isFined.value = true + coreVersion.value = values.getAsString("core_version") + coreName.value = "LSPosed" + supportVoice.value = voiceSwitch + } + AppRuntime.AccountInfo.apply { + uin.value = account + nick.value = nickname + } + lastActiveTime = System.currentTimeMillis() + startTimer() + } + + private fun startTimer() { + timer?.cancel() + timer = timer("SwitchStatus", true, 0, 5_000) { + if (lastActiveTime != 0L && System.currentTimeMillis() - lastActiveTime > 30 * 1000) { + AppRuntime.state.isFined.value = false + lastActiveTime = 0 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/internal/MultifunctionalProvider.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/internal/MultifunctionalProvider.kt index bb1aba1..4ccc1e8 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/internal/MultifunctionalProvider.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/internal/MultifunctionalProvider.kt @@ -5,9 +5,9 @@ import android.content.ContentValues import android.content.Context import android.content.Intent import android.database.Cursor -import android.net.Uri import moe.fuqiuluo.shamrock.ui.service.ModuleTalker import moe.fuqiuluo.shamrock.ui.service.handlers.* +import android.net.Uri class MultifunctionalProvider: ContentProvider() { override fun insert(uri: Uri, content: ContentValues?): Uri { @@ -28,8 +28,8 @@ class MultifunctionalProvider: ContentProvider() { override fun onCreate(): Boolean { ModuleTalker.register(InitHandler) - ModuleTalker.register(FetchPortHandler) ModuleTalker.register(LogHandler) + ModuleTalker.register(SwitchStatus) return true } @@ -58,7 +58,7 @@ class MultifunctionalProvider: ContentProvider() { inline fun Context.broadcastToModule(intentBuilder: Intent.() -> Unit) { val intent = Intent() - intent.action = "moe.fuqiuluo.xqbot.dynamic" + intent.action = "moe.fuqiuluo.kritor.dynamic" intent.intentBuilder() sendBroadcast(intent) } \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/tools/tabs.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/tools/tabs.kt index ec54545..effdf1d 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/tools/tabs.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/tools/tabs.kt @@ -1,23 +1,35 @@ package moe.fuqiuluo.shamrock.ui.tools +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColor +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.TweenSpec import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.foundation.Indication import androidx.compose.foundation.background +import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.selectable import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Text import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -31,8 +43,10 @@ import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp @@ -135,20 +149,18 @@ private fun TabBaselineLayout( text: @Composable (() -> Unit)?, icon: @Composable (() -> Unit)? ) { - Layout( - { - if (text != null) { - Box( - Modifier - .layoutId("text") - .padding(horizontal = HorizontalTextPadding) - ) { text() } - } - if (icon != null) { - Box(Modifier.layoutId("icon")) { icon() } - } + Layout({ + if (text != null) { + Box( + Modifier + .layoutId("text") + .padding(horizontal = HorizontalTextPadding) + ) { text() } } - ) { measurables, constraints -> + if (icon != null) { + Box(Modifier.layoutId("icon")) { icon() } + } + }) { measurables, constraints -> val textPlaceable = text?.let { measurables.first { it.layoutId == "text" }.measure( // Measure with loose constraints for height as we don't want the text to take up more @@ -247,21 +259,66 @@ fun ShamrockTab( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - text: @Composable (() -> Unit)? = null, - icon: @Composable (() -> Unit)? = null, selectedContentColor: Color = GlobalColor.TabSelected, unselectedContentColor: Color = selectedContentColor, indication: Indication? = rememberRipple(bounded = true, color = selectedContentColor), - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + titleWithIcon: Pair, + visibleState: MutableTransitionState ) { - val styledText: @Composable (() -> Unit)? = text?.let { - @Composable { - val style = - MaterialTheme.typography.fromToken(PrimaryNavigationTabTokens.LabelTextFont) - .copy(textAlign = TextAlign.Center) - ProvideTextStyle(style, content = text) + var text: @Composable (() -> Unit)? = null + var icon: @Composable (() -> Unit)? = null + + if (!selected) { + icon = { + Icon( + painter = painterResource(id = titleWithIcon.second), + contentDescription = titleWithIcon.first, + tint = Color.Unspecified, + modifier = Modifier + .height(24.dp) + .width(24.dp) + .padding(bottom = 5.dp) + .indication( + remember { MutableInteractionSource() }, + rememberRipple(color = Color.Transparent) + ) + ) + } + } else { + text = { + val style = MaterialTheme.typography + .fromToken(PrimaryNavigationTabTokens.LabelTextFont) + .copy(textAlign = TextAlign.Center) + + ProvideTextStyle(style) { + AnimatedVisibility( + visibleState = visibleState, + enter = remember { + scaleIn(animationSpec = TweenSpec(150, easing = FastOutLinearInEasing)) + }, + exit = remember { + scaleOut(animationSpec = TweenSpec(150, easing = FastOutSlowInEasing)) + }, + modifier = Modifier + ) { + Text( + text = titleWithIcon.first, + color = GlobalColor.TabItem, + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(bottom = 5.dp) + .indication( + remember { MutableInteractionSource() }, + rememberRipple(color = Color.Transparent) + ) + ) + } + } } } + ShamrockTab( selected, onClick, @@ -272,7 +329,10 @@ fun ShamrockTab( interactionSource, indication ) { - TabBaselineLayout(icon = icon, text = styledText) + TabBaselineLayout( + icon = icon, + text = text + ) } } diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/impl/OneBotHandlerProcessor.kt b/processor/src/main/java/moe/fuqiuluo/ksp/impl/OneBotHandlerProcessor.kt deleted file mode 100644 index ee95e5f..0000000 --- a/processor/src/main/java/moe/fuqiuluo/ksp/impl/OneBotHandlerProcessor.kt +++ /dev/null @@ -1,85 +0,0 @@ -@file:OptIn(KspExperimental::class) -@file:Suppress("LocalVariableName", "UNCHECKED_CAST") - -package moe.fuqiuluo.ksp.impl - -import com.google.devtools.ksp.KspExperimental -import com.google.devtools.ksp.getAnnotationsByType -import com.google.devtools.ksp.getClassDeclarationByName -import com.google.devtools.ksp.getKotlinClassByName -import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Dependencies -import com.google.devtools.ksp.processing.KSPLogger -import com.google.devtools.ksp.processing.Resolver -import com.google.devtools.ksp.processing.SymbolProcessor -import com.google.devtools.ksp.symbol.ClassKind -import com.google.devtools.ksp.symbol.KSAnnotated -import com.google.devtools.ksp.symbol.KSClassDeclaration -import com.google.devtools.ksp.symbol.KSVisitorVoid -import com.google.devtools.ksp.validate -import com.squareup.kotlinpoet.FileSpec -import com.squareup.kotlinpoet.FunSpec -import moe.fuqiuluo.symbols.OneBotHandler - -class OneBotHandlerProcessor( - private val codeGenerator: CodeGenerator, - private val logger: KSPLogger -): SymbolProcessor { - override fun process(resolver: Resolver): List { - val ActionManagerNode = resolver.getClassDeclarationByName("moe.fuqiuluo.shamrock.remote.action.ActionManager") - ?: resolver.getKotlinClassByName("moe.fuqiuluo.shamrock.remote.action.ActionManager") - ?: resolver.getClassDeclarationByName("ActionManager") - val symbols = resolver.getSymbolsWithAnnotation(OneBotHandler::class.qualifiedName!!) - val unableToProcess = symbols.filterNot { it.validate() } - if (ActionManagerNode != null) { - val oneBotHandlers = (symbols.filter { - it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.OBJECT - } as Sequence).toList() - - if (oneBotHandlers.isNotEmpty()) { - ActionManagerNode.accept(ActionManagerVisitor(oneBotHandlers), Unit) - } - } - - return unableToProcess.toList() - } - - inner class ActionManagerVisitor( - private val actionHandlers: List - ): KSVisitorVoid() { - override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { - val packageName = classDeclaration.packageName.asString() - - // generate kotlin `init { }` - val fileSpec = FileSpec.builder(packageName, classDeclaration.qualifiedName?.asString() ?: run { - throw IllegalStateException("ActionManagerVisitor: classDeclaration.qualifiedName is null") - }).addFunction(FunSpec.builder("initManager").apply { - actionHandlers.forEach { handler -> - // fetch the params of the annotation - val annotation = handler.getAnnotationsByType(OneBotHandler::class).first() - val actionName = annotation.actionName - val alias = annotation.alias - alias.forEach { name -> - addStatement("actionMap[\"$name\"] = ${handler.simpleName.asString()}") - } - addStatement("actionMap[\"$actionName\"] = ${handler.simpleName.asString()}") - } - }.build()).apply { - addImport("moe.fuqiuluo.shamrock.remote.action.ActionManager", "actionMap") - actionHandlers.forEach { - addImport(it.packageName.asString(), it.simpleName.asString()) - } - }.build() - - codeGenerator.createNewFile( - dependencies = Dependencies(aggregating = false), - packageName = packageName, - fileName = "Auto" + classDeclaration.simpleName.asString() - ).use { outputStream -> - outputStream.writer().use { - fileSpec.writeTo(it) - } - } - } - } -} \ No newline at end of file diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/impl/ProtobufProcessor.kt b/processor/src/main/java/moe/fuqiuluo/ksp/impl/ProtobufProcessor.kt index 7cf97dc..db503a3 100644 --- a/processor/src/main/java/moe/fuqiuluo/ksp/impl/ProtobufProcessor.kt +++ b/processor/src/main/java/moe/fuqiuluo/ksp/impl/ProtobufProcessor.kt @@ -32,7 +32,7 @@ class ProtobufProcessor( }.toList() if (actions.isNotEmpty()) { - actions.forEachIndexed { index, clz -> + actions.forEachIndexed { _, clz -> if (clz.isInternal()) return@forEachIndexed if (clz.isPrivate()) return@forEachIndexed diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/impl/XposedHookProcessor.kt b/processor/src/main/java/moe/fuqiuluo/ksp/impl/XposedHookProcessor.kt index cea2db4..201d89e 100644 --- a/processor/src/main/java/moe/fuqiuluo/ksp/impl/XposedHookProcessor.kt +++ b/processor/src/main/java/moe/fuqiuluo/ksp/impl/XposedHookProcessor.kt @@ -1,5 +1,5 @@ @file:Suppress("UNCHECKED_CAST", "LocalVariableName", "PrivatePropertyName") -@file:OptIn(KspExperimental::class) +@file:OptIn(KspExperimental::class, KspExperimental::class) package moe.fuqiuluo.ksp.impl @@ -27,10 +27,14 @@ class XposedHookProcessor( private val logger: KSPLogger ): SymbolProcessor { override fun process(resolver: Resolver): List { - val symbols = resolver.getSymbolsWithAnnotation(XposedHook::class.qualifiedName!!) + val symbols = resolver.getSymbolsWithAnnotation( + annotationName = XposedHook::class.qualifiedName!!, + inDepth = true + ) + logger.warn("Found ${symbols.count()} classes annotated with XposedHook") val unableToProcess = symbols.filterNot { it.validate() } val actions = (symbols.filter { - it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.CLASS + it is KSClassDeclaration && it.classKind == ClassKind.CLASS } as Sequence).toList() if (actions.isNotEmpty()) { @@ -46,7 +50,7 @@ class XposedHookProcessor( } val context = ClassName("android.content", "Context") - val packageName = "moe.fuqiuluo.shamrock.xposed.hooks" + val packageName = "moe.fuqiuluo.shamrock.xposed.actions" val fileSpec = FileSpec.builder(packageName, "AutoActionLoader").addFunction(FunSpec.builder("runFirstActions") .addParameter("ctx", context) .apply { @@ -96,16 +100,6 @@ class XposedHookProcessor( } } } - return unableToProcess.toList() } - - inner class ActionLoaderVisitor( - private val firstActions: List, - private val serviceActions: List, - ): KSVisitorVoid() { - override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { - - } - } } \ No newline at end of file diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/providers/OneBotHandlerProcessorProvider.kt b/processor/src/main/java/moe/fuqiuluo/ksp/providers/OneBotHandlerProcessorProvider.kt deleted file mode 100644 index 102d546..0000000 --- a/processor/src/main/java/moe/fuqiuluo/ksp/providers/OneBotHandlerProcessorProvider.kt +++ /dev/null @@ -1,17 +0,0 @@ -package moe.fuqiuluo.ksp.providers - -import com.google.auto.service.AutoService -import com.google.devtools.ksp.processing.SymbolProcessor -import com.google.devtools.ksp.processing.SymbolProcessorEnvironment -import com.google.devtools.ksp.processing.SymbolProcessorProvider -import moe.fuqiuluo.ksp.impl.OneBotHandlerProcessor - -@AutoService(SymbolProcessorProvider::class) -class OneBotHandlerProcessorProvider: SymbolProcessorProvider { - override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { - return OneBotHandlerProcessor( - environment.codeGenerator, - environment.logger - ) - } -} \ No newline at end of file diff --git a/protobuf/build.gradle.kts b/protobuf/build.gradle.kts index 06cedfb..4125f0b 100644 --- a/protobuf/build.gradle.kts +++ b/protobuf/build.gradle.kts @@ -47,5 +47,5 @@ dependencies { } tasks.withType().configureEach { - kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } \ No newline at end of file diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index e517625..8c5c967 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -72,9 +72,7 @@ dependencies { DEPENDENCY_ANDROIDX.forEach { implementation(it) } - implementation(DEPENDENCY_JAVA_WEBSOCKET) implementation(DEPENDENCY_PROTOBUF) - implementation(DEPENDENCY_JSON5K) implementation(room("runtime")) kapt(room("compiler")) @@ -83,11 +81,6 @@ dependencies { implementation(kotlinx("io-jvm", "0.1.16")) implementation(kotlinx("serialization-protobuf", "1.6.2")) - implementation(ktor("server", "core")) - implementation(ktor("server", "host-common")) - implementation(ktor("server", "status-pages")) - implementation(ktor("server", "netty")) - implementation(ktor("server", "content-negotiation")) implementation(ktor("client", "core")) implementation(ktor("client", "content-negotiation")) implementation(ktor("client", "cio")) @@ -100,7 +93,3 @@ dependencies { androidTestImplementation(platform("androidx.compose:compose-bom:2023.06.01")) androidTestImplementation("androidx.compose.ui:ui-test-junit4") } - -tasks.withType().configureEach { - kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" -} \ No newline at end of file diff --git a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteData.aidl b/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteData.aidl deleted file mode 100644 index 7f1d4c2..0000000 --- a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteData.aidl +++ /dev/null @@ -1,8 +0,0 @@ -// IByteData.aidl -package moe.fuqiuluo.shamrock.xposed.ipc.bytedata; - -import moe.fuqiuluo.shamrock.xposed.ipc.bytedata.IByteDataSign; - -interface IByteData { - IByteDataSign sign(String uin, String data, in byte[] salt); -} \ No newline at end of file diff --git a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteDataSign.aidl b/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteDataSign.aidl deleted file mode 100644 index b31eec0..0000000 --- a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteDataSign.aidl +++ /dev/null @@ -1,4 +0,0 @@ -// IByteDataSign.aidl -package moe.fuqiuluo.shamrock.xposed.ipc.bytedata; - -parcelable IByteDataSign; \ No newline at end of file diff --git a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSign.aidl b/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSign.aidl deleted file mode 100644 index d408374..0000000 --- a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSign.aidl +++ /dev/null @@ -1,4 +0,0 @@ -// IQSign.aidl -package moe.fuqiuluo.shamrock.xposed.ipc.qsign; - -parcelable IQSign; \ No newline at end of file diff --git a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSigner.aidl b/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSigner.aidl deleted file mode 100644 index 6007e73..0000000 --- a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSigner.aidl +++ /dev/null @@ -1,14 +0,0 @@ -// IQSigner.aidl -package moe.fuqiuluo.shamrock.xposed.ipc.qsign; - -import moe.fuqiuluo.shamrock.xposed.ipc.qsign.IQSign; - -interface IQSigner { - IQSign sign(String cmd, int seq, String uin, in byte[] buffer); - - byte[] energy(String module, in byte[] salt); - - byte[] xwDebugId(String uin, String start, String end); - - List getCmdWhiteList(); -} \ No newline at end of file diff --git a/xposed/src/main/cpp/clover.cpp b/xposed/src/main/cpp/clover.cpp index 7eb50fa..2604690 100644 --- a/xposed/src/main/cpp/clover.cpp +++ b/xposed/src/main/cpp/clover.cpp @@ -173,7 +173,7 @@ NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) { extern "C" JNIEXPORT jboolean JNICALL -Java_moe_fuqiuluo_shamrock_xposed_hooks_AntiDetection_antiNativeDetections(JNIEnv *env, +Java_moe_fuqiuluo_shamrock_xposed_actions_AntiDetection_antiNativeDetections(JNIEnv *env, jobject thiz) { if (hook_function == nullptr) return false; hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get); diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt deleted file mode 100644 index d66c70d..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt +++ /dev/null @@ -1,201 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.qqinterface.servlet - -import android.os.Bundle -import com.tencent.mobileqq.app.QQAppInterface -import com.tencent.mobileqq.msf.core.MsfCore -import com.tencent.mobileqq.pb.ByteStringMicro -import com.tencent.qphone.base.remote.ToServiceMsg -import io.ktor.utils.io.core.BytePacketBuilder -import io.ktor.utils.io.core.readBytes -import io.ktor.utils.io.core.writeFully -import io.ktor.utils.io.core.writeInt -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.encodeToByteArray - -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.utils.DeflateTools -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver -import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest -import protobuf.oidb.TrpcOidb -import mqq.app.MobileQQ -import protobuf.auto.toByteArray -import tencent.im.oidb.oidb_sso -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.resume - -internal abstract class BaseSvc { - companion object Default: CoroutineScope { - val currentUin: String - get() = app.currentAccountUin - - val app: QQAppInterface - get() = AppRuntimeFetcher.appRuntime as QQAppInterface - - fun createToServiceMsg(cmd: String): ToServiceMsg { - return ToServiceMsg("mobileqq.service", app.currentAccountUin, cmd) - } - - suspend fun sendOidbAW(cmd: String, cmdId: Int, serviceId: Int, data: ByteArray, trpc: Boolean = false, timeout: Long = 5000L): ByteArray? { - val seq = MsfCore.getNextSeq() - val buffer = withTimeoutOrNull(timeout) { - suspendCancellableCoroutine { continuation -> - launch(Dispatchers.Default) { - DynamicReceiver.register(IPCRequest(cmd, seq) { - val buffer = it.getByteArrayExtra("buffer")!! - continuation.resume(buffer) - }) - } - if (trpc) sendTrpcOidb(cmd, cmdId, serviceId, data, seq) - else sendOidb(cmd, cmdId, serviceId, data, seq) - } - }.also { - if (it == null) - DynamicReceiver.unregister(seq) - }?.copyOf() - try { - if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) { - val builder = BytePacketBuilder() - val deBuffer = DeflateTools.uncompress(buffer.slice(4)) - builder.writeInt(deBuffer.size) - builder.writeFully(deBuffer) - return builder.build().readBytes() - } - } catch (_: Exception) { } - return buffer - } - - suspend fun sendBufferAW(cmd: String, isPb: Boolean, data: ByteArray, timeout: Long = 5000L): ByteArray? { - val seq = MsfCore.getNextSeq() - val buffer = withTimeoutOrNull(timeout) { - suspendCancellableCoroutine { continuation -> - launch(Dispatchers.Default) { - DynamicReceiver.register(IPCRequest(cmd, seq) { - val buffer = it.getByteArrayExtra("buffer")!! - continuation.resume(buffer) - }) - sendBuffer(cmd, isPb, data, seq) - } - } - }.also { - if (it == null) - DynamicReceiver.unregister(seq) - }?.copyOf() - try { - if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) { - val builder = BytePacketBuilder() - val deBuffer = DeflateTools.uncompress(buffer.slice(4)) - builder.writeInt(deBuffer.size) - builder.writeFully(deBuffer) - return builder.build().readBytes() - } - } catch (_: Exception) { } - return buffer - } - - fun sendOidb(cmd: String, cmdId: Int, serviceId: Int, buffer: ByteArray, seq: Int = -1, trpc: Boolean = false) { - if (trpc) { - sendTrpcOidb(cmd, cmdId, serviceId, buffer, seq) - return - } - val to = createToServiceMsg(cmd) - val oidb = oidb_sso.OIDBSSOPkg() - oidb.uint32_command.set(cmdId) - oidb.uint32_service_type.set(serviceId) - oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(buffer)) - oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext())) - to.putWupBuffer(oidb.toByteArray()) - to.addAttribute("req_pb_protocol_flag", true) - if (seq != -1) { - to.addAttribute("shamrock_seq", seq) - } - app.sendToService(to) - } - - fun sendTrpcOidb(cmd: String, cmdId: Int, serviceId: Int, buffer: ByteArray, seq: Int = -1) { - val to = createToServiceMsg(cmd) - - val oidb = TrpcOidb( - cmd = cmdId, - service = serviceId, - buffer = buffer, - flag = 1 - ) - to.putWupBuffer(oidb.toByteArray()) - - to.addAttribute("req_pb_protocol_flag", true) - if (seq != -1) { - to.addAttribute("shamrock_seq", seq) - } - app.sendToService(to) - } - - fun sendBuffer(cmd: String, isPb: Boolean, buffer: ByteArray, seq: Int = MsfCore.getNextSeq()) { - val toServiceMsg = ToServiceMsg("mobileqq.service", app.currentUin, cmd) - toServiceMsg.putWupBuffer(buffer) - toServiceMsg.addAttribute("req_pb_protocol_flag", isPb) - toServiceMsg.addAttribute("shamrock_seq", seq) - app.sendToService(toServiceMsg) - } - - @OptIn(ExperimentalCoroutinesApi::class) - override val coroutineContext: CoroutineContext by lazy { - Dispatchers.IO.limitedParallelism(12) - } - } - - protected fun send(toServiceMsg: ToServiceMsg) { - app.sendToService(toServiceMsg) - } - - protected suspend fun sendAW(toServiceMsg: ToServiceMsg, timeout: Long = 5000L): ByteArray? { - val seq = MsfCore.getNextSeq() - val buffer = withTimeoutOrNull(timeout) { - suspendCancellableCoroutine { continuation -> - launch(Dispatchers.Default) { - DynamicReceiver.register(IPCRequest(toServiceMsg.serviceCmd, seq) { - val buffer = it.getByteArrayExtra("buffer")!! - continuation.resume(buffer) - }) - toServiceMsg.addAttribute("shamrock_seq", seq) - send(toServiceMsg) - } - } - }.also { - if (it == null) DynamicReceiver.unregister(seq) - }?.copyOf() - try { - if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) { - val builder = BytePacketBuilder() - val deBuffer = DeflateTools.uncompress(buffer.slice(4)) - builder.writeInt(deBuffer.size) - builder.writeFully(deBuffer) - return builder.build().readBytes() - } - } catch (_: Exception) { } - return buffer - } - - protected fun sendExtra(cmd: String, builder: (Bundle) -> Unit) { - val toServiceMsg = createToServiceMsg(cmd) - builder(toServiceMsg.extraData) - app.sendToService(toServiceMsg) - } - - protected fun sendPb(cmd: String, buffer: ByteArray, seq: Int) { - val toServiceMsg = createToServiceMsg(cmd) - toServiceMsg.putWupBuffer(buffer) - toServiceMsg.addAttribute("req_pb_protocol_flag", true) - toServiceMsg.addAttribute("shamrock_seq", seq) - app.sendToService(toServiceMsg) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt deleted file mode 100644 index a113b74..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt +++ /dev/null @@ -1,135 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet - -import VIP.GetCustomOnlineStatusReq -import VIP.GetCustomOnlineStatusRsp -import com.qq.jce.wup.UniPacket -import com.tencent.mobileqq.data.Card -import com.tencent.mobileqq.profilecard.api.IProfileDataService -import com.tencent.mobileqq.profilecard.api.IProfileProtocolService -import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver -import io.ktor.client.request.header -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType.Application.Json -import io.ktor.http.contentType -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import moe.fuqiuluo.shamrock.tools.GlobalClient -import moe.fuqiuluo.shamrock.tools.json -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import mqq.app.Packet -import tencent.im.oidb.cmd0x11b2.oidb_0x11b2 -import tencent.im.oidb.oidb_sso -import kotlin.coroutines.resume - -internal object CardSvc: BaseSvc() { - private val GetModelShowLock by lazy { - Mutex() - } - private val refreshCardLock by lazy { - Mutex() - } - - suspend fun getModelShow(uin: Long = app.longAccountUin): String { - return GetModelShowLock.withLock { - val uniPacket = UniPacket() - uniPacket.servantName = "VIP.CustomOnlineStatusServer.CustomOnlineStatusObj" - uniPacket.funcName = "GetCustomOnlineStatus" - val getCustomOnlineStatusReq = GetCustomOnlineStatusReq() - getCustomOnlineStatusReq.lUin = uin - getCustomOnlineStatusReq.sIMei = "" - uniPacket.put("req", getCustomOnlineStatusReq) - - val resp = sendBufferAW("VipCustom.GetCustomOnlineStatus", false, uniPacket.encode()) - ?: error("unable to fetch contact model_show") - Packet.decodePacket(resp, "rsp", GetCustomOnlineStatusRsp()).sBuffer - } - } - - suspend fun setModelShow(model: String, manu: String, modelShow: String, imei: String, show: Boolean) { - val cookie = TicketSvc.getCookie("vip.qq.com") - val csrf = TicketSvc.getCSRF(TicketSvc.getUin(), "vip.qq.com") - val p4token = TicketSvc.getPt4Token(TicketSvc.getUin(), "vip.qq.com") ?: "" - GlobalClient.post("https://club.vip.qq.com/srf-cgi-node?srfname=VIP.CustomOnlineStatusServer.CustomOnlineStatusObj.SetCustomOnlineStatus&ts=${System.currentTimeMillis()}&daid=18&g_tk=$csrf&pt4_token=$p4token") { - header("Cookie", cookie) - contentType(Json) - setBody(mapOf( - "servicesName" to "VIP.CustomOnlineStatusServer.CustomOnlineStatusObj", - "cmd" to "SetCustomOnlineStatus", - "args" to listOf(mapOf( - "sIMei" to imei, - "sModel" to model, - "sManu" to manu, - "lUin" to app.currentUin.toLong(), - "bShowInfo" to show, - "sModelShow" to modelShow - )) - ).json.toString()) - }.bodyAsText().let { - LogCenter.log({ "setModelShow() => $it" }, Level.DEBUG) - } - } - - suspend fun getSharePrivateArkMsg(peerId: Long): String { - val reqBody = oidb_0x11b2.BusinessCardV3Req() - reqBody.uin.set(peerId) - reqBody.jump_url.set("mqqapi://card/show_pslcard?src_type=internal&source=sharecard&version=1&uin=$peerId") - - val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11ca_0", 4790, 0, reqBody.toByteArray()) - ?: error("unable to fetch contact ark_json_text") - val body = oidb_sso.OIDBSSOPkg() - body.mergeFrom(buffer.slice(4)) - val rsp = oidb_0x11b2.BusinessCardV3Rsp() - rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray()) - return rsp.signed_ark_msg.get() - } - - suspend fun getProfileCard(uin: Long): Result { - return getProfileCardFromCache(uin).onFailure { - return refreshAndGetProfileCard(uin) - } - } - - fun getProfileCardFromCache(uin: Long): Result { - val profileDataService = app - .getRuntimeService(IProfileDataService::class.java, "all") - val card = profileDataService.getProfileCard(uin.toString(), true) - return if (card == null || card.strNick.isNullOrEmpty()) { - Result.failure(Exception("unable to fetch profile card")) - } else { - Result.success(card) - } - } - - suspend fun refreshAndGetProfileCard(uin: Long): Result { - val dataService = app - .getRuntimeService(IProfileDataService::class.java, "all") - val card = refreshCardLock.withLock { - suspendCancellableCoroutine { - app.addObserver(object: ProfileCardObserver() { - override fun onGetProfileCard(success: Boolean, obj: Any) { - app.removeObserver(this) - if (!success || obj !is Card) { - it.resume(null) - } else { - dataService.saveProfileCard(obj) - it.resume(obj) - } - } - }) - app.getRuntimeService(IProfileProtocolService::class.java, "all") - .requestProfileCard(app.currentUin, uin.toString(), 12, 0L, 0.toByte(), 0L, 0L, null, "", 0L, 10004, null, 0.toByte()) - } - } - return if (card == null || card.strNick.isNullOrEmpty()) { - Result.failure(Exception("unable to fetch profile card")) - } else { - Result.success(card) - } - } - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ChatSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ChatSvc.kt deleted file mode 100644 index b839ef9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ChatSvc.kt +++ /dev/null @@ -1,20 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet - -import kotlinx.serialization.encodeToByteArray -import protobuf.auto.toByteArray - -import protobuf.oidb.cmd0x9082.Oidb0x9082 - -internal object ChatSvc: BaseSvc() { - fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) { - val serviceId = if (isSet) 1 else 2 - sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082( - peer = peer.toULong(), - msgSeq = msgSeq, - faceIndex = faceIndex, - flag = 1u, - u1 = 0u, - u2 = 0u - ).toByteArray()) - } -} diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FileSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FileSvc.kt deleted file mode 100644 index dfebd80..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FileSvc.kt +++ /dev/null @@ -1,248 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet - -import com.tencent.mobileqq.pb.ByteStringMicro -import moe.fuqiuluo.qqinterface.servlet.structures.* -import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.tools.toHexString -import moe.fuqiuluo.shamrock.utils.DeflateTools -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.oidb.cmd0x6d7.CreateFolderReq -import protobuf.oidb.cmd0x6d7.DeleteFolderReq -import protobuf.oidb.cmd0x6d7.MoveFolderReq -import protobuf.oidb.cmd0x6d7.Oidb0x6d7ReqBody -import protobuf.oidb.cmd0x6d7.Oidb0x6d7RespBody -import protobuf.oidb.cmd0x6d7.RenameFolderReq -import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 -import tencent.im.oidb.cmd0x6d8.oidb_0x6d8 -import tencent.im.oidb.oidb_sso -import protobuf.group_file_common.FolderInfo as GroupFileCommonFolderInfo -import protobuf.auto.toByteArray - -internal object FileSvc: BaseSvc() { - suspend fun createFileFolder(groupId: Long, folderName: String, parentFolderId: String = "/"): Result { - val data = Oidb0x6d7ReqBody( - createFolder = CreateFolderReq( - groupCode = groupId.toULong(), - appId = 3u, - parentFolderId = parentFolderId, - folderName = folderName - ) - ).toByteArray() - val resultBuffer = sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data) - ?: return Result.failure(Exception("unable to fetch result")) - val oidbPkg = oidb_sso.OIDBSSOPkg() - oidbPkg.mergeFrom(resultBuffer.slice(4)) - val rsp = oidbPkg.bytes_bodybuffer.get() - .toByteArray() - .decodeProtobuf() - if (rsp.createFolder?.retCode != 0) { - return Result.failure(Exception("unable to create folder: ${rsp.createFolder?.retCode}")) - } - return Result.success(rsp.createFolder!!.folderInfo!!) - } - - suspend fun deleteGroupFolder(groupId: Long, folderUid: String): Boolean { - val buffer = sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody( - deleteFolder = DeleteFolderReq( - groupCode = groupId.toULong(), - appId = 3u, - folderId = folderUid - ) - ).toByteArray()) ?: return false - val oidbPkg = oidb_sso.OIDBSSOPkg() - oidbPkg.mergeFrom(buffer.slice(4)) - val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf() - return rsp.deleteFolder?.retCode == 0 - } - - suspend fun moveGroupFolder(groupId: Long, folderUid: String, newParentFolderUid: String): Boolean { - val buffer = sendOidbAW("OidbSvc.0x6d7_2", 1751, 2, Oidb0x6d7ReqBody( - moveFolder = MoveFolderReq( - groupCode = groupId.toULong(), - appId = 3u, - folderId = folderUid, - parentFolderId = "/" - ) - ).toByteArray()) ?: return false - val oidbPkg = oidb_sso.OIDBSSOPkg() - oidbPkg.mergeFrom(buffer.slice(4)) - val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf() - return rsp.moveFolder?.retCode == 0 - } - - suspend fun renameFolder(groupId: Long, folderUid: String, name: String): Boolean { - val buffer = sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody( - renameFolder = RenameFolderReq( - groupCode = groupId.toULong(), - appId = 3u, - folderId = folderUid, - folderName = name - ) - ).toByteArray()) ?: return false - val oidbPkg = oidb_sso.OIDBSSOPkg() - oidbPkg.mergeFrom(buffer.slice(4)) - val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf() - return rsp.renameFolder?.retCode == 0 - } - - suspend fun deleteGroupFile(groupId: Long, bizId: Int, fileUid: String): Boolean { - val oidb0x6d6ReqBody = oidb_0x6d6.ReqBody().apply { - delete_file_req.set(oidb_0x6d6.DeleteFileReqBody().apply { - uint64_group_code.set(groupId) - uint32_app_id.set(3) - uint32_bus_id.set(bizId) - str_parent_folder_id.set("/") - str_file_id.set(fileUid) - }) - } - val result = sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray()) - ?: return false - val oidbPkg = oidb_sso.OIDBSSOPkg() - oidbPkg.mergeFrom(result.slice(4)) - val rsp = oidb_0x6d6.RspBody().apply { - mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray()) - } - return rsp.delete_file_rsp.int32_ret_code.get() == 0 - } - - suspend fun getGroupFileSystemInfo(groupId: Long): FileSystemInfo { - val rspGetFileCntBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 2, oidb_0x6d8.ReqBody().also { - it.group_file_cnt_req.set(oidb_0x6d8.GetFileCountReqBody().also { - it.uint64_group_code.set(groupId) - it.uint32_app_id.set(3) - it.uint32_bus_id.set(0) - }) - }.toByteArray()) - val fileCnt: Int - val limitCnt: Int - if (rspGetFileCntBuffer != null) { - oidb_0x6d8.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg() - .mergeFrom(rspGetFileCntBuffer.slice(4)) - .bytes_bodybuffer.get() - .toByteArray() - ).group_file_cnt_rsp.apply { - fileCnt = uint32_all_file_count.get() - limitCnt = uint32_limit_count.get() - } - } else { - throw RuntimeException("获取群文件数量失败") - } - - val rspGetFileSpaceBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 3, oidb_0x6d8.ReqBody().also { - it.group_space_req.set(oidb_0x6d8.GetSpaceReqBody().apply { - uint64_group_code.set(groupId) - uint32_app_id.set(3) - }) - }.toByteArray()) - val totalSpace: Long - val usedSpace: Long - if (rspGetFileSpaceBuffer != null) { - oidb_0x6d8.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg() - .mergeFrom(rspGetFileSpaceBuffer.slice(4)) - .bytes_bodybuffer.get() - .toByteArray()).group_space_rsp.apply { - totalSpace = uint64_total_space.get() - usedSpace = uint64_used_space.get() - } - } else { - throw RuntimeException("获取群文件空间失败") - } - - return FileSystemInfo( - fileCnt, limitCnt, usedSpace, totalSpace - ) - } - - suspend fun getGroupRootFiles(groupId: Long): Result { - return getGroupFiles(groupId, "/") - } - - suspend fun getGroupFileInfo(groupId: Long, fileId: String, busid: Int): FileUrl { - return FileUrl(RichProtoSvc.getGroupFileDownUrl(groupId, fileId, busid)) - } - - suspend fun getGroupFiles(groupId: Long, folderId: String): Result { - val fileSystemInfo = getGroupFileSystemInfo(groupId) - val rspGetFileListBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also { - it.file_list_info_req.set(oidb_0x6d8.GetFileListReqBody().apply { - uint64_group_code.set(groupId) - uint32_app_id.set(3) - str_folder_id.set(folderId) - - uint32_file_count.set(fileSystemInfo.fileCount) - uint32_all_file_count.set(0) - uint32_req_from.set(3) - uint32_sort_by.set(oidb_0x6d8.GetFileListReqBody.SORT_BY_TIMESTAMP) - - uint32_filter_code.set(0) - uint64_uin.set(0) - - uint32_start_index.set(0) - - bytes_context.set(ByteStringMicro.copyFrom(EMPTY_BYTE_ARRAY)) - - uint32_show_onlinedoc_folder.set(0) - }) - }.toByteArray(), timeout = 15_000L) - - return kotlin.runCatching { - val files = arrayListOf() - val dirs = arrayListOf() - if (rspGetFileListBuffer != null) { - val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(rspGetFileListBuffer.slice(4).let { - if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it - }) - - oidb_0x6d8.RspBody().mergeFrom(oidb.bytes_bodybuffer.get().toByteArray()) - .file_list_info_rsp.apply { - rpt_item_list.get().forEach { file -> - if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) { - val fileInfo = file.file_info - files.add(FileInfo( - groupId = groupId, - fileId = fileInfo.str_file_id.get(), - fileName = fileInfo.str_file_name.get(), - fileSize = fileInfo.uint64_file_size.get(), - busid = fileInfo.uint32_bus_id.get(), - uploadTime = fileInfo.uint32_upload_time.get(), - deadTime = fileInfo.uint32_dead_time.get(), - modifyTime = fileInfo.uint32_modify_time.get(), - downloadTimes = fileInfo.uint32_download_times.get(), - uploadUin = fileInfo.uint64_uploader_uin.get(), - uploadNick = fileInfo.str_uploader_name.get(), - md5 = fileInfo.bytes_md5.get().toByteArray().toHexString(), - sha = fileInfo.bytes_sha.get().toByteArray().toHexString(), - // 根本没有 - sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString(), - )) - } - else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) { - val folderInfo = file.folder_info - dirs.add(FolderInfo( - groupId = groupId, - folderId = folderInfo.str_folder_id.get(), - folderName = folderInfo.str_folder_name.get(), - totalFileCount = folderInfo.uint32_total_file_count.get(), - createTime = folderInfo.uint32_create_time.get(), - creator = folderInfo.uint64_create_uin.get(), - creatorNick = folderInfo.str_creator_name.get() - )) - } else { - LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN) - } - } - } - } else { - throw RuntimeException("获取群文件列表失败") - } - - GroupFileList(files, dirs) - }.onFailure { - LogCenter.log(it.message + ", buffer: ${rspGetFileListBuffer.toHexString()}", Level.ERROR) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FriendSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FriendSvc.kt deleted file mode 100644 index 953239b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FriendSvc.kt +++ /dev/null @@ -1,121 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) -@file:Suppress("IllegalIdentifier") -package moe.fuqiuluo.qqinterface.servlet - -import com.tencent.common.app.AppInterface -import com.tencent.mobileqq.data.Friends -import com.tencent.mobileqq.friend.api.IFriendDataService -import com.tencent.mobileqq.friend.api.IFriendHandlerService -import com.tencent.mobileqq.qroute.QRoute -import com.tencent.mobileqq.relation.api.IAddFriendTempApi -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import mqq.app.AppRuntime -import tencent.mobileim.structmsg.structmsg -import kotlin.coroutines.resume - -internal object FriendSvc: BaseSvc() { - - suspend fun getFriendList(refresh: Boolean): Result> { - val runtime = AppRuntimeFetcher.appRuntime - val service = runtime.getRuntimeService(IFriendDataService::class.java, "all") - if(refresh || !service.isInitFinished) { - if(!requestFriendList(runtime, service)) { - return Result.failure(Exception("获取好友列表失败")) - } - } - return Result.success(service.allFriends) - } - - // ProfileService.Pb.ReqSystemMsgAction.Friend - fun requestFriendRequest(msgSeq: Long, uin: Long, remark: String = "", approve: Boolean? = true, notSee: Boolean? = false) { - val app = AppRuntimeFetcher.appRuntime - if (app !is AppInterface) - throw RuntimeException("AppRuntime cannot cast to AppInterface") - val service = QRoute.api(IAddFriendTempApi::class.java) - val action = structmsg.SystemMsgActionInfo() - action.type.set(if (approve != false) 2 else 3) - action.group_id.set(0) - action.remark.set(remark) - val snInfo = structmsg.AddFrdSNInfo() - snInfo.uint32_not_see_dynamic.set(if (notSee != false) 1 else 0) - snInfo.uint32_set_sn.set(0) - action.addFrdSNInfo.set(snInfo) - service.sendFriendSystemMsgAction(1, msgSeq, uin, 1, 2004, 11, 0, action, 0, - structmsg.StructMsg(), false, - app - ) - } - - suspend fun requestFriendSystemMsgNew(msgNum: Int, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 3): List? { - if (retryCnt < 0) { - return ArrayList() - } - val req = structmsg.ReqSystemMsgNew() - req.msg_num.set(msgNum) - req.latest_friend_seq.set(latestFriendSeq) - req.latest_group_seq.set(latestGroupSeq) - req.version.set(1000) - req.checktype.set(2) - val flag = structmsg.FlagInfo() -// flag.GrpMsg_Kick_Admin.set(1) -// flag.GrpMsg_HiddenGrp.set(1) -// flag.GrpMsg_WordingDown.set(1) - flag.FrdMsg_GetBusiCard.set(1) -// flag.GrpMsg_GetOfficialAccount.set(1) -// flag.GrpMsg_GetPayInGroup.set(1) - flag.FrdMsg_Discuss2ManyChat.set(1) -// flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1) - flag.FrdMsg_NeedWaitingMsg.set(1) - flag.FrdMsg_uint32_need_all_unread_msg.set(1) -// flag.GrpMsg_NeedAutoAdminWording.set(1) -// flag.GrpMsg_get_transfer_group_msg_flag.set(1) -// flag.GrpMsg_get_quit_pay_group_msg_flag.set(1) -// flag.GrpMsg_support_invite_auto_join.set(1) -// flag.GrpMsg_mask_invite_auto_join.set(1) -// flag.GrpMsg_GetDisbandedByAdmin.set(1) - flag.GrpMsg_GetC2cInviteJoinGroup.set(1) - req.flag.set(flag) - req.is_get_frd_ribbon.set(false) - req.is_get_grp_ribbon.set(false) - req.friend_msg_type_flag.set(1) - req.uint32_req_msg_type.set(1) - req.uint32_need_uid.set(1) - val respBuffer = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Friend", true, req.toByteArray()) - return if (respBuffer == null) { - ArrayList() - } else { - try { - val msg = structmsg.RspSystemMsgNew() - msg.mergeFrom(respBuffer.slice(4)) - return msg.friendmsgs.get() - } catch (err: Throwable) { - requestFriendSystemMsgNew(msgNum, latestFriendSeq, latestGroupSeq, retryCnt - 1) - } - - } - } - - - private suspend fun requestFriendList(runtime: AppRuntime, dataService: IFriendDataService): Boolean { - val service = runtime.getRuntimeService(IFriendHandlerService::class.java, "all") - service.requestFriendList(true, 0) - return suspendCancellableCoroutine { continuation -> - val waiter = GlobalScope.launch { - while (!dataService.isInitFinished) { - delay(200) - } - continuation.resume(true) - } - continuation.invokeOnCancellation { - waiter.cancel() - continuation.resume(false) - } - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GProSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GProSvc.kt deleted file mode 100644 index daea9dc..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GProSvc.kt +++ /dev/null @@ -1,361 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) - -package moe.fuqiuluo.qqinterface.servlet - -import com.tencent.mobileqq.qqguildsdk.api.IGPSService -import com.tencent.qqnt.kernel.nativeinterface.GProGuildRole -import com.tencent.qqnt.kernel.nativeinterface.GProRoleCreateInfo -import com.tencent.qqnt.kernel.nativeinterface.GProRoleMemberList -import com.tencent.qqnt.kernel.nativeinterface.GProRolePermission -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.ExperimentalSerializationApi - -import moe.fuqiuluo.qqinterface.servlet.structures.GProChannelInfo -import moe.fuqiuluo.qqinterface.servlet.structures.GetGuildMemberListNextToken -import moe.fuqiuluo.qqinterface.servlet.structures.GuildInfo -import moe.fuqiuluo.qqinterface.servlet.structures.GuildStatus -import moe.fuqiuluo.qqinterface.servlet.structures.SlowModeInfo -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.auto.toByteArray -import protobuf.guild.GetGuildFeedsReq -import protobuf.guild.GetGuildFeedsRsp -import protobuf.oidb.cmd0xf88.GProFilter -import protobuf.oidb.cmd0xf88.GProUserInfo -import protobuf.oidb.cmd0xf88.Oidb0xf88Req -import protobuf.oidb.cmd0xf88.Oidb0xf88Rsp -import protobuf.oidb.cmx0xf57.Oidb0xf57Filter -import protobuf.oidb.cmx0xf57.Oidb0xf57GuildInfo -import protobuf.oidb.cmx0xf57.Oidb0xf57MetaInfo -import protobuf.oidb.cmx0xf57.Oidb0xf57Req -import protobuf.oidb.cmx0xf57.Oidb0xf57Rsp -import protobuf.oidb.cmx0xf57.Oidb0xf57U1 -import protobuf.oidb.cmx0xf57.Oidb0xf57U2 -import protobuf.qweb.DEFAULT_DEVICE_INFO -import protobuf.qweb.QWebExtInfo -import protobuf.qweb.QWebReq -import protobuf.qweb.QWebRsp -import tencent.im.oidb.oidb_sso -import kotlin.coroutines.resume - -internal object GProSvc: BaseSvc() { - fun getSelfTinyId(): ULong { - val service = app.getRuntimeService(IGPSService::class.java, "all") - return service.selfTinyId.toULong() - } - - suspend fun getGuildInfo(guildId: ULong): Result { - val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf57_9", 0xf57, 9, Oidb0xf57Req( - filter = Oidb0xf57Filter( - u1 = Oidb0xf57U1(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u), - u2 = Oidb0xf57U2(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u) - ), - guildInfo = Oidb0xf57GuildInfo(guildId = guildId) - ).toByteArray()) - val body = oidb_sso.OIDBSSOPkg() - if (respBuffer == null) { - return Result.failure(Exception("unable to send packet")) - } - body.mergeFrom(respBuffer.slice(4)) - return runCatching { - body.bytes_bodybuffer.get() - .toByteArray() - .decodeProtobuf().metaInfo - } - } - - suspend fun getGuildFeeds(guildId: ULong, channelId: ULong, startIndex: Int): Result { - val buffer = sendBufferAW("QChannelSvr.trpc.qchannel.commreader.ComReader.GetGuildFeeds", true, QWebReq( - seq = 10, - qua = PlatformUtils.getQUA(), - deviceInfo = DEFAULT_DEVICE_INFO, - buffer = GetGuildFeedsReq( - count = 12, - from = startIndex, - feedAttchInfo = EMPTY_BYTE_ARRAY, - guildId = guildId, - getType = 1, - u7 = 0, - u8 = 1, - u9 = EMPTY_BYTE_ARRAY - ).toByteArray(), - traceId = app.account + "_0_0", - extinfo = listOf( - QWebExtInfo("fc-appid", "96"), - QWebExtInfo("environment_id", "shamrock"), - QWebExtInfo("tiny_id", getSelfTinyId().toString()), - ) - ).toByteArray()) ?: return Result.failure(Exception("unable to send packet")) - val webRsp = buffer.slice(4).decodeProtobuf() - if(webRsp.buffer == null) return Result.failure(Exception("server error")) - val wupBuffer = webRsp.buffer!! - val feeds = wupBuffer.decodeProtobuf() - return Result.success(feeds) - } - - fun getChannelList(guildId: ULong, refresh: Boolean = false): Result> { - if (refresh) { - refreshGuildInfo(guildId) - } - val result = arrayListOf() - app.getRuntimeService(IGPSService::class.java, "all").getChannelList(guildId.toString()).forEach { - result.add(GProChannelInfo( - ownerGuildId = guildId, - guildId = it.guildId, - channelId = it.channelUin.toLong(), - channelUin = it.channelUin.toLong(), - channelName = it.channelName ?: "", - channelType = it.type, - createTime = it.createTime, - creatorTinyId = it.creatorId.toLong(), - talkPermission = it.talkPermission, - visibleType = it.visibleType, - currentSlowMode = it.slowModeKey, - slowModes = it.gProSlowModeInfoList.map { - SlowModeInfo(it.slowModeKey, it.slowModeText, it.speakFrequency, it.slowModeCircle) - }, - appIconUrl = it.iconUrl, - jumpType = it.appChannelJumpType, - jumpSwitch = it.jumpSwitch, - jumpUrl = it.appChannelJumpUrl, - categoryId = it.categoryId, - myTalkPermission = it.myTalkPermissionType, - maxMemberCount = it.channelMemberMax - )) - } - return Result.success(result) - } - - fun refreshGuildInfo(guildId: ULong) { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - kernelGProService.refreshGuildInfo(guildId.toLong(), true, 1) - } - - suspend fun getGuildMemberList( - guildId: ULong, - startIndex: Long = 0, - roleIndex: Long = 1, - count: Int = 50, - fetchAll: Boolean = false, - result: ArrayList = arrayListOf() - ): Result>> { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - - val fetchGuildMemberListResult: Pair> = (withTimeoutOrNull(5000) { - suspendCancellableCoroutine { - kernelGProService.fetchMemberListWithRole(guildId.toLong(), 0, startIndex, roleIndex, count, 0) { code, reason, finish, nextIndex, nextRoleIdIndex, _, seq, roleList -> - if (code == 0) { - it.resume(GetGuildMemberListNextToken(nextIndex, nextRoleIdIndex, seq, finish) to roleList) - } else { - LogCenter.log("fetchMemberListWithRole failed: $code($reason)", Level.WARN) - it.resume(null) - } - } - } - }) ?: return Result.failure(Exception("unable to fetch guild member list")) - - val nextToken = fetchGuildMemberListResult.first - val roleList = fetchGuildMemberListResult.second - result.addAll(roleList) - return if (fetchAll) { - if (!fetchGuildMemberListResult.first.finish) { - getGuildMemberList(guildId, nextToken.startIndex, nextToken.roleIndex, count, true, result) - } else { - Result.success(nextToken.copy(finish = true) to result) - } - } else { - Result.success(nextToken to result) - } - } - - suspend fun getSelfGuildInfo(): Result { - val selfTinyId = getSelfTinyId() - return getUserGuildInfo(0u, selfTinyId) - } - - suspend fun getUserGuildInfo( - guildId: ULong, - memberTinyId: ULong - ): Result { - val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf88_1", 0xf88, 1, Oidb0xf88Req( - filter = GProFilter(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u), - memberId = 0uL, - tinyId = memberTinyId, - guildId = guildId - ).toByteArray()) - val body = oidb_sso.OIDBSSOPkg() - if (respBuffer == null) { - return Result.failure(Exception("unable to send packet")) - } - body.mergeFrom(respBuffer.slice(4)) - return runCatching { - body.bytes_bodybuffer.get().toByteArray().decodeProtobuf().userInfo!! - } - } - - private fun getGuildListByOldApi(result: ArrayList) { - app.getRuntimeService(IGPSService::class.java, "all").guildList?.forEach { - result.add(GuildInfo( - guildId = it.guildID.toLong(), - guildName = it.guildName ?: "", - guildDisplayId = it.guildNumber ?: "", - profile = it.profile ?: "", - status = GuildStatus( - isEnable = !it.isFrozen && !it.isBanned, - isBanned = it.isBanned, - isFrozen = it.isFrozen - ), - ownerId = 0, - shutUpTime = it.shutUpExpireTime, - allowSearch = it.allowSearch - )) - } - } - - private fun getGuildListByNt(result: ArrayList) { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - kernelGProService.guildListFromCache.forEach { - if (it.result != 0) return@forEach - val guildInfo = it.guildInfo - result.add(GuildInfo( - guildId = it.guildId, - guildName = guildInfo.guildName ?: "", - guildDisplayId = guildInfo.guildNumber ?: "", - profile = guildInfo.profile ?: "", - status = GuildStatus( - isEnable = guildInfo.guildStatus?.isEnable == 1, - isBanned = guildInfo.guildStatus?.isBanned == 1, - isFrozen = guildInfo.guildStatus?.isFrozen == 1 - ), - ownerId = guildInfo.ownerTinyid, - shutUpTime = guildInfo.shutupExpireTime, - allowSearch = guildInfo.allowSearch == 1 - )) - } - } - - suspend fun fetchGuildMemberRoles(guildId: ULong, tinyId: ULong, refresh: Boolean = false): Result> { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - if (refresh) { - kernelGProService.refreshGuildUserProfileInfo(guildId.toLong(), tinyId.toLong(), 1) - } - val result: ArrayList = withTimeoutOrNull(5000) { - suspendCancellableCoroutine { - kernelGProService.fetchMemberRoles(guildId.toLong(), 0, tinyId.toLong(), 2) { code, reason, roles -> - it.resume(roles) - } - } - } ?: return Result.failure(Exception("unable to fetch guild member roles")) - return Result.success(result) - } - - fun getGuildList(refresh: Boolean = false, forceOldApi: Boolean): ArrayList { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - if (refresh) { - kernelGProService.refreshGuildList(true) - kernelGProService.guildListFromCache.forEach { - refreshGuildInfo(it.guildId.toULong()) - } - } - val result = arrayListOf() - if (PlatformUtils.getQQVersionCode() < PlatformUtils.QQ_9_0_8_VER || forceOldApi) { - getGuildListByOldApi(result) - } else { - runCatching { - getGuildListByNt(result) - }.onFailure { - LogCenter.log("GetGuildListByNt failed: ${it.stackTraceToString()}", Level.ERROR) - getGuildListByOldApi(result) // 防止QQ更新API导致无法获取 - } - } - - return result - } - - suspend fun getGuildRoles(guildId: ULong): Result> { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - val roles: List = withTimeoutOrNull(5000) { - suspendCancellableCoroutine { - kernelGProService.fetchRoleListWithPermission(guildId.toLong(), 1) { code, _, roles, _, _, _ -> - if (code != 0) it.resume(null) else it.resume(roles) - } - } - } ?: return Result.failure(Exception("unable to fetch guild roles")) - return Result.success(roles) - } - - fun deleteGuildRole(guildId: ULong, roleId: ULong) { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - kernelGProService.deleteRole(guildId.toLong(), roleId.toLong()) { code, msg, result -> - if (code != 0) { - LogCenter.log("deleteGuildRole failed: $code($msg) => $result", Level.WARN) - } - } - } - - fun setMemberRole(guildId: ULong, tinyId: ULong, roleId: ULong, isSet: Boolean) { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - val addList = arrayListOf() - val rmList = arrayListOf() - (if (isSet) addList else rmList).add(roleId.toLong()) - kernelGProService.setMemberRoles(guildId.toLong(), 0, 0, tinyId.toLong(), addList, rmList) { code, msg, result -> - if (code != 0) { - LogCenter.log("setMemberRole failed: $code($msg) => $result", Level.WARN) - } - } - } - - suspend fun getGuildRolePermission(guildId: ULong, roleId: ULong): Result { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - val role:GProGuildRole = withTimeoutOrNull(5000) { - suspendCancellableCoroutine { - kernelGProService.fetchRoleWithPermission(guildId.toLong(), roleId.toLong(), 1) { code, msg, role, _, _, _ -> - if (code != 0) { - LogCenter.log("getGuildRolePermission failed: $code($msg)", Level.WARN) - it.resume(null) - } else it.resume(role) - } - } - } ?: return Result.failure(Exception("unable to fetch guild role permission")) - return Result.success(role) - } - - suspend fun updateGuildRole(guildId: ULong, roleId: ULong, name: String, color: Long): Result { - val oldInfo = getGuildRolePermission(guildId, roleId).onFailure { - return Result.failure(it) - }.getOrThrow() - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - val info = GProRoleCreateInfo( - name, color, oldInfo.bHoist, oldInfo.rolePermissions - ) - kernelGProService.setRoleInfo(guildId.toLong(), roleId.toLong(), info) { code, msg, result -> - if (code != 0) { - LogCenter.log("updateGuildRole failed: $code($msg) => $result", Level.WARN) - } - } - return Result.success(Unit) - } - - suspend fun createGuildRole(guildId: ULong, name: String, color: Long, initialUsers: ArrayList): Result { - val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService - val permission = GProRolePermission(false, arrayListOf()) - val info = GProRoleCreateInfo(name, color, false, permission) - val role: GProGuildRole = withTimeoutOrNull(5000) { - suspendCancellableCoroutine { - kernelGProService.createRole(guildId.toLong(), info, initialUsers) { code, msg, result, role -> - if (code != 0) { - LogCenter.log("createGuildRole failed: $code($msg) => $result", Level.WARN) - it.resume(null) - } else it.resume(role) - } - } - } ?: return Result.failure(Exception("unable to create guild role")) - return Result.success(role) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt deleted file mode 100644 index 4770f74..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt +++ /dev/null @@ -1,1152 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.qqinterface.servlet - -import KQQ.RespBatchProcess -import androidx.core.text.HtmlCompat -import com.qq.jce.wup.UniPacket -import com.tencent.common.app.AppInterface -import com.tencent.mobileqq.app.BusinessHandlerFactory -import com.tencent.mobileqq.app.QQAppInterface -import com.tencent.mobileqq.data.troop.TroopInfo -import com.tencent.mobileqq.data.troop.TroopMemberInfo -import com.tencent.mobileqq.pb.ByteStringMicro -import com.tencent.mobileqq.troop.api.ITroopInfoService -import com.tencent.mobileqq.troop.api.ITroopMemberInfoService -import com.tencent.protofile.join_group_link.join_group_link -import com.tencent.qphone.base.remote.ToServiceMsg -import com.tencent.qqnt.kernel.nativeinterface.MemberInfo -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import friendlist.stUinInfo -import io.ktor.client.call.body -import io.ktor.client.request.forms.MultiPartFormDataContent -import io.ktor.client.request.forms.formData -import io.ktor.client.request.forms.submitForm -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.http.ContentType -import io.ktor.http.Headers -import io.ktor.http.HttpHeaders -import io.ktor.http.contentType -import io.ktor.http.headers -import io.ktor.http.parameters -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.encodeToByteArray -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.decodeFromStream -import kotlinx.serialization.json.jsonObject - -import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin -import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getUin -import moe.fuqiuluo.qqinterface.servlet.structures.GroupAtAllRemainInfo -import moe.fuqiuluo.qqinterface.servlet.structures.NotJoinedGroupInfo -import moe.fuqiuluo.qqinterface.servlet.structures.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 -import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncement -import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessage -import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessageImage -import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole -import moe.fuqiuluo.shamrock.tools.EmptyJsonArray -import moe.fuqiuluo.shamrock.tools.GlobalClient -import moe.fuqiuluo.shamrock.tools.asInt -import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull -import moe.fuqiuluo.shamrock.tools.asJsonObject -import moe.fuqiuluo.shamrock.tools.asLong -import moe.fuqiuluo.shamrock.tools.asString -import moe.fuqiuluo.shamrock.tools.asStringOrNull -import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty -import moe.fuqiuluo.shamrock.tools.putBuf32Long -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.utils.FileUtils -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import protobuf.oidb.cmd0xf16.Oidb0xf16 -import protobuf.oidb.cmd0xf16.SetGroupRemarkReq -import mqq.app.MobileQQ -import protobuf.auto.toByteArray -import tencent.im.group.group_member_info -import tencent.im.oidb.cmd0x88d.oidb_0x88d -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.cmd0xeac.oidb_0xeac -import tencent.im.oidb.cmd0xeb7.oidb_0xeb7 -import tencent.im.oidb.cmd0xed3.oidb_cmd0xed3 -import tencent.im.oidb.oidb_sso -import tencent.im.troop.honor.troop_honor -import tencent.mobileim.structmsg.structmsg -import java.lang.reflect.Method -import java.lang.reflect.Modifier -import java.nio.ByteBuffer -import kotlin.coroutines.resume - -internal object GroupSvc: BaseSvc() { - private const val GET_MEMBER_ROLE_BY_NT = false - - private val RefreshTroopMemberInfoLock by lazy { - Mutex() - } - private val RefreshTroopMemberListLock by lazy { - Mutex() - } - - private lateinit var METHOD_REQ_MEMBER_INFO: Method - private lateinit var METHOD_REQ_MEMBER_INFO_V2: Method - private lateinit var METHOD_REQ_TROOP_LIST: Method - private lateinit var METHOD_REQ_TROOP_MEM_LIST: Method - private lateinit var METHOD_REQ_MODIFY_GROUP_NAME: Method - - suspend fun getGroupRemainAtAllRemain (groupId: Long): Result { - 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> { - val buffer = sendOidbAW("OidbSvc.0x899_0", 2201, 0, oidb_0x899.ReqBody().apply { - uint64_group_code.set(groupId) - uint64_start_uin.set(0) - uint32_identify_flag.set(6) - memberlist_opt.set(oidb_0x899.memberlist().apply { - uint64_member_uin.set(0) - uint32_shutup_timestap.set(0) - }) - }.toByteArray()) ?: 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 = oidb_0x899.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) - return Result.success(resp.rpt_memberlist.get().map { - ProhibitedMemberInfo(it.uint64_member_uin.get(), it.uint32_shutup_timestap.get()) - }) - } - - fun poke(groupId: Long, userId: Long) { - val req = oidb_cmd0xed3.ReqBody().apply { - uint64_group_code.set(groupId) - uint64_to_uin.set(userId) - uint32_msg_seq.set(0) - } - sendOidb("OidbSvc.0xed3", 3795, 1, req.toByteArray()) - } - - suspend fun getGroupMemberList(groupId: Long, refresh: Boolean): Result> { - val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") - var memberList = service.getAllTroopMembers(groupId.toString()) - if (refresh || memberList == null) { - memberList = requestTroopMemberInfo(service, groupId).onFailure { - return Result.failure(Exception("获取群成员列表失败")) - }.getOrThrow() - } - - getGroupInfo(groupId, true).onSuccess { - if(it.wMemberNum > memberList.size) { - return getGroupMemberList(groupId, true) - } - } - - return Result.success(memberList) - } - - suspend fun getGroupList(refresh: Boolean): Result> { - val service = app.getRuntimeService(ITroopInfoService::class.java, "all") - - var troopList = service.allTroopList - if(refresh || !service.isTroopCacheInited || troopList == null) { - if(!requestGroupInfo(service)) { - return Result.failure(Exception("获取群列表失败")) - } else { - troopList = service.allTroopList - } - } - return Result.success(troopList) - } - - suspend fun getNotJoinedGroupInfo(groupId: Long): Result { - return withTimeoutOrNull(5000) timeout@{ - val toServiceMsg = createToServiceMsg("ProfileService.ReqBatchProcess") - toServiceMsg.extraData.putLong("troop_code", groupId) - toServiceMsg.extraData.putBoolean("is_admin", false) - toServiceMsg.extraData.putInt("from", 0) - val buffer = sendAW(toServiceMsg) - val uniPacket = UniPacket(true) - uniPacket.encodeName = "utf-8" - uniPacket.decode(buffer) - val respBatchProcess = uniPacket.getByClass("RespBatchProcess", RespBatchProcess()) - val batchRespInfo = oidb_0x88d.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg() - .mergeFrom(respBatchProcess.batch_response_list.first().buffer) - .bytes_bodybuffer.get().toByteArray()).stzrspgroupinfo.get().firstOrNull() - ?: return@timeout Result.failure(Exception("获取群信息失败")) - val info = batchRespInfo.stgroupinfo - Result.success(NotJoinedGroupInfo( - groupId = batchRespInfo.uint64_group_code.get(), - maxMember = info.uint32_group_member_max_num.get(), - memberCount = info.uint32_group_member_num.get(), - groupName = info.string_group_name.get().toStringUtf8(), - groupDesc = info.string_group_finger_memo.get().toStringUtf8(), - owner = info.uint64_group_owner.get(), - createTime = info.uint32_group_create_time.get().toLong(), - groupFlag = info.uint32_group_flag.get(), - groupFlagExt = info.uint32_group_flag_ext.get() - )) - } ?: Result.failure(Exception("获取群信息超时")) - } - - suspend fun getGroupInfo(groupId: Long, refresh: Boolean): Result { - val service = app - .getRuntimeService(ITroopInfoService::class.java, "all") - - val groupInfo = getGroupInfo(groupId) - - return if(refresh || !service.isTroopCacheInited || groupInfo.troopuin.isNullOrBlank()) { - requestGroupInfo(service, groupId) - } else { - Result.success(groupInfo) - } - - } - - suspend fun setGroupUniqueTitle(groupId: Long, userId: Long, title: String) { - val localMemberInfo = getTroopMemberInfoByUin(groupId, userId, true).getOrThrow() - val req = Oidb_0x8fc.ReqBody() - req.uint64_group_code.set(groupId) - val memberInfo = Oidb_0x8fc.MemberInfo() - memberInfo.uint64_uin.set(userId) - memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty { - localMemberInfo.troopremark.ifNullOrEmpty("") - })) - memberInfo.bytes_special_title.set(ByteStringMicro.copyFromUtf8(title)) - memberInfo.uint32_special_title_expire_time.set(-1) - req.rpt_mem_level_info.add(memberInfo) - sendOidb("OidbSvc.0x8fc_2", 2300, 2, req.toByteArray()) - } - - fun modifyGroupMemberCard(groupId: Long, userId: Long, name: String): Boolean { - val createToServiceMsg: ToServiceMsg = createToServiceMsg("friendlist.ModifyGroupCardReq") - createToServiceMsg.extraData.putLong("dwZero", 0L) - createToServiceMsg.extraData.putLong("dwGroupCode", groupId) - val info = stUinInfo() - info.cGender = -1 - info.dwuin = userId - info.sEmail = "" - info.sName = name - info.sPhone = "" - info.sRemark = "" - info.dwFlag = 1 - createToServiceMsg.extraData.putSerializable("vecUinInfo", arrayListOf(info)) - createToServiceMsg.extraData.putLong("dwNewSeq", 0L) - send(createToServiceMsg) - return true - } - - fun modifyGroupRemark(groupId: Long, remark: String): Boolean { - sendOidb("OidbSvc.0xf16_1", 3862, 1, Oidb0xf16( - setGroupRemarkReq = SetGroupRemarkReq( - groupCode = groupId.toULong(), - groupUin = groupCode2GroupUin(groupId).toULong(), - groupRemark = remark - ) - ).toByteArray()) - return true - } - - suspend fun setEssenceMessage(groupId: Long, seq: Long, rand: Long): Pair { - val buffer = sendOidbAW("OidbSvc.0xeac_1", 3756, 1, oidb_0xeac.ReqBody().apply { - group_code.set(groupId) - msg_seq.set(seq.toInt()) - msg_random.set(rand.toInt()) - }.toByteArray()) ?: return Pair(false, "unknown error") - val body = oidb_sso.OIDBSSOPkg() - body.mergeFrom(buffer.slice(4)) - val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) - return if (result.wording.has()) { - LogCenter.log("设置群精华失败: ${result.wording.get()}") - Pair(false, "设置群精华失败: ${result.wording.get()}") - } else { - LogCenter.log("设置群精华 -> $groupId: $seq") - Pair(true, "ok") - } - } - - suspend fun deleteEssenceMessage(groupId: Long, seq: Long, rand: Long): Pair { - val buffer = sendOidbAW("OidbSvc.0xeac_2", 3756, 2, oidb_0xeac.ReqBody().apply { - group_code.set(groupId) - msg_seq.set(seq.toInt()) - msg_random.set(rand.toInt()) - }.toByteArray()) - val body = oidb_sso.OIDBSSOPkg() - if (buffer == null) { - return Pair(false, "unknown error") - } - body.mergeFrom(buffer.slice(4)) - val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) - return if (result.wording.has()) { - LogCenter.log("移除群精华失败: ${result.wording.get()}") - Pair(false, "移除群精华失败: ${result.wording.get()}") - } else { - LogCenter.log("移除群精华 -> $groupId: $seq") - Pair(true, "ok") - } - } - - fun setGroupAdmin(groupId: Long, userId: Long, enable: Boolean) { - val buffer = ByteBuffer.allocate(9) - buffer.putBuf32Long(groupId) - buffer.putBuf32Long(userId) - buffer.put(if (enable) 1 else 0) - val array = buffer.array() - sendOidb("OidbSvc.0x55c_1", 1372, 1, array) - } - - fun setGroupWholeBan(groupId: Long, enable: Boolean) { - val reqBody = oidb_0x89a.ReqBody() - reqBody.uint64_group_code.set(groupId) - reqBody.st_group_info.set(oidb_0x89a.groupinfo().apply { - uint32_shutup_time.set(if (enable) 268435455 else 0) - }) - sendOidb("OidbSvc.0x89a_0", 2202, 0, reqBody.toByteArray()) - } - - fun banMember(groupId: Long, memberUin: Long, time: Int) { - val buffer = ByteBuffer.allocate(1 * 8 + 7) - buffer.putBuf32Long(groupId) - buffer.put(32.toByte()) - buffer.putShort(1) - buffer.putBuf32Long(memberUin) - buffer.putInt(time) - val array = buffer.array() - sendOidb("OidbSvc.0x570_8", 1392, 8, array) - } - - fun kickMember(groupId: Long, rejectAddRequest: Boolean, kickMsg: String, vararg memberUin: Long) { - val reqBody = oidb_0x8a0.ReqBody() - reqBody.opt_uint64_group_code.set(groupId) - - memberUin.forEach { - val memberInfo = oidb_0x8a0.KickMemberInfo() - memberInfo.opt_uint32_operate.set(5) - memberInfo.opt_uint64_member_uin.set(it) - memberInfo.opt_uint32_flag.set(if (rejectAddRequest) 1 else 0) - reqBody.rpt_msg_kick_list.add(memberInfo) - } - if (kickMsg.isNotEmpty()) { - reqBody.bytes_kick_msg.set(ByteStringMicro.copyFrom(kickMsg.toByteArray())) - } - - sendOidb("OidbSvc.0x8a0_0", 2208, 0, reqBody.toByteArray()) - } - - fun getGroupInfo(groupId: Long): TroopInfo { - val runtime = AppRuntimeFetcher.appRuntime as QQAppInterface - - val service = runtime - .getRuntimeService(ITroopInfoService::class.java, "all") - - return service.getTroopInfo(groupId.toString()) - } - - fun getAdminList( - groupId: Long, - withOwner: Boolean = false - ): List { - val groupInfo = getGroupInfo(groupId) - return (groupInfo.Administrator ?: "") - .split("|", ",") - .also { - if (withOwner && it is ArrayList) { - it.add(0, groupInfo.troopowneruin) - } - } - .map { (it.ifNullOrEmpty("0")!!).toLong() } - .filter { it != 0L } - } - - suspend fun getMemberRole(groupId: Long, memberUin: Long): MemberRole { - if (!GET_MEMBER_ROLE_BY_NT) { - return when (memberUin) { - getOwner(groupId) -> MemberRole.Owner - in getAdminList(groupId) -> MemberRole.Admin - else -> MemberRole.Member - } - } - return when(getTroopMemberInfoByUinViaNt(groupId, memberUin, 3000).getOrNull()?.role) { - com.tencent.qqnt.kernel.nativeinterface.MemberRole.STRANGER -> MemberRole.Stranger - com.tencent.qqnt.kernel.nativeinterface.MemberRole.MEMBER -> MemberRole.Member - com.tencent.qqnt.kernel.nativeinterface.MemberRole.ADMIN -> MemberRole.Admin - com.tencent.qqnt.kernel.nativeinterface.MemberRole.OWNER -> MemberRole.Owner - com.tencent.qqnt.kernel.nativeinterface.MemberRole.UNSPECIFIED, null -> when (memberUin) { - getOwner(groupId) -> MemberRole.Owner - in getAdminList(groupId) -> MemberRole.Admin - else -> MemberRole.Member - } - } - } - - fun getOwner(groupId: Long): Long { - val groupInfo = getGroupInfo(groupId) - return groupInfo.troopowneruin?.toLong() ?: 0 - } - - fun isOwner(groupId: Long): Boolean { - val groupInfo = getGroupInfo(groupId) - return groupInfo.troopowneruin == app.account - } - - fun isAdmin(groupId: Long): Boolean { - val service = app - .getRuntimeService(ITroopInfoService::class.java, "all") - - val groupInfo = service.getTroopInfo(groupId.toString()) - - return groupInfo.isAdmin || groupInfo.troopowneruin == app.account - } - - fun resignTroop(groupId: Long) { - sendExtra("ProfileService.GroupMngReq") { - it.putInt("groupreqtype", 2) - it.putString("troop_uin", groupId.toString()) - it.putString("uin", currentUin) - } - } - - fun modifyTroopName(groupId: Long, name: String) { - val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MODIFY_HANDLER) - - if (!GroupSvc::METHOD_REQ_MODIFY_GROUP_NAME.isInitialized) { - METHOD_REQ_MODIFY_GROUP_NAME = businessHandler.javaClass.declaredMethods.first { - it.parameterCount == 3 - && it.parameterTypes[0] == String::class.java - && it.parameterTypes[1] == String::class.java - && it.parameterTypes[2] == Boolean::class.java - && !Modifier.isPrivate(it.modifiers) - } - } - - METHOD_REQ_MODIFY_GROUP_NAME.invoke(businessHandler, groupId.toString(), name, false) - } - - fun parseHonor(honor: String?): List { - return (honor ?: "") - .split("|") - .filter { it.isNotBlank() } - .map { it.toInt() } - } - - fun groupUin2GroupCode(groupuin: Long): Long { - var calc = groupuin / 1000000L - while (true) { - calc -= if (calc >= 0 + 202 && calc + 202 <= 10) { - (202 - 0).toLong() - } else if (calc >= 11 + 480 && calc <= 19 + 480) { - (480 - 11).toLong() - } else if (calc >= 20 + 2100 && calc <= 66 + 2100) { - (2100 - 20).toLong() - } else if (calc >= 67 + 2010 && calc <= 156 + 2010) { - (2010 - 67).toLong() - } else if (calc >= 157 + 2147 && calc <= 209 + 2147) { - (2147 - 157).toLong() - } else if (calc >= 210 + 4100 && calc <= 309 + 4100) { - (4100 - 210).toLong() - } else if (calc >= 310 + 3800 && calc <= 499 + 3800) { - (3800 - 310).toLong() - } else { - break - } - } - return calc * 1000000L + groupuin % 1000000L - } - - fun groupCode2GroupUin(groupcode: Long): Long { - var calc = groupcode / 1000000L - loop@ while (true) calc += when (calc) { - in 0..10 -> { - (202 - 0).toLong() - } - in 11..19 -> { - (480 - 11).toLong() - } - in 20..66 -> { - (2100 - 20).toLong() - } - in 67..156 -> { - (2010 - 67).toLong() - } - in 157..209 -> { - (2147 - 157).toLong() - } - in 210..309 -> { - (4100 - 210).toLong() - } - in 310..499 -> { - (3800 - 310).toLong() - } - else -> { - break@loop - } - } - return calc * 1000000L + groupcode % 1000000L - } - - suspend fun getShareTroopArkMsg(groupId: Long): String { - val reqBody = join_group_link.ReqBody() - reqBody.get_ark.set(true) - reqBody.type.set(1) - reqBody.group_code.set(groupId) - val buffer = sendBufferAW("GroupSvc.JoinGroupLink", true, reqBody.toByteArray()) - ?: error("unable to fetch contact ark_json_text") - val body = join_group_link.RspBody() - body.mergeFrom(buffer.slice(4)) - return body.signed_ark.get().toStringUtf8() - } - - suspend fun getTroopMemberInfoByUin( - groupId: Long, - uin: Long, - refresh: Boolean = false - ): Result { - val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") - var info = service.getTroopMember(groupId.toString(), uin.toString()) - if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) { - info = requestTroopMemberInfo(service, groupId, uin).getOrNull() - } - if (info == null) { - info = getTroopMemberInfoByUinViaNt(groupId, uin).getOrNull()?.let { - TroopMemberInfo().apply { - troopnick = it.cardName - friendnick = it.nick - } - } - } - try { - if (info != null && (info.alias == null || info.alias.isBlank())) { - val req = group_member_info.ReqBody() - req.uint64_group_code.set(groupId) - 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 { - Result.failure(Exception("获取群成员信息失败")) - } - } - - suspend fun getTroopMemberInfoByUinV2( - groupId: Long, - uin: Long, - refresh: Boolean = false - ): Result { - val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") - var info = service.getTroopMember(groupId.toString(), uin.toString()) - if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) { - info = requestTroopMemberInfo(service, groupId, uin, timeout = 2000).getOrNull() - } - if (info == null) { - info = getTroopMemberInfoByUinViaNt(groupId, uin, timeout = 2000L).getOrNull()?.let { - TroopMemberInfo().apply { - troopnick = it.cardName - friendnick = it.nick - } - } - } - try { - if (info != null && (info.alias == null || info.alias.isBlank())) { - val req = group_member_info.ReqBody() - req.uint64_group_code.set(groupId) - req.uint64_uin.set(uin) - 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(), timeout = 2000) - 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 { - Result.failure(Exception("获取群成员信息失败")) - } - } - - suspend fun getTroopMemberInfoByUinViaNt( - groupId: Long, - qq: Long, - timeout: Long = 5000L - ): Result { - return runCatching { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val groupService = sessionService.groupService - val info = withTimeoutOrNull(timeout) { - suspendCancellableCoroutine { - groupService.getTransferableMemberInfo(groupId) { code, _, data -> - if (code != 0) { - it.resume(null) - return@getTransferableMemberInfo - } - data.forEach { (_, info) -> - if (info.uin == qq) { - it.resume(info) - return@forEach - } - } - it.resume(null) - } - } - } - return if (info != null) { - Result.success(info) - } else { - Result.failure(Exception("获取群成员信息失败")) - } - } - } - - suspend fun getTroopMemberInfoByUid(groupId: Long, uid: String): Result { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val groupService = sessionService.groupService - val info = suspendCancellableCoroutine { - groupService.getTransferableMemberInfo(groupId) { code, _, data -> - if (code != 0) { - it.resume(null) - return@getTransferableMemberInfo - } - data.forEach { (tmpUid, info) -> - if (tmpUid == uid) { - it.resume(info) - return@forEach - } - } - } - } - return if (info != null) { - Result.success(info) - } else { - Result.failure(Exception("获取群成员信息失败")) - } - } - - private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long): Result> { - val info = RefreshTroopMemberListLock.withLock { - service.deleteTroopMembers(groupId.toString()) - refreshTroopMemberList(groupId) - - withTimeoutOrNull(10000) { - var memberList: List? - do { - delay(100) - memberList = service.getAllTroopMembers(groupId.toString()) - } while (memberList.isNullOrEmpty()) - return@withTimeoutOrNull memberList - } - } - return if (info != null) { - Result.success(info) - } else { - Result.failure(Exception("获取群成员信息失败")) - } - } - - private suspend fun requestGroupInfo( - service: ITroopInfoService - ): Boolean { - refreshTroopList() - - return suspendCancellableCoroutine { continuation -> - val waiter = GlobalScope.launch { - do { - delay(1000) - } while ( - !service.isTroopCacheInited - ) - continuation.resume(true) - } - continuation.invokeOnCancellation { - waiter.cancel() - continuation.resume(false) - } - } - } - - private fun refreshTroopMemberList(groupId: Long) { - val app = AppRuntimeFetcher.appRuntime - if (app !is AppInterface) - throw RuntimeException("AppRuntime cannot cast to AppInterface") - val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_LIST_HANDLER) - - // 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 - && it.parameterTypes[0] == Boolean::class.java - && it.parameterTypes[1] == String::class.java - && it.parameterTypes[2] == String::class.java - && it.parameterTypes[3] == Int::class.java - && !Modifier.isPrivate(it.modifiers) - } - } - - METHOD_REQ_TROOP_MEM_LIST.invoke(businessHandler, true, groupId.toString(), groupUin2GroupCode(groupId).toString(), 5) - } - - private fun refreshTroopList() { - val app = AppRuntimeFetcher.appRuntime - if (app !is AppInterface) - throw RuntimeException("AppRuntime cannot cast to AppInterface") - val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_LIST_HANDLER) - - if (!GroupSvc::METHOD_REQ_TROOP_LIST.isInitialized) { - METHOD_REQ_TROOP_LIST = businessHandler.javaClass.declaredMethods.first { - it.parameterCount == 0 && !Modifier.isPrivate(it.modifiers) && it.returnType == Void.TYPE - } - } - - METHOD_REQ_TROOP_LIST.invoke(businessHandler) - } - - private fun requestMemberInfo(groupId: Long, memberUin: Long) { - val app = AppRuntimeFetcher.appRuntime - if (app !is AppInterface) - throw RuntimeException("AppRuntime cannot cast to AppInterface") - val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER) - - if (!GroupSvc::METHOD_REQ_MEMBER_INFO.isInitialized) { - METHOD_REQ_MEMBER_INFO = businessHandler.javaClass.declaredMethods.first { - it.parameterCount == 2 && - it.parameterTypes[0] == Long::class.java && - it.parameterTypes[1] == Long::class.java && - !Modifier.isPrivate(it.modifiers) - } - } - - METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId, memberUin) - } - - private fun requestMemberInfoV2(groupId: Long, memberUin: Long) { - val app = AppRuntimeFetcher.appRuntime - if (app !is AppInterface) - throw RuntimeException("AppRuntime cannot cast to AppInterface") - val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER) - - if (!GroupSvc::METHOD_REQ_MEMBER_INFO_V2.isInitialized) { - METHOD_REQ_MEMBER_INFO_V2 = businessHandler.javaClass.declaredMethods.first { - it.parameterCount == 3 && - it.parameterTypes[0] == String::class.java && - it.parameterTypes[1] == String::class.java && - !Modifier.isPrivate(it.modifiers) - } - } - - METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler, groupId.toString(), groupUin2GroupCode(groupId).toString(), arrayListOf(memberUin.toString())) - } - - private suspend fun requestGroupInfo(dataService: ITroopInfoService, uin: Long): Result { - val strUin = uin.toString() - val info = withTimeoutOrNull(1000) { - var troopInfo: TroopInfo? - do { - troopInfo = dataService.getTroopInfo(strUin) - delay(100) - } while (troopInfo == null || troopInfo.troopuin.isNullOrBlank()) - return@withTimeoutOrNull troopInfo - } - return if (info != null) { - Result.success(info) - } else { - Result.failure(Exception("获取群列表失败")) - } - } - - private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result { - val info = RefreshTroopMemberInfoLock.withLock { - val groupIdStr = groupId.toString() - val memberUinStr = memberUin.toString() - - service.deleteTroopMember(groupIdStr, memberUinStr) - - requestMemberInfoV2(groupId, memberUin) - requestMemberInfo(groupId, memberUin) - - withTimeoutOrNull(timeout) { - while (!service.isMemberInCache(groupIdStr, memberUinStr)) { - delay(200) - } - return@withTimeoutOrNull service.getTroopMember(groupIdStr, memberUinStr) - } - } - return if (info != null) { - Result.success(info) - } else { - Result.failure(Exception("获取群成员信息失败")) - } - } - - // ProfileService.Pb.ReqSystemMsgAction.Group - suspend fun requestGroupRequest( - msgSeq: Long, - uin: Long, - gid: Long, - msg: String? = "", - approve: Boolean? = true, - notSee: Boolean? = false, - subType: String - ): Result{ - val req = structmsg.ReqSystemMsgAction().apply { - if (subType == "invite") { - msg_type.set(1) - src_id.set(3) - sub_src_id.set(10016) - group_msg_type.set(2) - } else { - msg_type.set(2) - src_id.set(2) - sub_src_id.set(30024) - group_msg_type.set(1) - } - msg_seq.set(msgSeq) - req_uin.set(uin) - sub_type.set(1) - action_info.set(structmsg.SystemMsgActionInfo().apply { - type.set(if (approve != false) 11 else 12) - group_code.set(gid) - if (subType == "add") { - this.msg.set(msg) - this.blacklist.set(notSee != false) - } - }) - language.set(1000) - } - val respBuffer = sendBufferAW("ProfileService.Pb.ReqSystemMsgAction.Group", true, req.toByteArray()) - ?: return Result.failure(Exception("操作失败")) - val rsp = structmsg.RspSystemMsgAction().mergeFrom(respBuffer.slice(4)) - return if (rsp.head.result.has()) { - if (rsp.head.result.get() == 0) { - Result.success(rsp.msg_detail.get()) - } else { - Result.failure(Exception(rsp.head.msg_fail.get())) - } - } else { - Result.failure(Exception("操作失败")) - } - } - - suspend fun requestGroupSystemMsgNew(msgNum: Int, reqMsgType: Int = 1, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 5): List { - if (retryCnt < 0) { - return ArrayList() - } - val req = structmsg.ReqSystemMsgNew() - req.msg_num.set(msgNum) - req.latest_friend_seq.set(latestFriendSeq) - req.latest_group_seq.set(latestGroupSeq) - req.version.set(1000) - req.checktype.set(3) - val flag = structmsg.FlagInfo() - flag.GrpMsg_Kick_Admin.set(1) - flag.GrpMsg_HiddenGrp.set(1) - flag.GrpMsg_WordingDown.set(1) -// flag.FrdMsg_GetBusiCard.set(1) - flag.GrpMsg_GetOfficialAccount.set(1) - flag.GrpMsg_GetPayInGroup.set(1) - flag.FrdMsg_Discuss2ManyChat.set(1) - flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1) - flag.FrdMsg_NeedWaitingMsg.set(1) -// flag.FrdMsg_uint32_need_all_unread_msg.set(1) - flag.GrpMsg_NeedAutoAdminWording.set(1) - flag.GrpMsg_get_transfer_group_msg_flag.set(1) - flag.GrpMsg_get_quit_pay_group_msg_flag.set(1) - flag.GrpMsg_support_invite_auto_join.set(1) - flag.GrpMsg_mask_invite_auto_join.set(1) - flag.GrpMsg_GetDisbandedByAdmin.set(1) - flag.GrpMsg_GetC2cInviteJoinGroup.set(1) - req.flag.set(flag) - req.is_get_frd_ribbon.set(false) - req.is_get_grp_ribbon.set(false) - req.friend_msg_type_flag.set(1) - req.uint32_req_msg_type.set(reqMsgType) - req.uint32_need_uid.set(1) - val respBuffer = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Group", true, req.toByteArray()) - return if (respBuffer == null) { - ArrayList() - } else { - try { - val msg = structmsg.RspSystemMsgNew() - msg.mergeFrom(respBuffer.slice(4)) - return msg.groupmsgs.get().orEmpty() - } catch (err: Throwable) { - requestGroupSystemMsgNew(msgNum, reqMsgType, latestFriendSeq, latestGroupSeq, retryCnt - 1) - } - } - } - - - @OptIn(ExperimentalSerializationApi::class) - suspend fun getEssenceMessageList(groupId: Long, page: Int = 0, pageSize: Int = 20): Result>{ -// GlobalClient.get() - val cookie = TicketSvc.getCookie("qun.qq.com") - val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin())) - val url = "https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=${bkn}&group_code=${groupId}&page_start=${page}&page_limit=${pageSize}" - val response = GlobalClient.get(url) { - header("Cookie", cookie) - } - val body = Json.decodeFromStream(response.body()) - if (body.jsonObject["retcode"].asInt == 0) { - val data = body.jsonObject["data"].asJsonObject - val list = data["msg_list"].asJsonArrayOrNull - ?: // is_end - return Result.success(ArrayList()) - return Result.success(list.map { - val obj = it.jsonObject - val msgSeq = obj["msg_seq"].asInt - val msg = EssenceMessage( - senderId = obj["sender_uin"].asString.toLong(), - senderNick = obj["sender_nick"].asString, - senderTime = obj["sender_time"].asLong, - operatorId = obj["add_digest_uin"].asString.toLong(), - operatorNick = obj["add_digest_nick"].asString, - operatorTime = obj["add_digest_time"].asLong, - messageId = 0, - messageSeq = msgSeq, - realId = msgSeq, - messageContent = obj["msg_content"] ?: EmptyJsonArray - ) - val mapping = MessageHelper.getMsgMappingBySeq(MsgConstant.KCHATTYPEGROUP, groupId.toString(), msgSeq) - if (mapping != null) { - msg.messageId = mapping.msgHashId - } - msg - }) - } else { - return Result.failure(Exception(body.jsonObject["retmsg"].asStringOrNull)) - } - } - - @OptIn(ExperimentalSerializationApi::class) - suspend fun getGroupAnnouncements(groupId: Long): Result>{ - val cookie = TicketSvc.getCookie("qun.qq.com") - val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin())) - val url = "https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=${bkn}&qid=${groupId}&ft=23&s=-1&n=20" - val response = GlobalClient.get(url) { - header("Cookie", cookie) - } - val body = Json.decodeFromStream(response.body()) - if (body.jsonObject["ec"].asInt == 0) { - val list = body.jsonObject["feeds"].asJsonArrayOrNull - ?: return Result.success(ArrayList()) - return Result.success(list.map { - val obj = it.jsonObject - GroupAnnouncement( - senderId = obj["u"].asLong, - publishTime = obj["pubt"].asLong, - message = GroupAnnouncementMessage( -// text = obj["msg"].asJsonObject["text"].asString, - text = fromHtml(obj["msg"].asJsonObject["text"].asString), - images = obj["msg"].asJsonObject["pics"].asJsonArrayOrNull?.map { pic -> - GroupAnnouncementMessageImage( - id = pic.jsonObject["id"].asString, - width = pic.jsonObject["w"].asString, - height = pic.jsonObject["h"].asString, - ) - } ?: ArrayList() - ) - ) - }) - } else { - return Result.failure(Exception(body.jsonObject["em"].asStringOrNull)) - } - } - - private fun fromHtml(htmlString: String): String { - return HtmlCompat - // 特殊处理 ,目的是替换为换行符,否则会被fromHtml忽略并移除 - .fromHtml(htmlString.replace(" ", "[shamrockplaceholder]"), HtmlCompat.FROM_HTML_MODE_LEGACY) - .toString() - .replace("[shamrockplaceholder]", "\n") - } - - @OptIn(ExperimentalSerializationApi::class) - suspend fun uploadImageTroopNotice(image: String): Result { - val file = FileUtils.parseAndSave(image) - val cookie = TicketSvc.getCookie("qun.qq.com") - val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin())) - val response = GlobalClient.post("https://web.qun.qq.com/cgi-bin/announce/upload_img") { - headers { - header("Cookie", cookie) - } - contentType(ContentType.MultiPart.FormData) - setBody(MultiPartFormDataContent( - // 黑人问号 ktor默认formdata传的tx不认。默认是name=bkn,非要写成name="bkn"才认? - formData { - append("filename", "001.png", Headers.build { - append(HttpHeaders.ContentDisposition, "name=\"filename\"") - }) - append("source", "troopNotice", Headers.build { - append(HttpHeaders.ContentDisposition, "name=\"source\"") - }) - append("bkn", bkn, Headers.build { - append(HttpHeaders.ContentDisposition, "name=\"bkn\"") - }) - append("m", "0", Headers.build { - append(HttpHeaders.ContentDisposition, "name=\"m\"") - }) - append("pic_up", file.readBytes(), Headers.build { - append(HttpHeaders.ContentType, "image/png") - append(HttpHeaders.ContentDisposition, "name=\"pic_up\" filename=\"001.png\"") - - }) - } - )) - } - val body = Json.decodeFromStream(response.body()) - if (body.jsonObject["ec"].asInt == 0) { - var idJsonStr = body.jsonObject["id"].asStringOrNull - return if (idJsonStr != null) { - idJsonStr = idJsonStr.replace(""", "\"") - val idJson = Json.decodeFromString(idJsonStr) - LogCenter.log(idJson.toString()) - Result.success(GroupAnnouncementMessageImage( - height = idJson.asJsonObject["h"].asString, - width = idJson.asJsonObject["w"].asString, - id = idJson.asJsonObject["id"].asString, - )) - } else { - Result.failure(Exception("图片上传失败")) - } - } else { - return Result.failure(Exception(body.jsonObject["em"].asStringOrNull)) - } - } - - @OptIn(ExperimentalSerializationApi::class) - suspend fun addQunNotice(groupId: Long, text: String, image: GroupAnnouncementMessageImage?) : Result { - val cookie = TicketSvc.getCookie("qun.qq.com") - val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin())) - val response = GlobalClient.submitForm( - url = "https://web.qun.qq.com/cgi-bin/announce/add_qun_notice", - formParameters = parameters { - append("qid", groupId.toString()) - append("bkn", bkn) - append("text", text) - append("pinned", "0") - append("type", "1") - // todo allow custom settings - append("settings", "{\"is_show_edit_card:\"1,\"tip_window_type\":1,\"confirm_required\":1}") - if (null != image) { - append("pic", image.id) - append("imgWidth", image.width) - append("imgHeight", image.height) - } - }, - block = { - headers { - header("Cookie", cookie) - } - } - ) - val body = Json.decodeFromStream(response.body()) - return if (body.jsonObject["ec"].asInt == 0) { - Result.success(true) - } else { - Result.failure(Exception(body.jsonObject["em"].asStringOrNull)) - } - } - - suspend fun groupSign(groupId: Long): Result { - 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(" ")}") - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/LbsSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/LbsSvc.kt deleted file mode 100644 index 84c5014..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/LbsSvc.kt +++ /dev/null @@ -1,57 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet - -import com.tencent.biz.map.trpcprotocol.LbsSendInfo -import com.tencent.mobileqq.msf.core.MsfCore -import com.tencent.proto.lbsshare.LBSShare -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import moe.fuqiuluo.shamrock.helper.IllegalParamsException -import moe.fuqiuluo.shamrock.tools.slice -import kotlin.math.roundToInt - -internal object LbsSvc: BaseSvc() { - suspend fun tryShareLocation(chatType: Int, peerId: Long, lat: Double, lon: Double): Result { - val req = LbsSendInfo.SendMessageReq() - req.uint64_peer_account.set(peerId) - when (chatType) { - MsgConstant.KCHATTYPEGROUP -> req.enum_relation_type.set(1) - MsgConstant.KCHATTYPEC2C -> req.enum_relation_type.set(0) - else -> error("Not supported chat type: $chatType") - } - req.str_name.set("位置分享") - req.str_address.set(getAddressWithLonLat(lat, lon).onFailure { - return Result.failure(it) - }.getOrNull()) - req.str_lat.set(lat.toString()) - req.str_lng.set(lon.toString()) - sendPb("trpc.qq_lbs.qq_lbs_ark.LocationArk.SsoSendMessage", req.toByteArray(), MsfCore.getNextSeq()) - - return Result.success(Unit) - } - - suspend fun getAddressWithLonLat(lat: Double, lon: Double): Result { - if (lat > 90 || lat < 0) { - return Result.failure(IllegalParamsException("纬度大小错误")) - } - if (lon > 180 || lon < 0) { - return Result.failure(IllegalParamsException("经度大小错误")) - } - val latO = (lat * 1000000).roundToInt() - val lngO = (lon * 1000000).roundToInt() - val req = LBSShare.LocationReq() - req.lat.set(latO) - req.lng.set(lngO) - req.coordinate.set(1) - req.keyword.set("") - req.category.set("") - req.page.set(0) - req.count.set(20) - req.requireMyLbs.set(1) - req.imei.set("") - val buffer = sendBufferAW("LbsShareSvr.location", true, req.toByteArray()) - ?: return Result.failure(Exception("获取位置失败")) - val resp = LBSShare.LocationResp() - resp.mergeFrom(buffer.slice(4)) - val location = resp.mylbs - return Result.success(location.addr.get()) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt deleted file mode 100644 index fda842b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt +++ /dev/null @@ -1,524 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet - -import com.tencent.mobileqq.qroute.QRoute -import com.tencent.mobileqq.troop.api.ITroopMemberNameService -import com.tencent.qqnt.kernel.api.IKernelService -import com.tencent.qqnt.kernel.nativeinterface.* -import com.tencent.qqnt.msg.api.IMsgService -import kotlinx.coroutines.delay -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject -import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment -import moe.fuqiuluo.qqinterface.servlet.msg.toJson -import moe.fuqiuluo.qqinterface.servlet.msg.toListMap -import moe.fuqiuluo.qqinterface.servlet.msg.toSegments -import moe.fuqiuluo.shamrock.helper.ContactHelper -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail -import moe.fuqiuluo.shamrock.remote.service.data.MessageSender -import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.utils.DeflateTools -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import moe.fuqiuluo.shamrock.xposed.helper.msgService -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.auto.toByteArray -import protobuf.message.* -import protobuf.message.longmsg.* -import java.util.* -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -import kotlin.random.Random - -internal object MsgSvc : BaseSvc() { - private suspend fun prepareTempChatFromGroup( - groupId: String, - peerId: String - ): Result { - LogCenter.log("主动临时消息,创建临时会话。", Level.INFO) - val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService - ?: return Result.failure(Exception("获取消息服务失败")) - msgService.prepareTempChat( - TempChatPrepareInfo( - MsgConstant.KCHATTYPETEMPC2CFROMGROUP, - ContactHelper.getUidByUinAsync(peerId = peerId.toLong()), - app.getRuntimeService(ITroopMemberNameService::class.java, "all") - .getTroopMemberNameRemarkFirst(groupId, peerId), - groupId, - EMPTY_BYTE_ARRAY, - app.currentUid, - "", - TempChatGameSession() - ) - ) { code, reason -> - if (code != 0) { - LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR) - } - } - return Result.success(Unit) - } - - suspend fun getTempChatInfo(chatType: Int, uid: String): Result { - val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService - ?: return Result.failure(Exception("获取消息服务失败")) - val info: TempChatInfo = withTimeoutOrNull(5000) { - suspendCancellableCoroutine { - msgService.getTempChatInfo(chatType, uid) { code, msg, tempChatInfo -> - if (code == 0) { - it.resume(tempChatInfo) - } else { - LogCenter.log("获取临时会话信息失败: $code:$msg", Level.ERROR) - it.resume(null) - } - } - } - } ?: return Result.failure(Exception("获取临时会话信息失败")) - return Result.success(info) - } - - /** - * 正常获取 - */ - suspend fun getMsg(hash: Int): Result { - val mapping = MessageHelper.getMsgMappingByHash(hash) - ?: return Result.failure(Exception("没有对应消息映射,消息获取失败")) - - val peerId = mapping.peerId - val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId) - - val msg = withTimeoutOrNull(5000) { - val service = QRoute.api(IMsgService::class.java) - suspendCancellableCoroutine { continuation -> - service.getMsgsByMsgId(contact, arrayListOf(mapping.qqMsgId)) { code, _, msgRecords -> - if (code == 0 && msgRecords.isNotEmpty()) { - continuation.resume(msgRecords.first()) - } else { - continuation.resume(null) - } - } - continuation.invokeOnCancellation { - continuation.resume(null) - } - } - } - - return if (msg != null) { - Result.success(msg) - } else { - Result.failure(Exception("获取消息失败")) - } - } - - suspend fun getMsgByQMsgId( - chatType: Int, - peerId: String, - qqMsgId: Long, - subPeerId: String = "" - ): Result { - val contact = MessageHelper.generateContact(chatType, peerId, subPeerId) - val service = QRoute.api(IMsgService::class.java) - - val msg = withTimeoutOrNull(5000) { - suspendCoroutine { continuation -> - service.getMsgsByMsgId(contact, arrayListOf(qqMsgId)) { code, _, msgRecords -> - if (code == 0 && msgRecords.isNotEmpty()) { - continuation.resume(msgRecords.first()) - } else { - continuation.resume(null) - } - } - } - } - - return if (msg != null) { - Result.success(msg) - } else { - Result.failure(Exception("获取消息失败")) - } - } - - /** - * 什么鸟屎都获取不到 - */ - suspend fun getMsgBySeq( - chatType: Int, - peerId: String, - seq: Long - ): Result { - val contact = MessageHelper.generateContact(chatType, peerId) - val msg = withTimeoutOrNull(1000) { - val service = QRoute.api(IMsgService::class.java) - suspendCancellableCoroutine { continuation -> - service.getMsgsBySeqs(contact, arrayListOf(seq)) { code, _, msgRecords -> - continuation.resume(msgRecords?.firstOrNull()) - } - continuation.invokeOnCancellation { - continuation.resume(null) - } - } - } - return if (msg != null) { - Result.success(msg) - } else { - Result.failure(Exception("获取消息失败")) - } - } - - /** - * 撤回消息 同步 HTTP API - */ - suspend fun recallMsg(msgHash: Int): Pair { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val msgService = sessionService.msgService - - val mapping = MessageHelper.getMsgMappingByHash(msgHash) - ?: return -1 to "无法找到消息映射" - - val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId) - - return suspendCancellableCoroutine { continuation -> - msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why -> - continuation.resume(code to why) - } - } - } - - /** - * 发送消息 - * - * Aio 腾讯内部命名 All In One - */ - suspend fun sendToAio( - chatType: Int, - peedId: String, - message: JsonArray, - fromId: String = peedId, - retryCnt: Int - ): Result { - // 主动临时消息 - when (chatType) { - MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> { - prepareTempChatFromGroup(fromId, peedId).onFailure { - LogCenter.log("主动临时消息,创建临时会话失败。", Level.ERROR) - return Result.failure(Exception("主动临时消息,创建临时会话失败。")) - } - } - } - val result = - MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0)) - .getOrElse { return Result.failure(it) } - return if (result.isTimeout) { - // 发送失败,可能网络问题出现红色感叹号,重试 - // 例如 rich media transfer failed - delay(100) - MessageHelper.resendMsg(chatType, peedId, fromId, result.qqMsgId, retryCnt, result.msgHashId) - } else { - Result.success(result) - } - } - - suspend fun uploadMultiMsg( - chatType: Int, - peerId: String, - fromId: String = peerId, - messages: JsonArray, - retryCnt: Int - ): Result { - return uploadMultiMsg(chatType, peerId, fromId, messages).onFailure { - if (retryCnt > 0) { - return uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt - 1) - } - } - } - - private suspend fun uploadMultiMsg( - chatType: Int, - peerId: String, - fromId: String = peerId, - messages: JsonArray, - ): Result { - var i = -1 - val desc = MutableList(messages.size) { "" } - val forwardMsg = mutableMapOf() - - val msgs = messages.mapNotNull { msg -> - kotlin.runCatching { - val data = msg.asJsonObject["data"].asJsonObject - if (data.containsKey("id")) { - val msgId = data["id"].asInt - val record = getMsg(msgId).onFailure { - error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it") - }.getOrThrow() - PushMsgBody( - msgHead = ResponseHead( - peerUid = record.senderUid, - receiverUid = record.peerUid, - forward = ResponseForward( - friendName = record.sendNickName - ), - responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp( - groupCode = record.peerUin.toULong(), - memberCard = record.sendMemberName, - u1 = 2 - ) else null - ), - contentHead = ContentHead( - msgType = when (record.chatType) { - MsgConstant.KCHATTYPEC2C -> 9 - MsgConstant.KCHATTYPEGROUP -> 82 - else -> throw UnsupportedOperationException("Unsupported chatType: $chatType") - }, - msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, - divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, - msgViaRandom = record.msgId, - sequence = record.msgSeq, // idk what this is(i++) - msgTime = record.msgTime, - u2 = 1, - u6 = 0, - u7 = 0, - msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm - forwardHead = ForwardHead( - u1 = 0, - u2 = 0, - u3 = 0, - ub641 = "", - avatar = "" - ) - ), - body = MsgBody( - richText = MessageHelper.messageArrayToRichText( - record.chatType, - record.msgId, - record.peerUin.toString(), - record.elements.toSegments( - record.chatType, - record.peerUin.toString(), - "0" - ).onEach { segment -> - if (segment.type == "forward") { - forwardMsg[segment.data["filename"] as String] = - segment.data["id"] as String - } - }.toJson() - ).onFailure { - error("消息合成失败: ${it.stackTraceToString()}") - }.onSuccess { - desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first - }.getOrThrow().second - ) - ) - } else if (data.containsKey("content")) { - PushMsgBody( - msgHead = ResponseHead( - peer = data["uin"]?.asLong ?: TicketSvc.getUin().toLong(), - peerUid = data["uid"]?.asString ?: TicketSvc.getUid(), - receiverUid = TicketSvc.getUid(), - forward = ResponseForward( - friendName = data["name"]?.asStringOrNull ?: TicketSvc.getNickname() - ) - ), - contentHead = ContentHead( - msgType = 9, - msgSubType = 175, - divSeq = 175, - msgViaRandom = Random.nextLong(), - sequence = data["seq"]?.asLong ?: Random.nextLong(), - msgTime = data["time"]?.asLong ?: (System.currentTimeMillis() / 1000), - u2 = 1, - u6 = 0, - u7 = 0, - msgSeq = data["seq"]?.asLong ?: Random.nextLong(), - forwardHead = ForwardHead( - u1 = 0, - u2 = 0, - u3 = 2, - ub641 = "", - avatar = "" - ) - ), - body = MsgBody( - richText = MessageHelper.messageArrayToRichText( - chatType = chatType, - msgId = Random.nextLong(), - peerId = data["uin"]?.asString ?: TicketSvc.getUin(), - messageList = when (data["content"]) { - is JsonObject -> listOf(data["content"] as JsonObject).json - is JsonArray -> data["content"] as JsonArray - else -> MessageHelper.decodeCQCode(data["content"].asString) - }.onEach { element -> - val elementData = element.asJsonObject["data"].asJsonObject - if (element.asJsonObject["type"].asString == "forward") { - forwardMsg[elementData["filename"].asString] = - elementData["id"].asString - } - } - ).onSuccess { - desc[++i] = (data["name"].asStringOrNull ?: data["uin"].asStringOrNull - ?: TicketSvc.getNickname()) + ": " + it.first - }.onFailure { - error("消息合成失败: ${it.stackTraceToString()}") - }.getOrThrow().second - ) - ) - } else error("消息节点缺少id或content字段") - }.onFailure { - LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN) - }.getOrNull() - }.ifEmpty { - return Result.failure(Exception("消息节点为空")) - } - - val payload = LongMsgPayload( - action = mutableListOf( - LongMsgAction( - command = "MultiMsg", - data = LongMsgContent( - body = msgs - ) - ) - ).apply { - forwardMsg.map { msg -> - addAll(getMultiMsg(msg.value).getOrElse { return Result.failure(Exception("无法获取嵌套转发消息: $it")) } - .map { action -> - if (action.command == "MultiMsg") LongMsgAction( - command = msg.key, - data = action.data - ) else action - }) - } - } - ) - LogCenter.log({ payload.toByteArray().toHexString() }, Level.DEBUG) - - val req = LongMsgReq( - sendInfo = when (chatType) { - MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo( - type = 1, - uid = LongMsgUid(if(peerId.startsWith("u_")) peerId else ContactHelper.getUidByUinAsync(peerId.toLong()) ), - payload = DeflateTools.gzip(payload.toByteArray()) - ) - MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo( - type = 3, - uid = LongMsgUid(fromId), - groupUin = fromId.toULong(), - payload = DeflateTools.gzip(payload.toByteArray()) - ) - else -> throw UnsupportedOperationException("Unsupported chatType: $chatType") - }, - setting = LongMsgSettings( - field1 = 4, - field2 = 2, - field3 = 9, - field4 = 0 - ) - ).toByteArray() - - val buffer = sendBufferAW("trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", true, req, timeout = 30_000) - ?: return Result.failure(Exception("unable to upload multi message, response timeout")) - val rsp = runCatching { - buffer.slice(4).decodeProtobuf() - }.getOrElse { - buffer.decodeProtobuf() - } - val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message")) - return Result.success(MessageSegment( - type = "forward", - data = mapOf( - "id" to resId, - "filename" to UUID.randomUUID().toString(), - "summary" to "查看${desc.size}条转发消息", - "desc" to desc.slice(0..if (i < 3) i else 3).joinToString("\n") - ) - )) - } - - suspend fun getMultiMsg(resId: String): Result> { - val req = LongMsgReq( - recvInfo = RecvLongMsgInfo( - uid = LongMsgUid(TicketSvc.getUid()), - resId = resId, - u1 = 3 - ), - setting = LongMsgSettings( - field1 = 2, - field2 = 2, - field3 = 9, - field4 = 0 - ) - ) - val buffer = sendBufferAW( - "trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg", - true, - req.toByteArray() - ) ?: return Result.failure(Exception("unable to get multi message")) - val rsp = buffer.slice(4).decodeProtobuf() - val zippedPayload = DeflateTools.ungzip( - rsp.recvResult?.payload ?: return Result.failure(Exception("payload is empty")) - ) - LogCenter.log(zippedPayload.toHexString(), Level.DEBUG) - return Result.success( - zippedPayload.decodeProtobuf().action - ?: return Result.failure(Exception("action is empty")) - ) - } - - suspend fun getForwardMsg(resId: String): Result> { - val result = getMultiMsg(resId).getOrElse { return Result.failure(it) } - result.forEach { - if (it.command == "MultiMsg") { - return Result.success(it.data?.body?.map { msg -> - val chatType = - if (msg.contentHead!!.msgType == 82) MsgConstant.KCHATTYPEGROUP else MsgConstant.KCHATTYPEC2C - MessageDetail( - time = msg.contentHead?.msgTime?.toInt() ?: 0, - msgType = MessageHelper.obtainDetailTypeByMsgType(chatType), - msgId = 0, // msgViaRandom为空 tx不给 - qqMsgId = 0, - msgSeq = msg.contentHead!!.msgSeq ?: 0, - realId = msg.contentHead!!.msgSeq ?: 0, - sender = MessageSender( - msg.msgHead?.peer ?: 0, - msg.msgHead?.responseGrp?.memberCard ?: msg.msgHead?.forward?.friendName ?: "", - "unknown", - 0, - msg.msgHead?.peerUid ?: "", - msg.msgHead?.peerUid ?: "" - ), - message = msg.body?.richText?.toSegments( - chatType, - msg.msgHead?.peer.toString(), - "0" - )?.toListMap() ?: emptyList(), - peerId = msg.msgHead?.peer ?: 0, - groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong() - ?: 0 else 0, - targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 0 else 0 - ) - } ?: return Result.failure(Exception("Msg is empty"))) - } - } - return Result.failure(Exception("Can't find msg")) - } - - class MessageCallback( - private val peerId: String, - var msgHash: Int - ) : IOperateCallback { - override fun onResult(code: Int, reason: String?) { - if (code != 0 && msgHash != 0) { - MessageHelper.removeMsgByHashCode(msgHash) - } - when (code) { - 110 -> LogCenter.log("消息发送: $peerId, 无该联系人无法发送。") - 120 -> LogCenter.log("消息发送: $peerId, 禁言状态无法发送。") - 5 -> LogCenter.log("消息发送: $peerId, 当前不支持该消息类型。") - else -> LogCenter.log("消息发送: $peerId, code: $code $reason") - } - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt deleted file mode 100644 index d74bf4e..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt +++ /dev/null @@ -1,112 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet - -import com.tencent.qqnt.kernel.nativeinterface.Contact -import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgService -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import io.ktor.utils.io.core.BytePacketBuilder -import io.ktor.utils.io.core.readBytes -import io.ktor.utils.io.core.writeFully -import io.ktor.utils.io.core.writeInt -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler -import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg -import moe.fuqiuluo.shamrock.tools.broadcast -import moe.fuqiuluo.shamrock.utils.DeflateTools -import mqq.app.MobileQQ -import protobuf.auto.toByteArray -import protobuf.message.* -import protobuf.message.element.LightAppElem -import protobuf.push.MessagePush -import kotlin.coroutines.resume -import kotlin.text.toByteArray - -internal object PacketSvc : BaseSvc() { - /** - * 伪造收到Json卡片消息 - */ - suspend fun fakeSelfRecvJsonMsg(msgService: IKernelMsgService, content: String): Long { - return fakeReceiveSelfMsg(msgService) { - listOf( - Elem( - lightApp = LightAppElem((byteArrayOf(1) + DeflateTools.compress(content.toByteArray()))) - ) - ) - } - } - - private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> List): Long { - val latestMsg = withTimeoutOrNull(3000) { - suspendCancellableCoroutine { - msgService.getMsgs( - Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""), - 0L, - 1, - true - ) { code, why, msgs -> - it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs)) - } - } - }?.data?.firstOrNull() - val msgSeq = (latestMsg?.msgSeq ?: 0) + 1 - - val msgPush = MessagePush( - msgBody = PushMsgBody( - msgHead = ResponseHead( - peer = app.longAccountUin, - peerUid = app.currentUid, - flag = 1001, - receiver = app.longAccountUin, - receiverUid = app.currentUid - ), - contentHead = ContentHead( - msgType = 166, - msgSubType = 11, - msgSeq = msgSeq, - msgViaRandom = msgSeq, - msgTime = System.currentTimeMillis() / 1000, - u2 = 1, - sequence = msgSeq, - msgRandom = msgService.getMsgUniqueId(System.currentTimeMillis()), - u4 = msgSeq - 2, - u5 = msgSeq - ), - body = MsgBody( - RichText( - elements = builder() - ) - ) - ) - ) - - fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, msgPush.toByteArray()) - return withTimeoutOrNull(5000L) { - suspendCancellableCoroutine { - MessageTempHandler.registerTemporaryMsgListener(msgSeq) { - it.resume(this.msgId) - } - it.invokeOnCancellation { - MessageTempHandler.unregisterTemporaryMsgListener(msgSeq) - } - } - } ?: -1L - } - - /** - * 伪造QQ收到某个包 - */ - private fun fakeReceive(cmd: String, seq: Int, buffer: ByteArray) { - MobileQQ.getContext().broadcast("msf") { - putExtra("__cmd", "fake_packet") - putExtra("package_cmd", cmd) - putExtra("package_uin", app.currentUin) - putExtra("package_seq", seq) - val wupBuffer = BytePacketBuilder().apply { - writeInt(buffer.size + 4) - writeFully(buffer) - }.build() - putExtra("package_buffer", wupBuffer.readBytes()) - wupBuffer.release() - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QFavSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QFavSvc.kt deleted file mode 100644 index 9b8bd0f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QFavSvc.kt +++ /dev/null @@ -1,402 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) - -package moe.fuqiuluo.qqinterface.servlet - -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 kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.encodeToByteArray - -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 protobuf.fav.WeiyunAddRichMediaReq -import protobuf.fav.WeiyunAuthor -import protobuf.fav.WeiyunCollectCommInfo -import protobuf.fav.WeiyunComm -import protobuf.fav.WeiyunCommonReq -import protobuf.fav.WeiyunFastUploadResourceReq -import protobuf.fav.WeiyunGetFavContentReq -import protobuf.fav.WeiyunGetFavListReq -import protobuf.fav.WeiyunMsgHead -import protobuf.fav.WeiyunPicInfo -import protobuf.fav.WeiyunRichMediaContent -import protobuf.fav.WeiyunRichMediaSummary -import mqq.manager.TicketManager -import oicq.wlogin_sdk.request.Ticket -import oicq.wlogin_sdk.request.WtTicketPromise -import oicq.wlogin_sdk.tools.ErrMsg -import protobuf.auto.toByteArray -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 { - val baseReq = WeiyunCommonReq( - getFavListReq = WeiyunGetFavListReq( - type = 0u, - bid = 0u, - category = category.toUInt(), - startTime = 0u, - orderType = 0u, - startPos = startPos.toUInt(), - pageSize = pageSize.toUInt(), - syncPolicy = 0u, - reqSource = 0u - ) - ) - return sendWeiyunReq(20000, baseReq) - } - - suspend fun getItemContent( - id: String - ): Result { - return sendWeiyunReq(20001, WeiyunCommonReq( - getFavContentReq = WeiyunGetFavContentReq( - cidList = arrayListOf(id) - ) - ) - ) - } - - 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 { - val md5Bytes = md5.hex2ByteArray() - return sendWeiyunReq(20009, WeiyunCommonReq( - addRichMediaReq = WeiyunAddRichMediaReq( - commInfo = WeiyunCollectCommInfo( - bid = 1u, - category = 1u, - author = WeiyunAuthor( - type = if (groupId == 0L) 1u else 2u, - numId = uin.toULong(), - strId = name, - groupId = groupId.toULong(), - groupName = groupName - ), - createTime = System.currentTimeMillis().toULong() - 2000u, - seq = System.currentTimeMillis().toULong() - 1000u, - bizDataList = arrayListOf("""{"recordAudioOnly":false,"audioOnly":false,"fileOnly":false}""".toByteArray()), - originalAppId = 0u, - customGroupId = 0u - ), - summary = WeiyunRichMediaSummary( - title = "", - brief = "[图片]", - picList = arrayListOf( - WeiyunPicInfo( - uri = picUrl, - md5 = md5Bytes, - sha1 = md5.toByteArray(), - name = "", - note = "", - width = width.toUInt(), - height = height.toUInt(), - size = size.toULong(), - type = 0u, - picId = pid - ) - ), - contentType = 1u - ), - richMediaContent = listOf( - WeiyunRichMediaContent( - rawData = """""".toByteArray(), - picList = listOf( - WeiyunPicInfo( - uri = picUrl, - md5 = md5Bytes, - sha1 = md5.toByteArray(), - name = "", - note = "", - width = width.toUInt(), - height = height.toUInt(), - size = size.toULong(), - type = 0u, - picId = pid - ) - ) - ) - ) - ) - ) - ) - } - - suspend fun applyUpImageMsg( - uin: Long, - name: String, - groupId: Long = 0, - groupName: String = "", - width: Int, height: Int, - image: File - ): Result { - if (!image.exists()) { - return Result.failure(IllegalArgumentException("image file not exists")) - } - val md5 = MD5.genFileMd5(image.absolutePath) - return sendWeiyunReq(20010, WeiyunCommonReq( - fastUploadResourceReq = WeiyunFastUploadResourceReq( - picInfoList = listOf( - WeiyunPicInfo( - md5 = md5, - name = md5.toHexString(), - width = width.toUInt(), - height = height.toUInt(), - size = image.length().toULong(), - type = 1u, - picId = "/storage/emulated/0/DCIM/temp.jpeg", - owner = WeiyunAuthor( - type = if (groupId == 0L) 1u else 2u, - numId = uin.toULong(), - strId = name, - groupId = groupId.toULong(), - groupName = groupName - ) - ) - ), - ) - ) - ) - } - - suspend fun addRichMediaMsg( - uin: Long, - name: String, - groupId: Long = 0, - groupName: String = "", - time: Long = System.currentTimeMillis(), - content: String - ): Result { - return sendWeiyunReq(20009, WeiyunCommonReq( - addRichMediaReq = WeiyunAddRichMediaReq( - commInfo = WeiyunCollectCommInfo( - bid = 1u, - category = 1u, - author = WeiyunAuthor( - type = if (groupId == 0L) 1u else 2u, - numId = uin.toULong(), - strId = name, - groupId = groupId.toULong(), - groupName = groupName - ), - createTime = time.toULong() - 2000u, - seq = time.toULong() - 1000u, - originalAppId = 0u, - customGroupId = 0u - ), - summary = WeiyunRichMediaSummary( - brief = content, - contentType = 1u - ), - richMediaContent = listOf( - WeiyunRichMediaContent( - rawData = content.textToHtml().toByteArray(), - ) - ) - ) - ) - ) - } - - private fun String.textToHtml(): String { - return replace("\n", "

") - } - - suspend fun sendPicUpBlock( - fileSize: Long, - offset: Long, - block: ByteArray, - blockSize: Long, - sha: ByteArray, - pid: String, - outputStream: ByteArrayOutputStream = ByteArrayOutputStream(), - ): Result { - 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, - req: WeiyunCommonReq, - outputStream: ByteArrayOutputStream = ByteArrayOutputStream(), - ): Result { - 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), WeiyunComm( - req = req - ).toByteArray())) - 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 { - return WeiyunMsgHead( - uin = app.longAccountUin.toULong(), - seq = seq++.toUInt(), - type = 1u, - cmd = cmd.toUInt(), - appId = APPID.toUInt(), - version = VERSION.toUInt(), - netType = 3u, - keyType = 27u, - key = pskey.toByteArray(), - majorVersion = MAJOR_VERSION.toUInt(), - minorVersion = MINOR_VERSION.toUInt(), - ).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) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QSafeSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QSafeSvc.kt deleted file mode 100644 index 3f7e9f0..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QSafeSvc.kt +++ /dev/null @@ -1,33 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet - -import QQService.SvcDevLoginInfo -import QQService.SvcReqGetDevLoginInfo -import QQService.SvcRspGetDevLoginInfo -import com.qq.jce.wup.UniPacket -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import mqq.app.MobileQQ -import mqq.app.Packet -import oicq.wlogin_sdk.tools.util - -internal object QSafeSvc: BaseSvc() { - - suspend fun getOnlineClients(): ArrayList? { - val req = SvcReqGetDevLoginInfo() - req.vecGuid = util.getGuidFromFile(MobileQQ.getContext()) - req.strAppName = MobileQQ.getMobileQQ().qqProcessName.split(":")[0] - req.iLoginType = 1 - req.iRequireMax = 20 - req.iGetDevListType = 6 - - val uniPacket = UniPacket() - uniPacket.servantName = "StatSvc" - uniPacket.funcName = "SvcReqGetDevLoginInfo" - uniPacket.put("SvcReqGetDevLoginInfo", req) - val resp = sendBufferAW("StatSvc.GetDevLoginInfo", false, uniPacket.encode()) - ?: return null - - return Packet.decodePacket(resp, "SvcRspGetDevLoginInfo", SvcRspGetDevLoginInfo()).vecCurrentLoginDevInfo - } - - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt deleted file mode 100644 index 415bf8f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt +++ /dev/null @@ -1,179 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet - -import com.tencent.guild.api.transfile.IGuildTransFileApi -import com.tencent.mobileqq.app.QQAppInterface -import com.tencent.mobileqq.pskey.oidb.cmd0x102a.oidb_cmd0x102a -import com.tencent.mobileqq.qroute.QRoute -import io.ktor.client.request.get -import io.ktor.client.request.header -import moe.fuqiuluo.shamrock.remote.service.data.BigDataTicket -import moe.fuqiuluo.shamrock.tools.GlobalClientNoRedirect -import moe.fuqiuluo.shamrock.tools.slice -import mqq.app.MobileQQ -import mqq.manager.TicketManager -import oicq.wlogin_sdk.request.Ticket -import tencent.im.oidb.oidb_sso - -internal object TicketSvc: BaseSvc() { - object SigType { - const val WLOGIN_A5 = 2 - const val WLOGIN_RESERVED = 16 - const val WLOGIN_STWEB = 32 // TLV 103 - const val WLOGIN_A2 = 64 - const val WLOGIN_ST = 128 - const val WLOGIN_AQSIG = 2097152 - const val WLOGIN_D2 = 262144 - const val WLOGIN_DA2 = 33554432 - const val WLOGIN_LHSIG = 4194304 - const val WLOGIN_LSKEY = 512 - const val WLOGIN_OPENKEY = 16384 - const val WLOGIN_PAYTOKEN = 8388608 - const val WLOGIN_PF = 16777216 - const val WLOGIN_PSKEY = 1048576 - const val WLOGIN_PT4Token = 134217728 - const val WLOGIN_QRPUSH = 67108864 - const val WLOGIN_SID = 524288 - const val WLOGIN_SIG64 = 8192 - const val WLOGIN_SKEY = 4096 - const val WLOGIN_TOKEN = 32768 - const val WLOGIN_VKEY = 131072 - - val ALL_TICKET = arrayOf( - WLOGIN_A5, WLOGIN_RESERVED, WLOGIN_STWEB, WLOGIN_A2, WLOGIN_ST, WLOGIN_AQSIG, WLOGIN_D2, WLOGIN_DA2, - WLOGIN_LHSIG, WLOGIN_LSKEY, WLOGIN_OPENKEY, WLOGIN_PAYTOKEN, WLOGIN_PF, WLOGIN_PSKEY, WLOGIN_PT4Token, - WLOGIN_QRPUSH, WLOGIN_SID, WLOGIN_SIG64, WLOGIN_SKEY, WLOGIN_TOKEN, WLOGIN_VKEY - ) - } - - fun getUin(): String { - return app.currentUin.ifBlank { "0" } - } - - fun getLongUin(): Long { - return app.longAccountUin - } - - fun getUid(): String { - return app.currentUid.ifBlank { "u_" } - } - - fun getNickname(): String { - return app.currentNickname - } - - fun getCookie(): String { - val uin = getUin() - val skey = getRealSkey(uin) - val pskey = getPSKey(uin) - return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey" - } - - suspend fun getCookie(domain: String): String { - val uin = getUin() - val skey = getRealSkey(uin) - val pskey = getPSKey(uin, domain) ?: "" - val pt4token = getPt4Token(uin, domain) ?: "" - return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token" - } - - fun getBigdataTicket(): BigDataTicket? { - return runCatching { - QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let { - BigDataTicket(it.getSessionKey(), it.getSessionSig()) - } - }.getOrNull() - } - - fun getCSRF(pskey: String = getPSKey(getUin())): String { - if (pskey.isEmpty()) { - return "0" - } - var v = 5381 - for (element in pskey) { - v += ((v shl 5) + element.code.toLong()).toInt() - } - return (v and Int.MAX_VALUE).toString() - } - - suspend fun getCSRF(uin: String, domain: String): String { - // 是不是要用Skey? - return getBkn(getPSKey(uin, domain) ?: getSKey(uin)) - } - - fun getBkn(arg: String): String { - var v: Long = 5381 - for (element in arg) { - v += (v shl 5 and 2147483647L) + element.code.toLong() - } - return (v and 2147483647L).toString() - } - - fun getTicket(uin: String, id: Int): Ticket? { - return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getLocalTicket(uin, id) - } - - fun getStWeb(uin: String): String { - return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getStweb(uin) - } - - fun getSKey(uin: String): String { - return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getSkey(uin) - } - - fun getRealSkey(uin: String): String { - return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getRealSkey(uin) - } - - fun getPSKey(uin: String): String { - val manager = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager) - manager.reloadCache(MobileQQ.getContext()) - return manager.getSuperkey(uin) ?: "" - } - - suspend fun getLessPSKey(vararg domain: String): Result> { - val req = oidb_cmd0x102a.GetPSkeyRequest() - req.domains.set(domain.toList()) - val buffer = sendOidbAW("OidbSvcTcp.0x102a", 4138, 0, req.toByteArray()) - ?: return Result.failure(Exception("getLessPSKey failed")) - val body = oidb_sso.OIDBSSOPkg() - body.mergeFrom(buffer.slice(4)) - val rsp = oidb_cmd0x102a.GetPSkeyResponse().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) - return Result.success(rsp.private_keys.get()) - } - - suspend fun getPSKey(uin: String, domain: String): String? { - return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPskey(uin, domain).let { - if (it.isNullOrBlank()) - getLessPSKey(domain).getOrNull()?.firstOrNull()?.key?.get() - else it - } - } - - fun getPt4Token(uin: String, domain: String): String? { - return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPt4Token(uin, domain) - } - - suspend fun GetHttpCookies(appid: String, daid: String, jumpurl: String): String? { - val uin = getUin() - val clientkey = getStWeb(uin) - var url = "https://ui.ptlogin2.qq.com/cgi-bin/login?pt_hide_ad=1&style=9&appid=$appid&pt_no_auth=1&pt_wxtest=1&daid=$daid&s_url=$jumpurl" - var cookie = GlobalClientNoRedirect.get(url).headers.getAll("Set-Cookie")?.joinToString(";") - url = "https://ssl.ptlogin2.qq.com/jump?u1=$jumpurl&pt_report=1&daid=$daid&style=9&keyindex=19&clientuin=$uin&clientkey=$clientkey" - GlobalClientNoRedirect.get(url) { - header("Cookie", cookie) - }.let { - cookie = it.headers.getAll("Set-Cookie")?.joinToString(";") - url = it.headers["Location"].toString() - } - cookie = GlobalClientNoRedirect.get(url).headers.getAll("Set-Cookie")?.joinToString(";") - val extractedCookie = StringBuilder() - val cookies = cookie?.split(";") - cookies?.filter { cookie -> - val cookiePair = cookie.trim().split("=") - cookiePair.size == 2 && cookiePair[1].isNotBlank() && cookiePair[0].trim() in listOf("uin", "skey", "p_uin", "p_skey", "pt4_token") - }?.forEach { - extractedCookie.append("$it; ") - } - return extractedCookie.toString().trim() - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/VisitorSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/VisitorSvc.kt deleted file mode 100644 index 57f6dc5..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/VisitorSvc.kt +++ /dev/null @@ -1,95 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet - -internal object VisitorSvc: BaseSvc() { - const val FROM_C2C_AIO = 2 - const val FROM_CONDITION_SEARCH = 9 - const val FROM_CONTACTS_TAB = 5 - const val FROM_FACE_2_FACE_ADD_FRIEND = 11 - const val FROM_MAYKNOW_FRIEND = 3 - const val FROM_QCIRCLE = 4 - const val FROM_QQ_TROOP = 1 - const val FROM_QZONE = 7 - const val FROM_SCAN = 6 - const val FROM_SEARCH = 8 - const val FROM_SETTING_ME = 12 - const val FROM_SHARE_CARD = 10 - - const val IS_BLACK_LIST = "is_blacklist_user_profile" - const val PROFILE_CARD_IS_BLACK = 2 - const val PROFILE_CARD_IS_BLACKED = 1 - const val PROFILE_CARD_NOT_BLACK = 3 - - const val SUB_FROM_C2C_AIO = 21 - const val SUB_FROM_C2C_INTERACTIVE_LOGO = 25 - const val SUB_FROM_C2C_LEFT_SLIDE = 23 - const val SUB_FROM_C2C_OTHER = 24 - const val SUB_FROM_C2C_SETTING = 22 - const val SUB_FROM_C2C_TOFU = 26 - const val SUB_FROM_CONDITION_SEARCH_OTHER = 99 - const val SUB_FROM_CONDITION_SEARCH_RESULT = 91 - const val SUB_FROM_CONTACTS_FRIEND_TAB = 51 - const val SUB_FROM_CONTACTS_TAB = 55 - const val SUB_FROM_FACE_2_FACE_ADD_FRIEND_RESULT_AVATAR = 111 - const val SUB_FROM_FACE_2_FACE_OTHER = 119 - const val SUB_FROM_FRIEND_APPLY = 56 - const val SUB_FROM_FRIEND_NOTIFY_MORE = 57 - const val SUB_FROM_FRIEND_NOTIFY_TAB = 54 - const val SUB_FROM_GROUPING_TAB = 52 - const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB = 31 - const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB_MORE = 37 - const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE = 34 - const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_MORE = 39 - const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_SEARCH = 36 - const val SUB_FROM_MAYKNOW_FRIEND_NEW_FRIEND_PAGE = 32 - const val SUB_FROM_MAYKNOW_FRIEND_OTHER = 35 - const val SUB_FROM_MAYKNOW_FRIEND_SEARCH = 33 - const val SUB_FROM_MAYKNOW_FRIEND_SEARCH_MORE = 38 - const val SUB_FROM_PHONE_LIST_TAB = 53 - const val SUB_FROM_QCIRCLE_OTHER = 42 - const val SUB_FROM_QCIRCLE_PROFILE = 41 - const val SUB_FROM_QQ_TROOP_ACTIVE_MEMBER = 15 - const val SUB_FROM_QQ_TROOP_ADMIN = 16 - const val SUB_FROM_QQ_TROOP_AIO = 11 - const val SUB_FROM_QQ_TROOP_MEMBER = 12 - const val SUB_FROM_QQ_TROOP_OTHER = 14 - const val SUB_FROM_QQ_TROOP_SETTING_MEMBER_LIST = 17 - const val SUB_FROM_QQ_TROOP_TEMP_SESSION = 13 - const val SUB_FROM_QRCODE_SCAN_DRAWER = 64 - const val SUB_FROM_QRCODE_SCAN_NEW = 61 - const val SUB_FROM_QRCODE_SCAN_OLD = 62 - const val SUB_FROM_QRCODE_SCAN_OTHER = 69 - const val SUB_FROM_QRCODE_SCAN_PROFILE = 63 - const val SUB_FROM_QZONE_HOME = 71 - const val SUB_FROM_QZONE_OTHER = 79 - const val SUB_FROM_SEARCH_CONTACT_TAB_MORE_FIND_PROFILE = 83 - const val SUB_FROM_SEARCH_FIND_PROFILE_TAB = 82 - const val SUB_FROM_SEARCH_MESSAGE_TAB_MORE_FIND_PROFILE = 84 - const val SUB_FROM_SEARCH_NEW_FRIEND_MORE_FIND_PROFILE = 85 - const val SUB_FROM_SEARCH_OTHER = 89 - const val SUB_FROM_SEARCH_TAB = 81 - const val SUB_FROM_SETTING_ME_AVATAR = 121 - const val SUB_FROM_SETTING_ME_OTHER = 129 - const val SUB_FROM_SHARE_CARD_C2C = 101 - const val SUB_FROM_SHARE_CARD_OTHER = 109 - const val SUB_FROM_SHARE_CARD_TROOP = 102 - const val SUB_FROM_TYPE_DEFAULT = 0 - - suspend fun vote(target: Long, count: Int): Result { - if(count !in 1 .. 20) { - return Result.failure(IllegalArgumentException("vote count must be in 1 .. 20")) - } - val card = CardSvc.getProfileCard(target).onFailure { - return Result.failure(RuntimeException("unable to fetch contact info")) - }.getOrThrow() - sendExtra("VisitorSvc.ReqFavorite") { - it.putLong(moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileProtocolConst.PARAM_SELF_UIN, currentUin.toLong()) - it.putLong(moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileProtocolConst.PARAM_TARGET_UIN, target) - it.putByteArray("vCookies", card.vCookies) - it.putBoolean("nearby_people", true) - it.putInt("favoriteSource", FROM_CONTACTS_TAB) - it.putInt("iCount", count) - it.putInt("from", FROM_CONTACTS_TAB) - } - return Result.success(Unit) - } -} diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/ArkMsgSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/ArkMsgSvc.kt deleted file mode 100644 index 0a3c47d..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/ArkMsgSvc.kt +++ /dev/null @@ -1,94 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.ark - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo -import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77 - -internal object ArkMsgSvc: BaseSvc() { - fun tryShareMusic( - chatType: Int, - peerId: Long, - msgId: Long, - arkAppInfo: ArkAppInfo, - title: String, - singer: String, - jumpUrl: String, - previewUrl: String, - musicUrl: String, - ) { - val req = oidb_cmd0xb77.ReqBody() - req.appid.set(arkAppInfo.appId) - req.app_type.set(1) - req.msg_style.set(4) - req.client_info.set(oidb_cmd0xb77.ClientInfo().also { - it.platform.set(1) - it.sdk_version.set(arkAppInfo.version) - it.android_package_name.set(arkAppInfo.packageName) - it.android_signature.set(arkAppInfo.signature) - }) - req.ext_info.set(oidb_cmd0xb77.ExtInfo().also { - it.msg_seq.set(msgId) - }) - req.recv_uin.set(peerId) - req.rich_msg_body.set(oidb_cmd0xb77.RichMsgBody().also { - it.title.set(title) - it.summary.set(singer) - it.url.set(jumpUrl) - it.picture_url.set(previewUrl) - it.music_url.set(musicUrl) - }) - when (chatType) { - MsgConstant.KCHATTYPEGROUP -> req.send_type.set(1) - MsgConstant.KCHATTYPEC2C -> req.send_type.set(0) - else -> error("不支持该聊天类型发送音乐分享") - } - sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray()) - } - - /* - suspend fun tryShareJsonMessage( - jsonString: String, - arkAppInfo: ArkAppInfo = ArkAppInfo.DanMaKu, - ): Result { - val msgSeq = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEC2C).qqMsgId - val req = oidb_cmd0xb77.ReqBody() - req.appid.set(arkAppInfo.appId) - req.app_type.set(1) - req.msg_style.set(10) - req.client_info.set(oidb_cmd0xb77.ClientInfo().also { - it.platform.set(1) - it.sdk_version.set(arkAppInfo.version) - it.android_package_name.set(arkAppInfo.packageName) - it.android_signature.set(arkAppInfo.signature) - }) - req.ext_info.set(oidb_cmd0xb77.ExtInfo().also { - it.tag_name.set(ByteStringMicro.copyFromUtf8("shamrock")) - it.msg_seq.set(msgSeq) - }) - req.send_type.set(0) - req.recv_uin.set(TicketSvc.getLongUin()) - req.mini_app_msg_body.set(oidb_cmd0xb77.MiniAppMsgBody().also { - it.mini_app_appid.set(arkAppInfo.miniAppId) - it.mini_app_path.set("pages") - it.web_page_url.set("https://im.qq.com/index/") - it.title.set("title") - it.desc.set("desc") - it.json_str.set(jsonString) - }) - sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray()) - val signedJson: String = withTimeoutOrNull(5.seconds) { - suspendCancellableCoroutine { - AioListener.registerTemporaryMsgListener(msgSeq) { - it.resume(elements.first { - it.elementType == MsgConstant.KELEMTYPEARKSTRUCT - }.arkElement.bytesData) - } - it.invokeOnCancellation { - AioListener.unregisterTemporaryMsgListener(msgSeq) - } - } - } ?: return Result.failure(Exception("unable to sign json")) - return Result.success(signedJson) - }*/ -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/LightAppSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/LightAppSvc.kt deleted file mode 100644 index 6f35f45..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/LightAppSvc.kt +++ /dev/null @@ -1,45 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.ark - -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.auto.toByteArray -import protobuf.lightapp.AdaptShareInfoReq -import protobuf.lightapp.AdaptShareInfoResp -import protobuf.qweb.DEFAULT_DEVICE_INFO -import protobuf.qweb.QWebReq -import protobuf.qweb.QWebRsp - -internal object LightAppSvc: BaseSvc() { - suspend fun adaptShareJumpUrl( - arkAppInfo: ArkAppInfo, - coverUrl: String, - desc: String, - url: String - ): Result { - val rsp = sendBufferAW("LightAppSvc.mini_app_share.AdaptShareInfo", true, QWebReq( - seq = 10, - qua = PlatformUtils.getQUA(), - deviceInfo = DEFAULT_DEVICE_INFO, - buffer = AdaptShareInfoReq( - appid = arkAppInfo.miniAppId.toString(), - title = arkAppInfo.appName, - desc = desc, - time = (System.currentTimeMillis() * 0.001).toULong(), - scene = 3u, - templetType = 1u, - businessType = 0u, - picUrl = coverUrl, - jumpUrl = "pages", - verType = 3u, - withShareTicket = 0u, - webURL = url, - ).toByteArray(), - traceId = app.account + "_0_0", - ).toByteArray())?.decodeProtobuf()?.buffer?.decodeProtobuf() - if (rsp == null || rsp.json.isNullOrEmpty()) - return Result.failure(Exception("unable to adapt ShareInfo")) - return Result.success(rsp.json!!) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/WeatherSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/WeatherSvc.kt deleted file mode 100644 index 92ee562..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/WeatherSvc.kt +++ /dev/null @@ -1,74 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.ark - -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.request.url -import io.ktor.client.statement.bodyAsText -import io.ktor.http.HttpStatusCode -import io.ktor.http.encodeURLQueryComponent -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.qqinterface.servlet.ark.data.Region -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.tools.* -import java.lang.Exception - -internal object WeatherSvc { - suspend fun fetchWeatherCard(code: Int): Result { - val cookie = TicketSvc.getCookie("mp.qq.com") - val resp = GlobalClient.get("https://weather.mp.qq.com/page/poster?_wv=2&&_wwv=4&adcode=$code") { - header("Cookie", cookie) - } - - if (resp.status != HttpStatusCode.OK) { - LogCenter.log("fetchWeatherCard: error: ${resp.status}, cookie: $cookie", Level.ERROR) - return Result.failure(Exception("search city failed")) - } - - val textJson = resp.bodyAsText() - .replace("\n", "") - .split("window.__INITIAL_STATE__ =")[1] - .split("};")[0].trim() + "}" - - //LogCenter.log(textJson) - - return Result.success(Json.parseToJsonElement(textJson).asJsonObject) - } - - suspend fun searchCity(query: String): Result> { - val pskey = TicketSvc.getPSKey(TicketSvc.getUin(), "mp.qq.com") ?: "" - val cookie = TicketSvc.getCookie("mp.qq.com") - val gtk = TicketSvc.getCSRF(pskey) - val resp = GlobalClient.get { - url("https://weather.mp.qq.com/trpc/weather/SearchRegions?g_tk=$gtk&key=${query.encodeURLQueryComponent()}&offset=0&count=25") - header("Cookie", cookie) - } - - if (resp.status != HttpStatusCode.OK) { - LogCenter.log("GetWeatherCityCode: error: ${resp.status}, cookie: $cookie, bkn: $gtk", Level.ERROR) - return Result.failure(Exception("search city failed")) - } - - val json = GlobalJson.parseToJsonElement(resp.bodyAsText()).asJsonObject - - - val cnt = json["totalCount"].asInt - if (cnt == 0) { - return Result.success(emptyList()) - } - - val regions = json["regions"].asJsonArray.map { - val region = it.asJsonObject - Region( - region["adcode"].asInt, - region["province"].asStringOrNull, - region["city"].asStringOrNull - ) - } - - return Result.success(regions) - } - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/ArkAppInfo.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/ArkAppInfo.kt deleted file mode 100644 index 8b950ef..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/ArkAppInfo.kt +++ /dev/null @@ -1,40 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.ark.data - -sealed class ArkAppInfo( - val appId: Long, - val version: String, - val packageName: String, - val signature: String, - val miniAppId: Long = 0, - val appName: String = "" -) { - data object QQMusic: ArkAppInfo( - appId = 100497308, - version = "0.0.0", - packageName = "com.tencent.qqmusic", - signature = "cbd27cd7c861227d013a25b2d10f0799" - ) - data object NetEaseMusic: ArkAppInfo( - appId = 100495085, - version = "0.0.0", - packageName = "com.netease.cloudmusic", - signature = "da6b069da1e2982db3e386233f68d76d" - ) - - data object DanMaKu: ArkAppInfo( - appId = 100951776, - version = "0.0.0", - packageName = "tv.danmaku.bili", - signature = "7194d531cbe7960a22007b9f6bdaa38b", - miniAppId = 1109937557, - appName = "哔哩哔哩" - ) - - data object Docs: ArkAppInfo( - appId = 0, - version = "0.0.0", - packageName = "", - signature = "f3da3147654d9a21f3237b88f20dce9c", - miniAppId = 1108338344 - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/Region.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/Region.kt deleted file mode 100644 index 6f96120..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/Region.kt +++ /dev/null @@ -1,10 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.ark.data - -import kotlinx.serialization.Serializable - -@Serializable -internal data class Region( - val adcode: Int, - val province: String?, - val city: String? -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt deleted file mode 100644 index 9f647ac..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt +++ /dev/null @@ -1,112 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.msg - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgElement -import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter -import moe.fuqiuluo.qqinterface.servlet.msg.converter.NtMsgElementConverter -import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.tools.toHexString -import protobuf.message.Elem -import protobuf.message.RichText - -@JvmName("richTextToSegments") -internal suspend fun RichText.toSegments( - chatType: Int, - peerId: String, - subPeer: String -): List { - val messageData = arrayListOf() - if (ptt != null) { - val md5 = ptt!!.fileMd5!! - messageData.add( - MessageSegment( - "record", mapOf( - "file" to md5.toHexString(), - "url" to when (chatType) { - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt!!.fileUuid!!) - MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl( - "0", - md5, - ptt!!.groupFileKey!! - ) - - else -> throw UnsupportedOperationException("Not supported chat type: $chatType") - }, - "magic" to ptt!!.pbReserve?.magic, - ) - ) - ) - } - elements?.forEach { msg -> - kotlin.runCatching { - val elementType = if (msg.text != null) { - 1 - } else if (msg.face != null) { - 2 - } else if (msg.notOnlineImage != null) { - 4 - } else if (msg.customFace != null) { - 8 - } else if (msg.generalFlags != null) { - 37 - } else if (msg.srcMsg != null) { - 45 - } else if (msg.lightApp != null) { - 51 - } else if (msg.commonElem != null) { - 53 - } else - throw UnsupportedOperationException("不支持的消息element类型:$msg") - val converter = ElemConverter[elementType] - converter?.invoke(chatType, peerId, subPeer, msg) - ?: throw UnsupportedOperationException("不支持的消息element类型:$elementType") - }.onSuccess { - messageData.add(it) - }.onFailure { - if (it is UnknownError) { - // 不处理的消息类型,抛出unknown error - } else { - LogCenter.log("消息element转换错误:$it", Level.WARN) - } - } - } - return messageData -} - -@JvmName("msgElementListToSegments") -internal suspend fun List.toSegments(chatType: Int, peerId: String, subPeer: String): List { - val messageData = arrayListOf() - this.forEach { msg -> - kotlin.runCatching { - val converter = NtMsgElementConverter[msg.elementType] - converter?.invoke(chatType, peerId, subPeer, msg) - ?: throw UnsupportedOperationException("不支持的消息element类型:${msg.elementType}") - }.onSuccess { - messageData.add(it) - }.onFailure { - if (it is UnknownError) { - // 不处理的消息类型,抛出unknown error - } else { - LogCenter.log("消息element转换错误:$it, elementType: ${msg.elementType}", Level.WARN) - } - } - } - return messageData -} - -internal suspend fun List.toCQCode(chatType: Int, peerId: String, subPeer: String): String { - if (this.isEmpty()) { - return "" - } - return MessageHelper.nativeEncodeCQCode(this.toSegments(chatType, peerId, subPeer).map { - val params = hashMapOf() - params["_type"] = it.type - it.data.forEach { (key, value) -> - params[key] = value.toString() - } - params - }) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt deleted file mode 100644 index 9f5fd5c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt +++ /dev/null @@ -1,34 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.msg - -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import moe.fuqiuluo.shamrock.tools.json - - -internal data class MessageSegment( - val type: String, - val data: Map = emptyMap() -) { - fun toJson(): JsonObject { - return mapOf( - "type" to type, - "data" to data - ).json - } -} - -internal fun List.toJson(): JsonArray { - return this.map { - it.toJson() - }.json -} - -internal fun List.toListMap(): List> { - return this.map { - mapOf( - "type" to it.type.json, - "data" to it.data.json - ) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageTempHandler.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageTempHandler.kt deleted file mode 100644 index c2f16dd..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageTempHandler.kt +++ /dev/null @@ -1,34 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.msg - -import com.tencent.qqnt.kernel.nativeinterface.MsgRecord -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import java.util.Collections - -internal object MessageTempHandler { - // 通过MSG SEQ临时监听器 - private val tempMessageListenerMap = Collections.synchronizedMap(HashMap Unit>()) - - fun registerTemporaryMsgListener( - msgSeq: Long, - listener: suspend MsgRecord.() -> Unit - ) { - LogCenter.log({ "注册临时消息监听器: $msgSeq" }, Level.DEBUG) - tempMessageListenerMap[msgSeq] = listener - } - - fun unregisterTemporaryMsgListener(msgSeq: Long) { - tempMessageListenerMap.remove(msgSeq) - } - - suspend fun notify(record: MsgRecord): Boolean { - tempMessageListenerMap.firstNotNullOfOrNull { - if (it.key == record.msgSeq) it else null - }?.let { - it.value(record) - tempMessageListenerMap.remove(it.key) - return true - } - return false - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt deleted file mode 100644 index 2bdf647..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt +++ /dev/null @@ -1,593 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.msg.converter - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import io.ktor.util.* -import kotlinx.io.core.ByteReadPacket -import kotlinx.io.core.discardExact -import kotlinx.io.core.readUInt -import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment -import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.db.ImageDB -import moe.fuqiuluo.shamrock.helper.db.ImageMapping -import moe.fuqiuluo.shamrock.helper.db.MessageDB -import moe.fuqiuluo.shamrock.tools.asJsonArray -import moe.fuqiuluo.shamrock.utils.DeflateTools -import moe.fuqiuluo.shamrock.tools.asJsonObject -import moe.fuqiuluo.shamrock.tools.asString -import moe.fuqiuluo.shamrock.tools.toHexString -import moe.fuqiuluo.symbols.decodeProtobuf -import moe.fuqiuluo.shamrock.tools.slice -import protobuf.message.Elem -import protobuf.message.element.commelem.ButtonExtra -import protobuf.message.element.commelem.MarkdownExtra -import protobuf.message.element.commelem.QFaceExtra - - -internal typealias IElemConverter = suspend (Int, String, String, Elem) -> MessageSegment - -internal object ElemConverter { - private val convertMap = mapOf( - 1 to ElemConverter::convertTextElem, - 2 to ElemConverter::convertFaceElem, - 4 to ElemConverter::convertNotOnlineImageElem, - 8 to ElemConverter::convertCustomFaceElem, -// MsgConstant.KELEMTYPEPTT to ElemConverter::convertVoiceElem, -// MsgConstant.KELEMTYPEVIDEO to ElemConverter::convertVideoElem, -// MsgConstant.KELEMTYPEMARKETFACE to ElemConverter::convertMarketFaceElem, - 37 to ElemConverter::convertGeneralFlagsElem, - 45 to ElemConverter::convertReplyElem, - 51 to ElemConverter::convertStructJsonElem, - 53 to ElemConverter::convertCommonElem, -// MsgConstant.KELEMTYPEGRAYTIP to ElemConverter::convertGrayTipsElem, -// MsgConstant.KELEMTYPEFILE to ElemConverter::convertFileElem, -// //MsgConstant.KELEMTYPEMULTIFORWARD to ElemConverter::convertXmlMultiMsgElem, -// //MsgConstant.KELEMTYPESTRUCTLONGMSG to ElemConverter::convertXmlLongMsgElem, -// MsgConstant.KELEMTYPEFACEBUBBLE to ElemConverter::convertBubbleFaceElem, - ) - - operator fun get(type: Int): IElemConverter? = convertMap[type] - - /** - * 文本 / 艾特 消息转换消息段 - */ - private suspend fun convertTextElem( - chatType: Int, - peerId: String, - subPeer: String, - element: Elem - ): MessageSegment { - val text = element.text!! - if (text.attr6Buf != null) { - val at = ByteReadPacket(text.attr6Buf!!) - at.discardExact(7) - val uin = at.readUInt() - return MessageSegment( - type = "at", - data = mapOf( - "qq" to uin - ) - ) - } else { - return MessageSegment( - type = "text", - data = mapOf( - "text" to text.str!! - ) - ) - } - } - - /** - * 小表情 / 戳一戳 消息转换消息段 - */ - private suspend fun convertFaceElem( - chatType: Int, - peerId: String, - subPeer: String, - element: Elem - ): MessageSegment { - val face = element.face!! - return MessageSegment( - type = "face", - data = mapOf( - "id" to face.index!! - ) - ) - - } - - /** - * 图片消息转换消息段 - */ - private suspend fun convertCustomFaceElem( - chatType: Int, - peerId: String, - subPeer: String, - element: Elem - ): MessageSegment { - val customFace = element.customFace!! - - val md5 = customFace.md5.toHexString() - - ImageDB.getInstance().imageMappingDao().insert( - ImageMapping( - fileName = md5.uppercase(), - md5 = md5.uppercase(), - chatType = chatType, - size = customFace.size!!.toLong(), - sha = "", - fileId = "", - storeId = 0, - ) - ) - - val origUrl = customFace.origUrl!! - - return MessageSegment( - type = "image", - data = mapOf( - "file" to md5, - "url" to when (chatType) { - MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl( - origUrl, - md5 - ) - - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5) - MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5) - else -> throw UnsupportedOperationException("Not supported chat type: $chatType") - }, - "type" to if (customFace.origin == true) "original" else "show" - ) - ) - } - - private suspend fun convertNotOnlineImageElem( - chatType: Int, - peerId: String, - subPeer: String, - element: Elem - ): MessageSegment { - val notOnlineImage = element.notOnlineImage!! - - val md5 = notOnlineImage.picMd5.toHexString() - - ImageDB.getInstance().imageMappingDao().insert( - ImageMapping( - fileName = md5.uppercase(), - md5 = md5.uppercase(), - chatType = chatType, - size = notOnlineImage.fileLen!!.toLong(), - sha = "", - fileId = "", - storeId = 0, - ) - ) - - val origUrl = notOnlineImage.origUrl!! - - return MessageSegment( - type = "image", - data = mapOf( - "file" to md5, - "url" to when (chatType) { - MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl( - origUrl, - md5 - ) - - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5) - MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5) - else -> throw UnsupportedOperationException("Not supported chat type: $chatType") - }, - "type" to if (notOnlineImage.original == true) "original" else "show" - ) - ) - } - - private suspend fun convertGeneralFlagsElem( - chatType: Int, - peerId: String, - subPeer: String, - element: Elem - ): MessageSegment { - val generalFlags = element.generalFlags!! - if (generalFlags.longTextFlag == 1u) { - return MessageSegment( - type = "general_flags", - data = mapOf( - "res_id" to generalFlags.longTextResid - ) - ) - } - throw UnknownError("no segment") - } - -// /** -// * 视频消息转换消息段 -// */ -// private suspend fun convertVideoElem( -// chatType: Int, -// peerId: String, -// subPeer: String, -// element: Elem -// ): MessageSegment { -// val video = element.videoElement -// val md5 = if (video.fileName.contains("/")) { -// video.videoMd5.takeIf { -// !it.isNullOrEmpty() -// }?.hex2ByteArray() ?: video.fileName.split("/").let { -// it[it.size - 2].hex2ByteArray() -// } -// } else video.fileName.split(".")[0].hex2ByteArray() -// -// //LogCenter.log({ "receive video msg: $video" }, Level.DEBUG) -// -// return MessageSegment( -// type = "video", -// data = mapOf( -// "file" to video.fileName, -// "url" to when (chatType) { -// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) -// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid) -// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) -// else -> throw UnsupportedOperationException("Not supported chat type: $chatType") -// } -// ).also { -// if ((it["url"] as String).isBlank()) -// it.remove("url") -// } -// ) -// } -// -// /** -// * 商城大表情消息转换消息段 -// */ -// private suspend fun convertMarketFaceElem( -// chatType: Int, -// peerId: String, -// subPeer: String, -// element: Elem -// ): MessageSegment { -// val face = element.marketFaceElement -// return when (face.emojiId.lowercase()) { -// "4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice") -// "83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps") -// else -> MessageSegment( -// type = "mface", -// data = mapOf( -// "id" to face.emojiId -// ) -// ) -// } -// } -// - /** - * 回复消息转消息段 - */ - private suspend fun convertReplyElem( - chatType: Int, - peerId: String, - subPeer: String, - element: Elem - ): MessageSegment { - val srcMsg = element.srcMsg!! - val msgId = srcMsg.pbReserve?.msgRand?.toLong() ?: 0 - val msgHash = if (msgId != 0L) { - MessageHelper.generateMsgIdHash(chatType, msgId) - } else { - val msgSeq = srcMsg.origSeqs?.first()?.toInt() ?: 0 - MessageDB.getInstance().messageMappingDao() - .queryByMsgSeq(chatType, peerId, msgSeq)?.msgHashId - ?: kotlin.run { - LogCenter.log("消息映射关系未找到: Message($msgSeq)", Level.WARN) - MessageHelper.generateMsgIdHash(chatType, msgId) - } - } - - return MessageSegment( - type = "reply", - data = mapOf( - "id" to msgHash - ) - ) - } - - /** - * JSON消息转消息段 - */ - private suspend fun convertStructJsonElem( - chatType: Int, - peerId: String, - subPeer: String, - element: Elem - ): MessageSegment { - val data = element.lightApp!!.data!! - val jsonStr = String(if (data[0].toInt() == 1) DeflateTools.uncompress(data.slice(1)) else data.slice(1)) - val json = jsonStr.asJsonObject - return when (json["app"].asString) { - "com.tencent.multimsg" -> { - val info = json["meta"].asJsonObject["detail"].asJsonObject - MessageSegment( - type = "forward", - data = mapOf( - "id" to info["resid"].asString, - "filename" to info["uniseq"].asString, - "summary" to info["summary"].asString, - "desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString } - ) - ) - } - - "com.tencent.troopsharecard" -> { - val info = json["meta"].asJsonObject["contact"].asJsonObject - MessageSegment( - type = "contact", - data = mapOf( - "type" to "group", - "id" to info["jumpUrl"].asString.split("group_code=")[1] - ) - ) - } - - "com.tencent.contact.lua" -> { - val info = json["meta"].asJsonObject["contact"].asJsonObject - MessageSegment( - type = "contact", - data = mapOf( - "type" to "private", - "id" to info["jumpUrl"].asString.split("uin=")[1] - ) - ) - } - - "com.tencent.map" -> { - val info = json["meta"].asJsonObject["Location.Search"].asJsonObject - MessageSegment( - type = "location", - data = mapOf( - "lat" to info["lat"].asString, - "lon" to info["lng"].asString, - "content" to info["address"].asString, - "title" to info["name"].asString - ) - ) - } - - else -> MessageSegment( - type = "json", - data = mapOf( - "data" to jsonStr - ) - ) - } - } - - - private suspend fun convertCommonElem( - chatType: Int, - peerId: String, - subPeer: String, - element: Elem - ): MessageSegment { - val commonElem = element.commonElem!! - return when (commonElem.serviceType) { - - 37 -> { - val qFaceExtra = commonElem.elem!!.decodeProtobuf() - when (qFaceExtra.faceId) { - 358 -> MessageSegment( - type = "dice", - data = mapOf( - "result" to qFaceExtra.result!! - ) - ) - - 359 -> MessageSegment( - type = "rps", - data = mapOf( - "result" to qFaceExtra.result!! - ) - ) - - else -> MessageSegment( - type = "face", - data = mapOf( - "id" to qFaceExtra.faceId!!, - "big" to true, - "result" to qFaceExtra.result!! // (1布 2剪 3锤) (骰子123456) - ) - ) - } - } - - 45 -> { - val markdownExtra = commonElem.elem!!.decodeProtobuf() - MessageSegment( - type = "markdown", - data = mapOf( - "content" to markdownExtra.content!! - ) - ) - } - - 46 -> { - val buttonExtra = commonElem.elem!!.decodeProtobuf() - MessageSegment( - type = "button", - data = buttonExtra.field1!!.let { - mapOf( - "rows" to it.rows!!.map { row -> - mapOf( - "buttons" to row.buttons!!.map { button -> - val renderData = button.renderData - val action = button.action - val permission = action?.permission - mapOf( - "id" to button.id, - "render_data" to mapOf( - "label" to (renderData?.label ?: ""), - "visited_label" to (renderData?.visitedLabel ?: ""), - "style" to (renderData?.style ?: 0) - ), - "action" to mapOf( - "type" to (action?.type ?: 0), - "permission" to mapOf( - "type" to (permission?.type ?: 0), - "specify_role_ids" to permission?.specifyRoleIds, - "specify_user_ids" to permission?.specifyUserIds - ), - "unsupport_tips" to (action?.unsupportTips ?: ""), - "data" to (action?.data ?: ""), - "reply" to action?.reply, - "enter" to action?.enter, - ) - ) - }) - }, - "appid" to it.appid - ) - } - ) - } - - else -> MessageSegment( - type = "common", - data = mapOf( - "data" to commonElem.elem!!.encodeBase64() - ) - ) - } - } - - -// -// /** -// * 灰色提示条消息过滤 -// */ -// private suspend fun convertGrayTipsElem( -// chatType: Int, -// peerId: String, -// subPeer: String, -// element: Elem -// ): MessageSegment { -// val tip = element.grayTipElement -// when (tip.subElementType) { -// MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> { -// val notify = tip.jsonGrayTipElement -// when (notify.busiId) { -// /* 新人入群 */ 17L, /* 群戳一戳 */1061L, -// /* 群撤回 */1014L, /* 群设精消息 */2401L, -// /* 群头衔 */2407L -> { -// } -// -// else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN) -// } -// } -// -// MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> { -// val notify = tip.xmlElement -// when (notify.busiId) { -// /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {} -// else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN) -// } -// } -// -// else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN) -// } -// // 提示类消息,这里提供的是一个xml,不具备解析通用性 -// // 在这里不推送 -// throw UnknownError() -// } -// -// /** -// * 文件消息转换消息段 -// */ -// private suspend fun convertFileElem( -// chatType: Int, -// peerId: String, -// subPeer: String, -// element: Elem -// ): MessageSegment { -// val fileMsg = element.fileElement -// val fileName = fileMsg.fileName -// val fileSize = fileMsg.fileSize -// val expireTime = fileMsg.expireTime ?: 0 -// val fileId = fileMsg.fileUuid -// val bizId = fileMsg.fileBizId ?: 0 -// val fileSubId = fileMsg.fileSubId ?: "" -// val url = when (chatType) { -// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) -// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId) -// else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId) -// } -// -// return MessageSegment( -// type = "file", -// data = mapOf( -// "name" to fileName, -// "size" to fileSize, -// "expire" to expireTime, -// "id" to fileId, -// "url" to url, -// "biz" to bizId, -// "sub" to fileSubId -// ) -// ) -// } -// -// /** -// * 老板QQ的合并转发信息 -// */ -// private suspend fun convertXmlMultiMsgElem( -// chatType: Int, -// peerId: String, -// subPeer: String, -// element: Elem -// ): MessageSegment { -// val multiMsg = element.multiForwardElem -// return MessageSegment( -// type = "forward", -// data = mapOf( -// "id" to multiMsg.resId -// ) -// ) -// } -// -// private suspend fun convertXmlLongMsgElem( -// chatType: Int, -// peerId: String, -// subPeer: String, -// element: Elem -// ): MessageSegment { -// val longMsg = element.structLongElem -// return MessageSegment( -// type = "forward", -// data = mapOf( -// "id" to longMsg.resId -// ) -// ) -// } -// -// -// private suspend fun convertBubbleFaceElem( -// chatType: Int, -// peerId: String, -// subPeer: String, -// element: Elem -// ): MessageSegment { -// val bubbleElement = element.faceBubbleElement -// return MessageSegment( -// type = "bubble_face", -// data = mapOf( -// "id" to bubbleElement.yellowFaceInfo.index, -// "count" to (bubbleElement.faceCount ?: 1), -// ) -// ) -// } - - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt deleted file mode 100644 index 2cff2c6..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt +++ /dev/null @@ -1,608 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.msg.converter - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgElement -import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment -import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc -import moe.fuqiuluo.shamrock.helper.ContactHelper -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.helper.db.ImageDB -import moe.fuqiuluo.shamrock.helper.db.ImageMapping -import moe.fuqiuluo.shamrock.helper.db.MessageDB -import moe.fuqiuluo.shamrock.tools.asJsonArray -import moe.fuqiuluo.shamrock.tools.asJsonObject -import moe.fuqiuluo.shamrock.tools.asString -import moe.fuqiuluo.shamrock.tools.hex2ByteArray -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER - -internal typealias IMsgElementConverter = suspend (Int, String, String, MsgElement) -> MessageSegment - -internal object NtMsgElementConverter { - private val convertMap = hashMapOf( - MsgConstant.KELEMTYPETEXT to NtMsgElementConverter::convertTextElem, - MsgConstant.KELEMTYPEFACE to NtMsgElementConverter::convertFaceElem, - MsgConstant.KELEMTYPEPIC to NtMsgElementConverter::convertImageElem, - MsgConstant.KELEMTYPEPTT to NtMsgElementConverter::convertVoiceElem, - MsgConstant.KELEMTYPEVIDEO to NtMsgElementConverter::convertVideoElem, - MsgConstant.KELEMTYPEMARKETFACE to NtMsgElementConverter::convertMarketFaceElem, - MsgConstant.KELEMTYPEARKSTRUCT to NtMsgElementConverter::convertStructJsonElem, - MsgConstant.KELEMTYPEREPLY to NtMsgElementConverter::convertReplyElem, - MsgConstant.KELEMTYPEGRAYTIP to NtMsgElementConverter::convertGrayTipsElem, - MsgConstant.KELEMTYPEFILE to NtMsgElementConverter::convertFileElem, - MsgConstant.KELEMTYPEMARKDOWN to NtMsgElementConverter::convertMarkdownElem, - //MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem, - //MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem, - MsgConstant.KELEMTYPEFACEBUBBLE to NtMsgElementConverter::convertBubbleFaceElem, - MsgConstant.KELEMTYPEINLINEKEYBOARD to NtMsgElementConverter::convertInlineKeyboardElem - ) - - operator fun get(type: Int): IMsgElementConverter? = convertMap[type] - - /** - * 文本 / 艾特 消息转换消息段 - */ - private suspend fun convertTextElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val text = element.textElement - return if (text.atType != MsgConstant.ATTYPEUNKNOWN) { - MessageSegment( - type = "at", - data = hashMapOf( - "qq" to ContactHelper.getUinByUidAsync(text.atNtUid), - ) - ) - } else { - MessageSegment( - type = "text", - data = hashMapOf( - "text" to text.content - ) - ) - } - } - - /** - * 小表情 / 戳一戳 消息转换消息段 - */ - private suspend fun convertFaceElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val face = element.faceElement - - if (face.faceType == 5) { - return MessageSegment( - type = "poke", - data = hashMapOf( - "type" to face.pokeType, - "id" to face.vaspokeId, - "strength" to face.pokeStrength - ) - ) - } - when (face.faceIndex) { - 114 -> { - return MessageSegment( - type = "basketball", - data = hashMapOf( - "id" to face.resultId.ifEmpty { "0" }.toInt(), - ) - ) - } - - 358 -> { - if (face.sourceType == 1) return MessageSegment("new_dice") - return MessageSegment( - type = "new_dice", - data = hashMapOf( - "id" to face.resultId.ifEmpty { "0" }.toInt() - ) - ) - } - - 359 -> { - if (face.resultId.isEmpty()) return MessageSegment("new_rps") - return MessageSegment( - type = "new_rps", - data = hashMapOf( - "id" to face.resultId.ifEmpty { "0" }.toInt() - ) - ) - } - - 394 -> { - //LogCenter.log(face.toString()) - return MessageSegment( - type = "face", - data = hashMapOf( - "id" to face.faceIndex, - "big" to (face.faceType == 3), - "result" to (face.resultId ?: "1") - ) - ) - } - - else -> return MessageSegment( - type = "face", - data = hashMapOf( - "id" to face.faceIndex, - "big" to (face.faceType == 3) - ) - ) - } - } - - /** - * 图片消息转换消息段 - */ - private suspend fun convertImageElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val image = element.picElement - val md5 = (image.md5HexStr ?: image.fileName - .replace("{", "") - .replace("}", "") - .replace("-", "").split(".")[0]) - .uppercase() - - var storeId = 0 - if (PlatformUtils.getQQVersionCode() > QQ_9_0_8_VER) { - storeId = image.storeID - } - - ImageDB.getInstance().imageMappingDao().insert( - ImageMapping( - fileName = md5, - md5 = md5, - chatType = chatType, - size = image.fileSize, - sha = "", - fileId = image.fileUuid, - storeId = storeId, - ) - ) - - //LogCenter.log(image.toString()) - - val originalUrl = image.originImageUrl ?: "" - LogCenter.log({ "receive image: $image" }, Level.DEBUG) - - /* - PicElement{picSubType=0,fileName=A655FCDADABC40D0CEAF6F9AF92937CD.jpg,fileSize=142865,picWidth=886,picHeight=1920,original=false,md5HexStr=a655fcdadabc40d0ceaf6f9af92937cd,sourcePath=null,thumbPath=null,transferStatus=2,progress=0,picType=1000,invalidState=0,fileUuid=CgoxMDI5Mzc0MTE1EhTnucgrUbp3MJjjagUM2-VxSQ5V7hiR3Agg_goo9ZCZt-HNhANQgJqeAQ,fileSubId=,thumbFileSize=0,fileBizId=null,downloadIndex=null,summary=,emojiFrom=null,emojiWebUrl=null,emojiAd=EmojiAD{url=,desc=,},emojiMall=EmojiMall{packageId=0,emojiId=0,},emojiZplan=EmojiZPlan{actionId=0,actionName=,actionType=0,playerNumber=0,peerUid=0,bytesReserveInfo=,},originImageMd5=,originImageUrl=null,importRichMediaContext=null,isFlashPic=false,} - */ - return MessageSegment( - type = "image", - data = hashMapOf( - "file" to md5, - "url" to when (chatType) { - MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl( - originalUrl = originalUrl, - md5 = md5, - fileId = image.fileUuid, - width = image.picWidth.toUInt(), - height = image.picHeight.toUInt(), - sha = "", - fileSize = image.fileSize.toULong(), - peer = peerId - ) - - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl( - originalUrl = originalUrl, - md5 = md5, - fileId = image.fileUuid, - width = image.picWidth.toUInt(), - height = image.picHeight.toUInt(), - sha = "", - fileSize = image.fileSize.toULong(), - peer = peerId, - storeId = storeId - ) - - MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl( - originalUrl = originalUrl, - md5 = md5, - fileId = image.fileUuid, - width = image.picWidth.toUInt(), - height = image.picHeight.toUInt(), - sha = "", - fileSize = image.fileSize.toULong(), - peer = peerId, - subPeer = subPeer - ) - - else -> throw UnsupportedOperationException("Not supported chat type: $chatType") - }, - "subType" to image.picSubType, - "type" to if (image.isFlashPic == true) "flash" else if (image.original) "original" else "show" - ) - ) - } - - /** - * 语音消息转换消息段 - */ - private suspend fun convertVoiceElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val record = element.pttElement - - val md5 = if (record.fileName.startsWith("silk")) - record.fileName.substring(5) - else record.md5HexStr - - return MessageSegment( - type = "record", - data = hashMapOf( - "file" to md5, - "url" to when (chatType) { - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid) - MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl( - "0", - md5.hex2ByteArray(), - record.fileUuid - ) - - else -> throw UnsupportedOperationException("Not supported chat type: $chatType") - } - ).also { - if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) { - it["magic"] = "1" - } - if ((it["url"] as String).isBlank()) { - it.remove("url") - } - } - ) - } - - /** - * 视频消息转换消息段 - */ - private suspend fun convertVideoElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val video = element.videoElement - val md5 = if (video.fileName.contains("/")) { - video.videoMd5.takeIf { - !it.isNullOrEmpty() - }?.hex2ByteArray() ?: video.fileName.split("/").let { - it[it.size - 2].hex2ByteArray() - } - } else video.fileName.split(".")[0].hex2ByteArray() - - //LogCenter.log({ "receive video msg: $video" }, Level.DEBUG) - - return MessageSegment( - type = "video", - data = hashMapOf( - "file" to video.fileName, - "url" to when (chatType) { - MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid) - MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) - else -> throw UnsupportedOperationException("Not supported chat type: $chatType") - } - ).also { - if ((it["url"] as String).isBlank()) - it.remove("url") - } - ) - } - - /** - * 商城大表情消息转换消息段 - */ - private suspend fun convertMarketFaceElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val face = element.marketFaceElement - return when (face.emojiId.lowercase()) { - "4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice") - "83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps") - else -> MessageSegment( - type = "mface", - data = hashMapOf( - "id" to face.emojiId - ) - ) - } - } - - /** - * JSON消息转消息段 - */ - private suspend fun convertStructJsonElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val data = element.arkElement.bytesData.asJsonObject - return when (data["app"].asString) { - "com.tencent.multimsg" -> { - val info = data["meta"].asJsonObject["detail"].asJsonObject - MessageSegment( - type = "forward", - data = mapOf( - "id" to info["resid"].asString, - "filename" to info["uniseq"].asString, - "summary" to info["summary"].asString, - "desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString } - ) - ) - } - - "com.tencent.troopsharecard" -> { - val info = data["meta"].asJsonObject["contact"].asJsonObject - MessageSegment( - type = "contact", - data = hashMapOf( - "type" to "group", - "id" to info["jumpUrl"].asString.split("group_code=")[1] - ) - ) - } - - "com.tencent.contact.lua" -> { - val info = data["meta"].asJsonObject["contact"].asJsonObject - MessageSegment( - type = "contact", - data = hashMapOf( - "type" to "private", - "id" to info["jumpUrl"].asString.split("uin=")[1] - ) - ) - } - - "com.tencent.map" -> { - val info = data["meta"].asJsonObject["Location.Search"].asJsonObject - MessageSegment( - type = "location", - data = hashMapOf( - "lat" to info["lat"].asString, - "lon" to info["lng"].asString, - "content" to info["address"].asString, - "title" to info["name"].asString - ) - ) - } - - else -> MessageSegment( - type = "json", - data = mapOf( - "data" to element.arkElement.bytesData.asJsonObject.toString() - ) - ) - } - } - - /** - * 回复消息转消息段 - */ - private suspend fun convertReplyElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val reply = element.replyElement - val msgId = reply.replayMsgId - val msgHash = if (msgId != 0L) { - MessageHelper.generateMsgIdHash(chatType, msgId) - } else { - MessageDB.getInstance().messageMappingDao() - .queryByMsgSeq(chatType, peerId, reply.replayMsgSeq?.toInt() ?: 0)?.msgHashId - ?: kotlin.run { - LogCenter.log("消息映射关系未找到: Message($reply)", Level.WARN) - MessageHelper.generateMsgIdHash(chatType, reply.sourceMsgIdInRecords) - } - } - - return MessageSegment( - type = "reply", - data = mapOf( - "id" to msgHash - ) - ) - } - - /** - * 灰色提示条消息过滤 - */ - private suspend fun convertGrayTipsElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val tip = element.grayTipElement - when (tip.subElementType) { - MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> { - val notify = tip.jsonGrayTipElement - when (notify.busiId) { - /* 新人入群 */ 17L, /* 群戳一戳 */1061L, - /* 群撤回 */1014L, /* 群设精消息 */2401L, - /* 群头衔 */2407L -> { - } - - else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN) - } - } - - MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> { - val notify = tip.xmlElement - when (notify.busiId) { - /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {} - else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN) - } - } - - else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN) - } - // 提示类消息,这里提供的是一个xml,不具备解析通用性 - // 在这里不推送 - throw UnknownError() - } - - /** - * 文件消息转换消息段 - */ - private suspend fun convertFileElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val fileMsg = element.fileElement - val fileName = fileMsg.fileName - val fileSize = fileMsg.fileSize - val expireTime = fileMsg.expireTime ?: 0 - val fileId = fileMsg.fileUuid - val bizId = fileMsg.fileBizId ?: 0 - val fileSubId = fileMsg.fileSubId ?: "" - val url = when (chatType) { - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) - MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId) - else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId) - } - - return MessageSegment( - type = "file", - data = mapOf( - "name" to fileName, - "size" to fileSize, - "expire" to expireTime, - "id" to fileId, - "url" to url, - "biz" to bizId, - "sub" to fileSubId - ) - ) - } - - /** - * 老板QQ的合并转发信息 - */ - private suspend fun convertXmlMultiMsgElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val multiMsg = element.multiForwardMsgElement - return MessageSegment( - type = "forward", - data = mapOf( - "id" to multiMsg.resId - ) - ) - } - - private suspend fun convertXmlLongMsgElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val longMsg = element.structLongMsgElement - return MessageSegment( - type = "forward", - data = mapOf( - "id" to longMsg.resId - ) - ) - } - - private suspend fun convertMarkdownElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val markdown = element.markdownElement - return MessageSegment( - type = "markdown", - data = mapOf( - "content" to markdown.content - ) - ) - } - - private suspend fun convertBubbleFaceElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val bubbleElement = element.faceBubbleElement - return MessageSegment( - type = "bubble_face", - data = mapOf( - "id" to bubbleElement.yellowFaceInfo.index, - "count" to (bubbleElement.faceCount ?: 1), - ) - ) - } - - private suspend fun convertInlineKeyboardElem( - chatType: Int, - peerId: String, - subPeer: String, - element: MsgElement - ): MessageSegment { - val keyboard = element.inlineKeyboardElement - return MessageSegment( - type = "button", - data = mapOf( - "rows" to keyboard.rows.map { row -> - mapOf("buttons" to row.buttons.map { button -> - mapOf( - "id" to button.id, - "render_data" to mapOf( - "label" to (button?.label ?: ""), - "visited_label" to (button?.visitedLabel ?: ""), - "style" to (button?.style ?: 0) - - ), - "action" to mapOf( - "type" to (button?.type ?: 0), - "permission" to mapOf( - "type" to (button?.permissionType ?: 0), - "specify_role_ids" to button?.specifyRoleIds, - "specify_user_ids" to button?.specifyTinyids - ), - "unsupport_tips" to (button?.unsupportTips ?: ""), - "data" to (button?.data ?: ""), - "reply" to button?.isReply, - "enter" to button?.enter - ) - ) - }) - - }, - "appid" to keyboard.botAppid - ) - ) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt deleted file mode 100644 index bf8fbbc..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt +++ /dev/null @@ -1,717 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.msg.maker - -import android.graphics.BitmapFactory -import androidx.exifinterface.media.ExifInterface -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import kotlinx.serialization.json.JsonObject -import moe.fuqiuluo.qqinterface.servlet.CardSvc -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc -import moe.fuqiuluo.qqinterface.servlet.msg.toJson -import moe.fuqiuluo.qqinterface.servlet.msg.toSegments -import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc -import moe.fuqiuluo.shamrock.helper.* -import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToRichText -import moe.fuqiuluo.shamrock.helper.MessageHelper.obtainMessageTypeByDetailType -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.utils.DeflateTools -import moe.fuqiuluo.shamrock.utils.FileUtils -import protobuf.auto.toByteArray -import protobuf.message.Elem -import protobuf.message.Ptt -import protobuf.message.RichText -import protobuf.message.element.* -import protobuf.message.element.commelem.* -import protobuf.oidb.cmd0x11c5.C2CUserInfo -import protobuf.oidb.cmd0x11c5.GroupUserInfo -import java.io.File -import java.nio.ByteBuffer -import java.util.* -import kotlin.random.Random -import kotlin.random.nextULong -import kotlin.time.Duration.Companion.seconds - -internal typealias IElemMaker = suspend (ElemMaker, Int, Long, String, JsonObject) -> Unit - -internal class ElemMaker { - companion object { - private val makerArray = hashMapOf( - "text" to ElemMaker::createTextElem, - "at" to ElemMaker::createAtElem, - "face" to ElemMaker::createFaceElem, - "pic" to ElemMaker::createImageElem, - "image" to ElemMaker::createImageElem, - "reply" to ElemMaker::createReplyElem, - "forward" to ElemMaker::createForwardStruct, - "weather" to ElemMaker::createWeatherElem, - "json" to ElemMaker::createJsonElem, - "poke" to ElemMaker::createPokeElem, - "dice" to ElemMaker::createNewDiceElem, - "rps" to ElemMaker::createNewRpsElem, - "markdown" to ElemMaker::createMarkdownElem, - "button" to ElemMaker::createButtonElem, -// "anonymous" to ElemMaker::createAnonymousElem, -// "share" to ElemMaker::createShareElem, -// "contact" to ElemMaker::createContactElem, -// "location" to ElemMaker::createLocationElem, -// "music" to ElemMaker::createMusicElem, -// "touch" to ElemMaker::createTouchElem, -// "multi_msg" to MessageMaker::createLongMsgStruct, -// "bubble_face" to ElemMaker::createBubbleFaceElem, - "voice" to ElemMaker::createRecordElem, - "record" to ElemMaker::createRecordElem, -// "video" to ElemMaker::createVideoElem, - ) - - operator fun get(type: String): IElemMaker? = makerArray[type] - } - - private var rich = RichText() - private val elems = mutableListOf() - private var summary = StringBuilder() - - fun getRich(): RichText { - rich.elements = elems - return rich - } - - fun getDesc(): String { - return summary.toString() - } - - private suspend fun createTextElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - data.checkAndThrow("text") - val text = data["text"].asString - val elem = Elem( - text = TextMsg(text) - ) - elems.add(elem) - summary.append(text) - } - - private suspend fun createAtElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - when (chatType) { - MsgConstant.KCHATTYPEGROUP -> { - data.checkAndThrow("qq") - val qq: Long - val type: Int - val display = when (val qqStr = data["qq"].asString) { - "0", "all" -> { - qq = 0 - type = 1 - "@全体成员" - } - - "online" -> { - qq = 0 - type = 64 - "@在线成员" - } - - else -> { - qq = qqStr.toLong() - type = 0 - "@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2( - peerId.toLong(), - qq, - true - ).let { - val info = it.getOrNull() - if (info == null) - LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR) - else info.troopnick - .ifNullOrEmpty(info.friendnick) - .ifNullOrEmpty(qqStr) - }) - } - } - - val attr6: ByteBuffer = ByteBuffer.allocate(6) - attr6.put(byteArrayOf(0, 1, 0, 0, 0)) - attr6.putChar(display.length.toChar()) - attr6.putChar(type.toChar()) - attr6.putBuf32Long(qq) - attr6.put(byteArrayOf(0, 0)) - val elem = Elem( - text = TextMsg(str = display, attr6Buf = attr6.array()) - ) - elems.add(elem) - summary.append(display) - } - - MsgConstant.KCHATTYPEC2C -> { - data.checkAndThrow("qq") - - val qq = data["qq"].asLong - val display = - "@" + (data["name"].asStringOrNull ?: CardSvc.getProfileCard(qq) - .onSuccess { - it.strNick.ifNullOrEmpty(qq.toString()) - }.onFailure { - LogCenter.log("无法获取QQ信息: $qq", Level.WARN) - }) - - val elem = Elem( - text = TextMsg(str = display) - ) - elems.add(elem) - summary.append(display) - } - - else -> throw UnsupportedOperationException("Unsupported chatType($chatType) for AtMsg") - } - } - - private suspend fun createFaceElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - data.checkAndThrow("id") - val faceId = data["id"].asInt - val elem = if (data["big"].asBooleanOrNull == true) { - Elem( - commonElem = CommonElem( - serviceType = 37, - elem = QFaceExtra( - packId = "1", - stickerId = "1", - faceId = faceId, - field4 = 1, - field5 = 1, - result = "", - faceText = "", //todo 表情名字 - field9 = 1 - ).toByteArray(), - businessType = 1 - ) - ) - } else { - Elem( - face = FaceMsg( - index = faceId - ) - ) - } - elems.add(elem) - summary.append("[表情]") - } - - private suspend fun createImageElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - val type = data["type"].asStringOrNull ?: "original" - val isOriginal = type == "original" - val filePath = data["file"].asStringOrNull - val url = data["url"].asStringOrNull - var file: File? = null - if (filePath != null) { - val md5 = filePath - .replace(regex = "[{}\\-]".toRegex(), replacement = "") - .split(".")[0].lowercase() - file = if (md5.length == 32) { - FileUtils.getFileByMd5(md5) - } else { - FileUtils.parseAndSave(filePath) - } - } - if ((file == null || !file.exists()) && url != null) { - file = FileUtils.parseAndSave(url) - } - if (file?.exists() == false) { - throw LogicException("Image(${file.name}) file is not exists, please check your filename.") - } - requireNotNull(file) - - val options = BitmapFactory.Options() - options.inJustDecodeBounds = true - BitmapFactory.decodeFile(file.absolutePath, options) - val exifInterface = ExifInterface(file.absolutePath) - val orientation = exifInterface.getAttributeInt( - ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_UNDEFINED - ) - val picWidth: Int - val picHeight: Int - if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) { - picWidth = options.outWidth - picHeight = options.outHeight - } else { - picWidth = options.outHeight - picHeight = options.outWidth - } - - val fileInfo = NtV2RichMediaSvc.tryUploadResourceByNt( - chatType = chatType, - elementType = MsgConstant.KELEMTYPEPIC, - resources = arrayListOf(file), - timeout = 30.seconds - ).getOrThrow().first() - - runCatching { - fileInfo.uuid.toUInt() - }.onFailure { - NtV2RichMediaSvc.requestUploadNtPic(file, fileInfo.md5, fileInfo.sha, fileInfo.fileName, picWidth.toUInt(), picHeight.toUInt(), 5) { - when(chatType) { - MsgConstant.KCHATTYPEGROUP -> { - sceneType = 2u - grp = GroupUserInfo(peerId.toULong()) - } - MsgConstant.KCHATTYPEC2C -> { - sceneType = 1u - c2c = C2CUserInfo( - accountType = 2u, - uid = ContactHelper.getUidByUinAsync(peerId.toLong()) - ) - } - else -> error("不支持的合并转发图片类型") - } - }.onFailure { - LogCenter.log("获取MultiMedia图片信息失败: $it", Level.ERROR) - }.onSuccess { - //LogCenter.log({ "获取MultiMedia图片信息成功: ${it.hashCode()}" }, Level.INFO) - elems.add(Elem( - commonElem = CommonElem( - serviceType = 48, - businessType = 10, - elem = it.msgInfo!!.toByteArray() - ) - )) - } - }.onSuccess { uuid -> - elems.add(when (chatType) { - MsgConstant.KCHATTYPEGROUP -> Elem( - customFace = CustomFace( - filePath = fileInfo.fileName, - fileId = uuid, - serverIp = 0u, - serverPort = 0u, - fileType = FileUtils.getPicType(file).toUInt(), - useful = 1u, - md5 = fileInfo.md5.hex2ByteArray(), - bizType = data["subType"].asIntOrNull?.toUInt(), - imageType = FileUtils.getPicType(file).toUInt(), - width = picWidth.toUInt(), - height = picHeight.toUInt(), - size = fileInfo.fileSize.toUInt(), - origin = isOriginal, - thumbWidth = 0u, - thumbHeight = 0u, - pbReserve = CustomFace.Companion.PbReserve( - field1 = 0, - field3 = 0, - field4 = 0, - field10 = 0, - field21 = CustomFace.Companion.Object1( - field1 = 0, - field2 = "", - field3 = 0, - field4 = 0, - field5 = 0, - md5Str = fileInfo.md5 - ) - ) - ) - ) - MsgConstant.KCHATTYPEC2C -> Elem( - notOnlineImage = NotOnlineImage( - filePath = fileInfo.fileName, - fileLen = fileInfo.fileSize.toUInt(), - downloadPath = fileInfo.uuid, - imgType = FileUtils.getPicType(file).toUInt(), - picMd5 = fileInfo.md5.hex2ByteArray(), - picHeight = picWidth.toUInt(), - picWidth = picHeight.toUInt(), - resId = fileInfo.uuid, - original = isOriginal, // true - pbReserve = NotOnlineImage.Companion.PbReserve( - field1 = 0, - field3 = 0, - field4 = 0, - field10 = 0, - field20 = NotOnlineImage.Companion.Object1( - field1 = 0, - field2 = "", - field3 = 0, - field4 = 0, - field5 = 0, - field7 = "", - ), - md5Str = fileInfo.md5 - ) - ) - ) - else -> throw LogicException("Not supported chatType($chatType) for PictureMsg") - }) - } - - summary.append("[图片]") - } - - private suspend fun createReplyElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - data.checkAndThrow("id") - val msgHash = data["id"].asInt - val mapping = MessageHelper.getMsgMappingByHash(msgHash) - ?: throw Exception("不存在该消息映射,无法回复消息") - - if (mapping.qqMsgId == 0L) { - // 貌似获取失败了,555 - throw Exception("无法获取被回复消息") - } - - val elem = if (data.containsKey("text")) { - data.checkAndThrow("qq", "time", "seq") - Elem( - srcMsg = SourceMsg( - origSeqs = listOf(data["seq"].asInt), - senderUin = data["qq"].asString.toULong(), - time = data["time"].asString.toULong(), - flag = 1u, - elems = listOf( - Elem( - text = TextMsg( - data["text"].asString - ) - ) - ), - type = 0u, - pbReserve = SourceMsg.Companion.PbReserve( - msgRand = Random.nextInt().toULong(), - field8 = Random.nextInt(0, 10000) - ), - ) - ) - } else { - val msg = - MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull() - ?: throw Exception("无法获取被回复消息") - Elem( - srcMsg = SourceMsg( - origSeqs = listOf(msg.msgSeq.toInt()), - senderUin = msg.senderUin.toULong(), - time = msg.msgTime.toULong(), - flag = 1u, - elems = messageArrayToRichText( - msg.chatType, - msg.msgId, - msg.peerUin.toString(), - msg.elements.toSegments( - msg.chatType, - if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(), - msg.channelId ?: msg.peerUin.toString() - ).toJson() - ).getOrElse { throw Exception("解析回复消息失败: $it") }.second.elements, - type = 0u, - pbReserve = SourceMsg.Companion.PbReserve( - msgRand = Random.nextULong(), - senderUid = msg.senderUid, - receiverUid = TicketSvc.getUid(), - field8 = Random.nextInt(0, 10000) - ), - ) - ) - } - elems.add(elem) - summary.append("[回复消息]") - } - - private suspend fun createJsonElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - data.checkAndThrow("data") - - val elem = Elem( - lightApp = LightAppElem( - data = byteArrayOf(1) + DeflateTools.compress(data.toString().toByteArray()) - ) - ) - elems.add(elem) - summary .append( "[Json消息]" ) - } - - private suspend fun createForwardStruct( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - data.checkAndThrow("id") - val resId = data["id"].asString - val filename = data["filename"].asStringOrNull ?: UUID.randomUUID().toString().uppercase() - var summary = data["summary"].asStringOrNull - val descriptions = data["desc"].asStringOrNull - var news = descriptions?.split("\n")?.map { "text" to it } - - if (news == null || summary == null) { - val forwardMsg = MsgSvc.getForwardMsg(resId).getOrThrow() - if (news == null) { - news = forwardMsg.map { - "text" to it.sender.nickName + ": " + messageArrayToRichText( - obtainMessageTypeByDetailType(it.msgType), - it.qqMsgId, - it.peerId.toString(), - it.message.json - ).getOrThrow().first - } - } - if (summary == null) { - summary = "查看${forwardMsg.size}条转发消息" - } - } - - val json = mapOf( - "app" to "com.tencent.multimsg", - "config" to mapOf( - "autosize" to 1, - "forward" to 1, - "round" to 1, - "type" to "normal", - "width" to 300 - ), - "desc" to "[聊天记录]", - "extra" to mapOf( - "filename" to filename, - "tsum" to 2 - ).json.toString(), - "meta" to mapOf( - "detail" to mapOf( - "news" to news, - "resid" to resId, - "source" to "群聊的聊天记录", - "summary" to summary, - "uniseq" to filename - ) - ), - "prompt" to "[聊天记录]", - "ver" to "0.0.0.5", - "view" to "contact" - ) - val elem = Elem( - lightApp = LightAppElem( - data = byteArrayOf(1) + DeflateTools.compress(json.json.toString().toByteArray()) - ) - ) - elems.add(elem) - this.summary .append( "[聊天记录]" ) - } - - private suspend fun createWeatherElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - var code = data["code"].asIntOrNull - - if (code == null) { - data.checkAndThrow("city") - val city = data["city"].asString - code = WeatherSvc.searchCity(city).onFailure { - LogCenter.log("无法获取城市天气: $city", Level.ERROR) - }.getOrNull()?.firstOrNull()?.adcode - } - - if (code != null) { - val weatherCard = WeatherSvc.fetchWeatherCard(code).getOrThrow() -// OidbSvc.0xdc2_34 -// 00 00 00 DF 08 C2 1B 10 22 22 C4 01 0A B7 01 08 A2 E0 F2 2F 10 01 18 00 2A 02 08 01 58 FB 91 F6 AE 02 62 A1 01 08 01 52 08 E5 8C 97 E4 BA AC 20 20 5A 19 2D 33 C2 B0 2F 33 C2 B0 0A E7 A9 BA E6 B0 94 E8 B4 A8 E9 87 8F 3A E8 89 AF 62 11 5B E5 88 86 E4 BA AB 5D 20 E5 8C 97 E4 BA AC 20 20 6A 25 68 74 74 70 73 3A 2F 2F 77 65 61 74 68 65 72 2E 6D 70 2E 71 71 2E 63 6F 6D 2F 3F 73 74 3D 30 26 5F 77 76 3D 31 72 3E 68 74 74 70 73 3A 2F 2F 69 6D 67 63 61 63 68 65 2E 71 71 2E 63 6F 6D 2F 61 63 2F 71 71 77 65 61 74 68 65 72 2F 69 6D 61 67 65 2F 73 68 61 72 65 5F 69 63 6F 6E 2F 66 69 6E 65 2E 70 6E 67 12 08 08 01 10 FB 91 F6 AE 02 32 0D 61 6E 64 72 6F 69 64 20 39 2E 30 2E 38 - val elem = Elem( - lightApp = LightAppElem( - data = byteArrayOf(1) + DeflateTools.compress( - weatherCard["weekStore"] - .asJsonObject["share"].asString.toByteArray() - ) - ) - ) - elems.add(elem) - summary .append( "[天气卡片]" ) - } else { - throw LogicException("无法获取城市天气") - } - } - - private suspend fun createPokeElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - data.checkAndThrow("type", "id") - val elem = Elem( - commonElem = CommonElem( - serviceType = 2, - elem = PokeExtra( - type = data["type"].asInt, - field7 = 0, - field8 = 0 - ).toByteArray(), - businessType = data["id"].asInt - ) - ) - elems.add(elem) - summary .append( "[戳一戳]" ) - } - - private suspend fun createNewDiceElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - val elem = Elem( - commonElem = CommonElem( - serviceType = 37, - elem = QFaceExtra( - packId = "1", - stickerId = "33", - faceId = 358, - field4 = 1, - field5 = 2, - result = "", - faceText = "/骰子", - field9 = 1 - ).toByteArray(), - businessType = 2 - ) - ) - elems.add(elem) - summary .append( "[骰子]" ) - } - - private suspend fun createNewRpsElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - val elem = Elem( - commonElem = CommonElem( - serviceType = 37, - elem = QFaceExtra( - packId = "1", - stickerId = "34", - faceId = 359, - field4 = 1, - field5 = 2, - result = "", - faceText = "/包剪锤", - field9 = 1 - ).toByteArray(), - businessType = 1 - ) - ) - elems.add(elem) - summary .append( "[包剪锤]" ) - } - - private suspend fun createMarkdownElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - data.checkAndThrow("content") - val elem = Elem( - commonElem = CommonElem( - serviceType = 45, - elem = MarkdownExtra(data["content"].asString).toByteArray(), - businessType = 1 - ) - ) - elems.add(elem) - summary.append("[Markdown消息]") - } - - private suspend fun createButtonElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - data.checkAndThrow("rows") - val elem = Elem( - commonElem = CommonElem( - serviceType = 46, - elem = ButtonExtra( - field1 = Object1( - rows = data["rows"].asJsonArray.map { row -> - Row(buttons = row.asJsonObject["buttons"].asJsonArray.map { - val button = it.asJsonObject - val renderData = button["render_data"].asJsonObject - val action = button["action"].asJsonObject - val permission = action["permission"].asJsonObject - Button( - id = button["id"].asStringOrNull, - renderData = RenderData( - label = renderData["label"].asString, - visitedLabel = renderData["visited_label"].asString, - style = renderData["style"].asInt - ), - action = Action( - type = action["type"].asInt, - permission = Permission( - type = permission["type"].asInt, - specifyRoleIds = permission["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString }, - specifyUserIds = permission["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString } - ), - unsupportTips = action["unsupport_tips"].asString, - data = action["data"].asString, - reply = action["reply"].asBooleanOrNull, - enter = action["enter"].asBooleanOrNull - ) - ) - }) - }, - appid = data["appid"].asIntOrNull - ) - ).toByteArray(), - businessType = 1 - ) - ) - elems.add(elem) - summary.append("[Button消息]") - } - - private suspend fun createRecordElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ) { - data.checkAndThrow("content") - rich.ptt= Ptt( - - ) - summary .append( "[语音消息]" ) - } - - private fun JsonObject.checkAndThrow(vararg key: String) { - key.forEach { - if (!containsKey(it)) throw ParamsException(it) - } - } -} diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt deleted file mode 100644 index f064e76..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt +++ /dev/null @@ -1,1103 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.msg.maker - -import android.graphics.BitmapFactory -import androidx.exifinterface.media.ExifInterface -import com.tencent.mobileqq.app.QQAppInterface -import com.tencent.mobileqq.emoticon.QQSysFaceUtil -import com.tencent.mobileqq.pb.ByteStringMicro -import com.tencent.mobileqq.qroute.QRoute -import com.tencent.qphone.base.remote.ToServiceMsg -import com.tencent.qqnt.aio.adapter.api.IAIOPttApi -import com.tencent.qqnt.kernel.nativeinterface.* -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import moe.fuqiuluo.qqinterface.servlet.CardSvc -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.qqinterface.servlet.LbsSvc -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo -import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc -import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc -import moe.fuqiuluo.qqinterface.servlet.transfile.* -import moe.fuqiuluo.qqinterface.servlet.transfile.FileTransfer -import moe.fuqiuluo.qqinterface.servlet.transfile.data.PictureResource -import moe.fuqiuluo.qqinterface.servlet.transfile.data.Private -import moe.fuqiuluo.qqinterface.servlet.transfile.Transfer -import moe.fuqiuluo.qqinterface.servlet.transfile.data.Troop -import moe.fuqiuluo.qqinterface.servlet.transfile.data.VideoResource -import moe.fuqiuluo.qqinterface.servlet.transfile.data.VoiceResource -import moe.fuqiuluo.shamrock.helper.ActionMsgException -import moe.fuqiuluo.shamrock.helper.ContactHelper -import moe.fuqiuluo.shamrock.helper.IllegalParamsException -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LocalCacheHelper -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.LogicException -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.helper.MusicHelper -import moe.fuqiuluo.shamrock.helper.ParamsException -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.utils.AudioUtils -import moe.fuqiuluo.shamrock.utils.FileUtils -import moe.fuqiuluo.shamrock.utils.MediaType -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import moe.fuqiuluo.shamrock.xposed.helper.msgService -import mqq.app.MobileQQ -import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77 -import tencent.im.oidb.cmd0xdc2.oidb_cmd0xdc2 -import tencent.im.oidb.oidb_sso -import java.io.File -import java.util.* -import kotlin.collections.ArrayList -import kotlin.math.roundToInt -import kotlin.random.Random -import kotlin.random.nextInt - -internal typealias IMsgElementMaker = suspend (Int, Long, String, JsonObject) -> Result - -internal object NtMsgElementMaker { - private val makerMap = hashMapOf( - "text" to NtMsgElementMaker::createTextElem, - "face" to NtMsgElementMaker::createFaceElem, - "pic" to NtMsgElementMaker::createImageElem, - "image" to NtMsgElementMaker::createImageElem, - "voice" to NtMsgElementMaker::createRecordElem, - "record" to NtMsgElementMaker::createRecordElem, - "at" to NtMsgElementMaker::createAtElem, - "video" to NtMsgElementMaker::createVideoElem, - "markdown" to NtMsgElementMaker::createMarkdownElem, - "dice" to NtMsgElementMaker::createDiceElem, - "rps" to NtMsgElementMaker::createRpsElem, - "poke" to NtMsgElementMaker::createPokeElem, - "anonymous" to NtMsgElementMaker::createAnonymousElem, - "share" to NtMsgElementMaker::createShareElem, - "contact" to NtMsgElementMaker::createContactElem, - "location" to NtMsgElementMaker::createLocationElem, - "music" to NtMsgElementMaker::createMusicElem, - "reply" to NtMsgElementMaker::createReplyElem, - "touch" to NtMsgElementMaker::createTouchElem, - "weather" to NtMsgElementMaker::createWeatherElem, - "json" to NtMsgElementMaker::createJsonElem, - "forward" to NtMsgElementMaker::createForwardStruct, - "new_dice" to NtMsgElementMaker::createNewDiceElem, - "new_rps" to NtMsgElementMaker::createNewRpsElem, - "basketball" to NtMsgElementMaker::createBasketballElem, - //"multi_msg" to NtMsgElementMaker::createLongMsgStruct, - "bubble_face" to NtMsgElementMaker::createBubbleFaceElem, - "button" to NtMsgElementMaker::createInlineKeywordElem, - "inline_keyboard" to NtMsgElementMaker::createInlineKeywordElem - ) - - operator fun get(type: String): IMsgElementMaker? = makerMap[type] - - private suspend fun createForwardStruct( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("id") - val resId = data["id"].asString - val filename = data["filename"].asStringOrNull ?: UUID.randomUUID().toString().uppercase() - var summary = data["summary"].asStringOrNull - val descriptions = data["desc"].asStringOrNull - var news = descriptions?.split("\n")?.map { "text" to it } - - if (news == null || summary == null) { - val forwardMsg = MsgSvc.getForwardMsg(resId).getOrElse { return Result.failure(it) } - if (news == null) { - news = forwardMsg.map { - "text" to it.sender.nickName + ": " + MessageHelper.messageArrayToRichText( - MessageHelper.obtainMessageTypeByDetailType(it.msgType), - it.qqMsgId, - it.peerId.toString(), - it.message.json - ).getOrThrow().first - } - } - if (summary == null) { - summary = "查看${forwardMsg.size}条转发消息" - } - } - - val json = mapOf( - "app" to "com.tencent.multimsg", - "config" to mapOf( - "autosize" to 1, - "forward" to 1, - "round" to 1, - "type" to "normal", - "width" to 300 - ), - "desc" to "[聊天记录]", - "extra" to mapOf( - "filename" to filename, - "tsum" to 2 - ).json.toString(), - "meta" to mapOf( - "detail" to mapOf( - "news" to news, - "resid" to resId, - "source" to "群聊的聊天记录", - "summary" to summary, - "uniseq" to filename - ) - ), - "prompt" to "[聊天记录]", - "ver" to "0.0.0.5", - "view" to "contact" - ) - return createJsonElem( - chatType, msgId, peerId, mapOf( - "data" to json - ).json - ) - } - - private suspend fun createInlineKeywordElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - fun tryNewKeyboardButton(button: JsonObject): InlineKeyboardButton { - val renderData = button["render_data"].asJsonObject - val action = button["action"].asJsonObject - val permission = action["permission"].asJsonObject - return runCatching { - InlineKeyboardButton( - button["id"].asStringOrNull ?: "", - renderData["label"].asString, - renderData["visited_label"].asString, - renderData["style"].asInt, - action["type"].asInt, - action["click_limit"].asInt, - action["unsupport_tips"].asString, - action["data"].asString, - action["at_bot_show_channel_list"].asBooleanOrNull ?: false, - permission["type"].asInt, - ArrayList(permission["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString } - ?: arrayListOf()), - ArrayList(permission["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString } - ?: arrayListOf()), - false, 0, false, arrayListOf() - ) - }.getOrElse { - InlineKeyboardButton( - button["id"].asStringOrNull ?: "", - renderData["label"].asString, - renderData["visited_label"].asString, - renderData["style"].asInt, - action["type"].asInt, - action["click_limit"].asInt, - action["unsupport_tips"].asString, - action["data"].asString, - action["at_bot_show_channel_list"].asBooleanOrNull ?: false, - permission["type"].asInt, - ArrayList(permission["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString } - ?: arrayListOf()), - ArrayList(permission["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString } - ?: arrayListOf()), - ) - } - } - - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEINLINEKEYBOARD - val rows = arrayListOf() - - val keyboard = Json.parseToJsonElement(data["data"].asString).asJsonObject - keyboard["rows"].asJsonArray.forEach { - val row = it.asJsonObject - val buttons = arrayListOf() - row["buttons"].asJsonArray.forEach { button -> - val btn = button.asJsonObject - buttons.add(tryNewKeyboardButton(btn)) - } - rows.add(InlineKeyboardRow(buttons)) - } - elem.inlineKeyboardElement = InlineKeyboardElement(rows, keyboard["appid"].asLong) - return Result.success(elem) - } - - private suspend fun createBubbleFaceElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("id", "count") - val faceId = data["id"].asInt - val local = QQSysFaceUtil.convertToLocal(faceId) - val name = QQSysFaceUtil.getFaceDescription(local) - val count = data["count"].asInt - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEFACEBUBBLE - val face = FaceBubbleElement() - face.faceType = 13 - face.faceCount = count - face.faceSummary = QQSysFaceUtil.getPrueFaceDescription(name) - val smallYellowFaceInfo = SmallYellowFaceInfo() - smallYellowFaceInfo.index = faceId - smallYellowFaceInfo.compatibleText = face.faceSummary - smallYellowFaceInfo.text = face.faceSummary - face.yellowFaceInfo = smallYellowFaceInfo - face.faceFlag = 0 - face.content = data["text"].asStringOrNull ?: "[${face.faceSummary}]x$count" - elem.faceBubbleElement = face - return Result.success(elem) - } - - private suspend fun createBasketballElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEFACE - val face = FaceElement() - face.faceIndex = 114 - face.faceText = "/篮球" - face.faceType = 3 - face.packId = "1" - face.stickerId = "13" - face.sourceType = 1 - face.stickerType = 2 - face.resultId = Random.nextInt(1..5).toString() - face.surpriseId = "" - face.randomType = 1 - elem.faceElement = face - return Result.success(elem) - } - - private suspend fun createNewRpsElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEFACE - val face = FaceElement() - face.faceIndex = 359 - face.faceText = "/包剪锤" - face.faceType = 3 - face.packId = "1" - face.stickerId = "34" - face.sourceType = 1 - face.stickerType = 2 - face.resultId = "" - face.surpriseId = "" - face.randomType = 1 - elem.faceElement = face - return Result.success(elem) - } - - private suspend fun createNewDiceElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEFACE - val face = FaceElement() - face.faceIndex = 358 - face.faceText = "/骰子" - face.faceType = 3 - face.packId = "1" - face.stickerId = "33" - face.sourceType = 1 - face.stickerType = 2 - face.resultId = "" - face.surpriseId = "" - face.randomType = 1 - elem.faceElement = face - return Result.success(elem) - } - - private suspend fun createJsonElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("data") - val jsonStr = data["data"].let { - if (it is JsonObject) { - it.asJsonObject.toString() - } else { - // 检查字符串是否是合法json,不然qq会闪退 - try { - val str = it.asString - val element = Json.decodeFromString(str) - if (element !is JsonObject) { - return Result.failure(Exception("不合法的JSON字符串")) - } - str - } catch (err: Throwable) { - LogCenter.log(err.stackTraceToString(), Level.ERROR) - return Result.failure(Exception("不合法的JSON字符串")) - } - } - } - val element = MsgElement() - element.elementType = MsgConstant.KELEMTYPEARKSTRUCT - val ark = ArkElement(jsonStr, null, null) - element.arkElement = ark - return Result.success(element) - } - - private suspend fun createTouchElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("id") - GroupSvc.poke(peerId.toLong(), data["id"].asLong) - return Result.failure(ActionMsgException) - } - - private suspend fun createWeatherElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - var code = data["code"].asIntOrNull - - if (code == null) { - data.checkAndThrow("city") - val city = data["city"].asString - code = WeatherSvc.searchCity(city).onFailure { - LogCenter.log("无法获取城市天气: $city", Level.ERROR) - }.getOrNull()?.firstOrNull()?.adcode - } - - if (code != null) { - WeatherSvc.fetchWeatherCard(code).onSuccess { - val element = MsgElement() - element.elementType = MsgConstant.KELEMTYPEARKSTRUCT - val share = it["weekStore"] - .asJsonObject["share"] - .asJsonObject["data"].toString() - - element.arkElement = - ArkElement(share, null, MsgConstant.ARKSTRUCTELEMENTSUBTYPEUNKNOWN) - - return Result.success(element) - }.onFailure { - LogCenter.log("无法发送天气分享", Level.ERROR) - } - } - return Result.failure(ActionMsgException) - } - - private suspend fun createReplyElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("id") - val element = MsgElement() - element.elementType = MsgConstant.KELEMTYPEREPLY - val reply = ReplyElement() - - val msgHash = data["id"].asInt - val mapping = MessageHelper.getMsgMappingByHash(msgHash) - ?: return Result.failure(Exception("不存在该消息映射,无法回复消息")) - - reply.replayMsgId = mapping.qqMsgId - reply.sourceMsgIdInRecords = mapping.qqMsgId - - if (reply.replayMsgId == 0L) { - // 貌似获取失败了,555 - LogCenter.log("无法获取被回复消息", Level.ERROR) - } - - if (data.containsKey("text")) { - data.checkAndThrow("qq", "time", "seq") - reply.replayMsgSeq = data["seq"].asLong - reply.sourceMsgText = data["text"].asString - reply.replyMsgTime = data["time"].asLong - reply.senderUid = data["qq"].asString.toLong() - } - element.replyElement = reply - return Result.success(element) - } - - private suspend fun createMusicElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("type") - - when (val type = data["type"].asString) { - "qq" -> { - data.checkAndThrow("id") - val id = data["id"].asString - if (!MusicHelper.tryShareQQMusicById(chatType, peerId.toLong(), msgId, id)) { - LogCenter.log("无法发送QQ音乐分享", Level.ERROR) - } - } - - "163" -> { - data.checkAndThrow("id") - val id = data["id"].asString - if (!MusicHelper.tryShare163MusicById(chatType, peerId.toLong(), msgId, id)) { - LogCenter.log("无法发送网易云音乐分享", Level.ERROR) - } - } - - "custom" -> { - data.checkAndThrow("url", "audio", "title") - ArkMsgSvc.tryShareMusic( - chatType, - peerId.toLong(), - msgId, - ArkAppInfo.QQMusic, - data["title"].asString, - data["singer"].asStringOrNull ?: "", - data["url"].asString, - data["image"].asStringOrNull ?: "", - data["audio"].asString - ) - } - - else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR) - } - - return Result.failure(ActionMsgException) - } - - private suspend fun createLocationElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("lat", "lon") - - val lat = data["lat"].asString.toDouble() - val lon = data["lon"].asString.toDouble() - - LbsSvc.tryShareLocation(chatType, peerId.toLong(), lat, lon).onFailure { - LogCenter.log("无法发送位置分享", Level.ERROR) - } - - return Result.failure(ActionMsgException) - } - - private suspend fun createContactElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("id") - val type = data["type"].asStringOrNull ?: data["kind"].asStringOrNull - val id = data["id"].asString - val elem = MsgElement() - - when (type) { - "private", "qq" -> { - val ark = ArkElement(CardSvc.getSharePrivateArkMsg(id.toLong()), null, null) - elem.arkElement = ark - } - - "group" -> { - val ark = ArkElement(GroupSvc.getShareTroopArkMsg(id.toLong()), null, null) - elem.arkElement = ark - } - - else -> throw IllegalParamsException("type") - } - - elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT - - return Result.success(elem) - } - - private suspend fun createShareElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("title", "url") - - val url = data["url"].asString - val image = if (data.containsKey("image")) { - data["image"].asString - } else { - val startWithPrefix = url.startsWith("http://") || url.startsWith("https://") - val endWithPrefix = url.startsWith("/") - "http://" + url.split("/")[if (startWithPrefix) 2 else 0] + if (!endWithPrefix) { - "/favicon.ico" - } else { - "favicon.ico" - } - } - val title = data["title"].asString - val content = data["content"].asStringOrNull - - val reqBody = oidb_cmd0xdc2.ReqBody() - val info = oidb_cmd0xb77.ReqBody() - info.appid.set(100446242L) - info.app_type.set(1) - info.msg_style.set(0) - info.recv_uin.set(peerId.toLong()) - val clientInfo = oidb_cmd0xb77.ClientInfo() - clientInfo.platform.set(1) - info.client_info.set(clientInfo) - val richMsgBody = oidb_cmd0xb77.RichMsgBody() - richMsgBody.using_ark.set(true) - richMsgBody.title.set(title) - richMsgBody.summary.set(content ?: url) - richMsgBody.brief.set("[分享] $title") - richMsgBody.url.set(url) - richMsgBody.picture_url.set(image) - info.ext_info.set(oidb_cmd0xb77.ExtInfo().also { - it.msg_seq.set(msgId) - }) - info.rich_msg_body.set(richMsgBody) - reqBody.msg_body.set(info) - val sendTo = oidb_cmd0xdc2.BatchSendReq() - when (chatType) { - MsgConstant.KCHATTYPEGROUP -> sendTo.send_type.set(1) - MsgConstant.KCHATTYPEC2C -> sendTo.send_type.set(0) - else -> return createTextElem( - chatType = chatType, - msgId = msgId, - peerId = peerId, - data = JsonObject(mapOf("text" to JsonPrimitive("[分享] $title\n地址: $url"))) - ) - } - sendTo.recv_uin.set(peerId.toLong()) - reqBody.batch_send_req.add(sendTo) - val app = AppRuntimeFetcher.appRuntime as QQAppInterface - val to = ToServiceMsg("mobileqq.service", app.currentAccountUin, "OidbSvc.0xdc2_34") - val oidb = oidb_sso.OIDBSSOPkg() - oidb.uint32_command.set(0xdc2) - oidb.uint32_service_type.set(34) - oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(reqBody.toByteArray())) - oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext())) - to.putWupBuffer(oidb.toByteArray()) - to.addAttribute("req_pb_protocol_flag", true) - app.sendToService(to) - return Result.failure(ActionMsgException) - } - - private suspend fun createAnonymousElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - return Result.failure(ActionMsgException) - } - - private suspend fun createPokeElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("type", "id") - val elem = MsgElement() - val face = FaceElement() - face.faceIndex = 0 - face.faceText = "" - face.faceType = 5 - face.packId = null - face.pokeType = data["type"].asInt - face.spokeSummary = "" - face.doubleHit = 0 - face.vaspokeId = data["id"].asInt - face.vaspokeName = "" - face.vaspokeMinver = "" - face.pokeStrength = (data["strength"].asIntOrNull ?: data["cnt"].asIntOrNull - ?: data["count"].asIntOrNull ?: data["time"].asIntOrNull ?: 0).also { - if (it < 0 || it > 3) throw IllegalParamsException("strength") - } - face.msgType = 0 - face.faceBubbleCount = 0 - face.oldVersionStr = "[截一戳]请使用最新版手机QQ体验新功能。" - face.pokeFlag = 0 - elem.elementType = MsgConstant.KELEMTYPEFACE - elem.faceElement = face - return Result.success(elem) - } - - private suspend fun createFaceElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("id") - - val serverId = data["id"].asInt - val big = (data["big"].asBooleanOrNull ?: false) || serverId == 394 - - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEFACE - val face = FaceElement() - - // 1 old face - // 2 normal face - // 3 super face - // 4 is market face - // 5 is vas poke - face.faceType = if (big) 3 else 2 - face.faceIndex = serverId - face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId)) - if (serverId == 394) { - face.stickerId = "40" - face.packId = "1" - face.sourceType = 1 - face.stickerType = 3 - face.randomType = 1 - face.resultId = data["result"].asStringOrNull ?: Random.nextInt(1..5).toString() - } else if (big) { - face.imageType = 0 - face.stickerId = "30" - face.packId = "1" - face.sourceType = 1 - face.stickerType = 1 - face.randomType = 1 - } else { - face.imageType = 0 - face.packId = "0" - } - elem.faceElement = face - - return Result.success(elem) - } - - private suspend fun createRpsElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEMARKETFACE - val market = MarketFaceElement( - 6, 1, 11415, 3, 0, 200, 200, - "[猜拳]", "83C8A293AE65CA140F348120A77448EE", "7de39febcf45e6db", - null, null, 0, 0, 0, 1, 0, - null, null, null, - "", null, null, - null, null, arrayListOf(MarketFaceSupportSize(200, 200)), null - ) - elem.marketFaceElement = market - return Result.success(elem) - } - - private suspend fun createDiceElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEMARKETFACE - val market = MarketFaceElement( - 6, 1, 11464, 3, 0, 200, 200, - "[骰子]", "4823d3adb15df08014ce5d6796b76ee1", "409e2a69b16918f9", - null, null, 0, 0, 0, 1, 0, - null, null, null, // jumpurl - "", null, null, - null, null, arrayListOf(MarketFaceSupportSize(200, 200)), null - ) - elem.marketFaceElement = market - return Result.success(elem) - } - - private suspend fun createMarkdownElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("content") - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEMARKDOWN - val markdown = MarkdownElement(data["content"].asString) - elem.markdownElement = markdown - return Result.success(elem) - } - - private suspend fun createVideoElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("file") - - val file = data["file"].asString.let { - val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase() - var file = if (md5.length == 32) { - FileUtils.getFileByMd5(it) - } else { - FileUtils.parseAndSave(it) - } - if (!file.exists() && data.containsKey("url")) { - file = FileUtils.parseAndSave(data["url"].asString) - } - return@let file - } - if (!file.exists()) { - throw LogicException("Video(${file.name}) file is not exists, please check your filename.") - } - val elem = MsgElement() - val video = VideoElement() - - video.videoMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) - - val msgService = NTServiceFetcher.kernelService.msgService!! - val originalPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - 5, 2, video.videoMd5, file.name, 1, 0, null, "", true - ) - ) - val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - 5, 1, video.videoMd5, file.name, 2, 0, null, "", true - ) - ) - if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( - originalPath - ) != file.length() - ) { - QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) - AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!) - } - - if (ShamrockConfig.enableOldBDH()) { - Transfer with when (chatType) { - MsgConstant.KCHATTYPEGROUP -> Troop(peerId) - MsgConstant.KCHATTYPEC2C -> Private(peerId) - else -> error("Not supported chatType($chatType) for VideoMsg") - } trans VideoResource(file, File(thumbPath.toString())) - } - - video.fileTime = AudioUtils.getVideoTime(file) - video.fileSize = file.length() - video.fileName = file.name - video.fileFormat = FileTransfer.VIDEO_FORMAT_MP4 - video.thumbSize = QQNTWrapperUtil.CppProxy.getFileSize(thumbPath).toInt() - val options = BitmapFactory.Options() - BitmapFactory.decodeFile(thumbPath, options) - video.thumbWidth = options.outWidth - video.thumbHeight = options.outHeight - video.thumbMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(thumbPath) - video.thumbPath = hashMapOf(0 to thumbPath) - - elem.videoElement = video - elem.elementType = MsgConstant.KELEMTYPEVIDEO - - return Result.success(elem) - } - - private suspend fun createAtElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { - if (chatType != MsgConstant.KCHATTYPEGROUP) { - return Result.failure(ActionMsgException) - } - data.checkAndThrow("qq") - - val elem = MsgElement() - val qqStr = data["qq"].asString - - val at = TextElement() - when (qqStr) { - "0", "all" -> { - at.content = "@全体成员" - at.atType = MsgConstant.ATTYPEALL - at.atNtUid = "0" - } - - "admin" -> { - at.content = "@管理员" - at.atRoleId = 1 - at.atType = MsgConstant.ATTYPEROLE - at.atNtUid = "0" - } - - "online" -> { - at.content = "@在线成员" - at.atType = MsgConstant.ATTYPEONLINE - at.atNtUid = "0" - } - - else -> { - val qq = qqStr.toLong() - val name = data["name"].asStringOrNull - if (name == null) { - val info = GroupSvc.getTroopMemberInfoByUinV2(peerId.toLong(), qq, true).onFailure { - LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR) - }.getOrNull() - if (info != null) { - at.content = "@${ - info.troopnick - .ifNullOrEmpty(info.friendnick) - .ifNullOrEmpty(qqStr) - }" - } else { - at.content = "@$qqStr" - } - } else { - at.content = "@$name" - } - at.atType = MsgConstant.ATTYPEONE - at.atNtUid = ContactHelper.getUidByUinAsync(qq) - } - } - - elem.textElement = at - elem.elementType = MsgConstant.KELEMTYPETEXT - - return Result.success(elem) - } - - private suspend fun createRecordElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - var file = data["file"].asStringOrNull?.let { - val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "") - .replace(" ", "") - .split(".")[0].lowercase() - if (md5.length == 32) { - LocalCacheHelper.getCachePttFile(md5) - } else { - FileUtils.parseAndSave(it) - } - } - if (file == null || (!file.exists() && data.containsKey("url"))) { - file = FileUtils.parseAndSave(data["url"].asString) - } - if (!file.exists()) { - return Result.failure(LogicException("Voice(${file.name}) file is not exists, please check your filename.")) - } - val isMagic = data["magic"].asStringOrNull == "1" - - val ptt = PttElement() - - when (AudioUtils.getMediaType(file)) { - MediaType.Silk -> { - LogCenter.log({ "Silk: $file" }, Level.DEBUG) - ptt.formatType = MsgConstant.KPTTFORMATTYPESILK - ptt.duration = QRoute.api(IAIOPttApi::class.java) - .getPttFileDuration(file.absolutePath) - } - - MediaType.Amr -> { - LogCenter.log({ "Amr: $file" }, Level.DEBUG) - ptt.duration = AudioUtils.getDurationSec(file) - ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR - } - - MediaType.Pcm -> { - LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG) - val result = AudioUtils.pcmToSilk(file) - ptt.duration = (result.second * 0.001).roundToInt() - file = result.first - ptt.formatType = MsgConstant.KPTTFORMATTYPESILK - } - - else -> { - LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG) - val result = AudioUtils.audioToSilk(file) - ptt.duration = runCatching { - QRoute.api(IAIOPttApi::class.java) - .getPttFileDuration(result.second.absolutePath) - }.getOrElse { - result.first - } - file = result.second - ptt.formatType = MsgConstant.KPTTFORMATTYPESILK - } - } - - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEPTT - ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) - - if (ShamrockConfig.enableOldBDH()) { - if (!(Transfer with when (chatType) { - MsgConstant.KCHATTYPEGROUP -> Troop(peerId) - MsgConstant.KCHATTYPEC2C -> Private(peerId) - MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Private(peerId) - else -> error("Not supported chatType($chatType) for RecordMsg") - } trans VoiceResource(file)) - ) { - return Result.failure(RuntimeException("上传语音失败: $file")) - } - - ptt.filePath = file.absolutePath - } else { - val msgService = NTServiceFetcher.kernelService.msgService!! - - val originalPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - MsgConstant.KELEMTYPEPTT, 0, ptt.md5HexStr, file.name, 1, 0, null, "", true - ) - ) - if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != file.length()) { - QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) - } - if (originalPath != null) { - ptt.filePath = originalPath - } else { - ptt.filePath = file.absolutePath - } - } - - ptt.canConvert2Text = true - ptt.fileId = 0 - ptt.fileUuid = "" - ptt.text = "" - - if (!isMagic) { - ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD - ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE - } else { - ptt.voiceType = MsgConstant.KPTTVOICETYPEVOICECHANGE - ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPEECHO - } - - elem.pttElement = ptt - - return Result.success(elem) - } - - private suspend fun createImageElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - val type = data["type"].asStringOrNull ?: "" - val isOriginal = type == "original" - val isFlash = type == "flash" - val filePath = data["file"].asStringOrNull - val url = data["url"].asStringOrNull - var file: File? = null - if (filePath != null) { - val md5 = filePath - .replace(regex = "[{}\\-]".toRegex(), replacement = "") - .split(".")[0].lowercase() - file = if (md5.length == 32) { - FileUtils.getFileByMd5(md5) - } else { - FileUtils.parseAndSave(filePath) - } - } - if ((file == null || !file.exists()) && url != null) { - file = FileUtils.parseAndSave(url) - } - if (file?.exists() == false) { - throw LogicException("Image(${file.name}) file is not exists, please check your filename.") - } - requireNotNull(file) - - if (ShamrockConfig.enableOldBDH()) { - Transfer with when (chatType) { - MsgConstant.KCHATTYPEGROUP -> Troop(peerId) - MsgConstant.KCHATTYPEC2C -> Private(peerId) - else -> error("Not supported chatType($chatType) for PictureMsg") - } trans PictureResource(file) - } - - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPEPIC - val pic = PicElement() - pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) - - val msgService = NTServiceFetcher.kernelService.msgService!! - val originalPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - 2, 0, pic.md5HexStr, file.name, 1, 0, null, "", true - ) - ) - if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( - originalPath - ) != file.length() - ) { - val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - 2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true - ) - ) - QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) - QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath) - } - - val options = BitmapFactory.Options() - options.inJustDecodeBounds = true - BitmapFactory.decodeFile(file.absolutePath, options) - val exifInterface = ExifInterface(file.absolutePath) - val orientation = exifInterface.getAttributeInt( - ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_UNDEFINED - ) - if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) { - pic.picWidth = options.outWidth - pic.picHeight = options.outHeight - } else { - pic.picWidth = options.outHeight - pic.picHeight = options.outWidth - } - pic.sourcePath = file.absolutePath - pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath) - pic.original = isOriginal - pic.picType = FileUtils.getPicType(file) - // GO-CQHTTP扩展参数 支持 - pic.picSubType = data["subType"].asIntOrNull ?: 0 - pic.isFlashPic = isFlash - - //if (PlatformUtils.getQQVersionCode() >= PlatformUtils.QQ_9_0_8_VER && !ShamrockConfig.enableOldBDH()) { - // pic.storeID = 1 - //} - - elem.picElement = pic - - return Result.success(elem) - } - - private suspend fun createTextElem( - chatType: Int, - msgId: Long, - peerId: String, - data: JsonObject - ): Result { - data.checkAndThrow("text") - val elem = MsgElement() - elem.elementType = MsgConstant.KELEMTYPETEXT - val text = TextElement() - text.content = data["text"].asString - elem.textElement = text - return Result.success(elem) - } - - private fun JsonObject.checkAndThrow(vararg key: String) { - key.forEach { - if (!containsKey(it)) throw ParamsException(it) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Files.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Files.kt deleted file mode 100644 index 8c3eb36..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Files.kt +++ /dev/null @@ -1,53 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.structures - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class FileUrl( - @SerialName("url") val url: String, -) - -@Serializable -data class GroupFileList( - @SerialName("files") val files: List, - @SerialName("folders") val folders: List, -) - -@Serializable -data class FileInfo( - @SerialName("group_id") val groupId: Long, - @SerialName("file_id") val fileId: String, - @SerialName("file_name") val fileName: String, - @SerialName("file_size") val fileSize: Long, - @SerialName("busid") val busid: Int, - @SerialName("upload_time") val uploadTime: Int, - @SerialName("dead_time") val deadTime: Int, - @SerialName("modify_time") val modifyTime: Int, - @SerialName("download_times") val downloadTimes: Int, - @SerialName("uploader") val uploadUin: Long, - @SerialName("upload_name") val uploadNick: String, - @SerialName("sha") val sha: String, - @SerialName("sha3") val sha3: String, - @SerialName("md5") val md5: String, - -) - -@Serializable -data class FolderInfo( - @SerialName("group_id") val groupId: Long, - @SerialName("folder_id") val folderId: String, - @SerialName("folder_name") val folderName: String, - @SerialName("total_file_count") val totalFileCount: Int, - @SerialName("create_time") val createTime: Int, - @SerialName("creator") val creator: Long, - @SerialName("creator_name") val creatorNick: String, -) - -@Serializable -data class FileSystemInfo( - @SerialName("file_count") val fileCount: Int, - @SerialName("limit_count") val fileLimitCount: Int, - @SerialName("used_space") val usedSpace: Long, - @SerialName("total_space") val totalSpace: Long, -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Group.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Group.kt deleted file mode 100644 index 83b18b7..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Group.kt +++ /dev/null @@ -1,30 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.structures - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@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 -) - -@Serializable -internal data class NotJoinedGroupInfo( - @SerialName("group_id") val groupId: Long, - @SerialName("max_member_cnt") val maxMember: Int, - @SerialName("member_count") val memberCount: Int, - @SerialName("group_name") val groupName: String, - @SerialName("group_desc") val groupDesc: String, - @SerialName("owner") val owner: Long, - @SerialName("create_time") val createTime: Long, - @SerialName("group_flag") val groupFlag: Int, - @SerialName("group_flag_ext") val groupFlagExt: Int, -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Guild.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Guild.kt deleted file mode 100644 index f18dde0..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/Guild.kt +++ /dev/null @@ -1,79 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.structures - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import moe.fuqiuluo.symbols.Protobuf - -@Serializable -data class GuildInfo( - @SerialName("guild_id") var guildId: Long, - @SerialName("guild_name") var guildName: String, - @SerialName("guild_display_id") var guildDisplayId: String, - @SerialName("profile") var profile: String, - @SerialName("status") var status: GuildStatus, - @SerialName("owner_id") var ownerId: Long, - @SerialName("shutup_expire_time") var shutUpTime: Long, - @SerialName("allow_search") var allowSearch: Boolean -) - -@Serializable -data class GuildStatus( - @SerialName("is_enable") var isEnable: Boolean, - @SerialName("is_banned") var isBanned: Boolean, - @SerialName("is_frozen") var isFrozen: Boolean -) - -@Serializable -data class GProChannelInfo( - @SerialName("owner_guild_id") val ownerGuildId: ULong, - @SerialName("channel_id") val channelId: Long, - @SerialName("channel_uin") val channelUin: Long, - @SerialName("guild_id") val guildId: String, - @SerialName("channel_type") val channelType: Int, - @SerialName("channel_name") val channelName: String, - @SerialName("create_time") val createTime: Long, - @SerialName("max_member_count") val maxMemberCount: Int, - @SerialName("creator_tiny_id") val creatorTinyId: Long, - @SerialName("talk_permission") val talkPermission: Int, - @SerialName("visible_type") val visibleType: Int, - @SerialName("current_slow_mode") val currentSlowMode: Int, - @SerialName("slow_modes") val slowModes: List, - @SerialName("icon_url") val appIconUrl: String? = null, - @SerialName("jump_switch") val jumpSwitch: Int = Int.MIN_VALUE, - @SerialName("jump_type") val jumpType: Int = Int.MIN_VALUE, - @SerialName("jump_url") val jumpUrl: String? = null, - @SerialName("category_id") val categoryId: Long = Long.MIN_VALUE, - @SerialName("my_talk_permission") val myTalkPermission: Int = Int.MIN_VALUE, -) - -@Serializable -data class SlowModeInfo( - @SerialName("slow_mode_key") val slowModeKey: Int, - @SerialName("slow_mode_text") val slowModeText: String, - @SerialName("speak_frequency") val speakFrequency: Int, - @SerialName("slow_mode_circle") val slowModeCircle: Int -) - -@Serializable -data class GetGuildMemberListNextToken( - @SerialName("start_index") val startIndex: Long, - @SerialName("role_index") val roleIndex: Long, - @SerialName("seq") val seq: Int, - @SerialName("finish") val finish: Boolean -): Protobuf - -@Serializable -data class GuildMemberInfo( - @SerialName("tiny_id") val tinyId: Long, - @SerialName("title") val title: String, - @SerialName("nickname") val nickname: String, - @SerialName("role_id") val roleId: Long, - @SerialName("role_name") val roleName: String, - @SerialName("role_color") val roleColor: Long, - @SerialName("join_time") val joinTime: Long, - @SerialName("robot_type") val robotType: Int, - @SerialName("type") val type: Int, - @SerialName("in_black") val inBlack: Boolean, - @SerialName("platform") val platform: Int, -) - diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/RichMedia.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/RichMedia.kt deleted file mode 100644 index ab20378..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/structures/RichMedia.kt +++ /dev/null @@ -1,20 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.structures - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class UploadResult( - @SerialName("files") val files: List -) - -@Serializable -data class CommFileInfo( - @SerialName("mode_id") val modeId: Long, - @SerialName("name") val fileName: String, - @SerialName("size") val fileSize: Long, - @SerialName("md5") val md5: String, - @SerialName("uuid") val uuid: String, - @SerialName("sub_id") val subId: String, - @SerialName("sha") val sha: String, -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/FileTransfer.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/FileTransfer.kt deleted file mode 100644 index ecda50b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/FileTransfer.kt +++ /dev/null @@ -1,190 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.qqinterface.servlet.transfile - -import com.tencent.mobileqq.transfile.BaseTransProcessor -import com.tencent.mobileqq.transfile.FileMsg -import com.tencent.mobileqq.transfile.TransferRequest -import com.tencent.mobileqq.transfile.api.ITransFileController -import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.utils.MD5 -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import mqq.app.AppRuntime -import java.io.File -import kotlin.coroutines.resume -import kotlin.math.abs -import kotlin.random.Random - -internal abstract class FileTransfer { - suspend fun transC2CResource( - peerId: String, - file: File, - fileType: Int, busiType: Int, - wait: Boolean = true, - builder: (TransferRequest) -> Unit - ): Boolean { - val runtime = AppRuntimeFetcher.appRuntime - val transferRequest = TransferRequest() - transferRequest.needSendMsg = false - transferRequest.mSelfUin = runtime.account - transferRequest.mPeerUin = peerId - transferRequest.mSecondId = runtime.currentAccountUin - transferRequest.mUinType = FileMsg.UIN_BUDDY - transferRequest.mFileType = fileType - transferRequest.mUniseq = createMessageUniseq() - transferRequest.mIsUp = true - builder(transferRequest) - transferRequest.mBusiType = busiType - transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath) - transferRequest.mLocalPath = file.absolutePath - return transAndWait(runtime, transferRequest, wait) - } - - suspend fun transTroopResource( - groupId: String, - file: File, - fileType: Int, busiType: Int, - wait: Boolean = true, - builder: (TransferRequest) -> Unit - ): Boolean { - val runtime = AppRuntimeFetcher.appRuntime - val transferRequest = TransferRequest() - transferRequest.needSendMsg = false - transferRequest.mSelfUin = runtime.account - transferRequest.mPeerUin = groupId - transferRequest.mSecondId = runtime.currentAccountUin - transferRequest.mUinType = FileMsg.UIN_TROOP - transferRequest.mFileType = fileType - transferRequest.mUniseq = createMessageUniseq() - transferRequest.mIsUp = true - builder(transferRequest) - transferRequest.mBusiType = busiType - transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath) - transferRequest.mLocalPath = file.absolutePath - return transAndWait(runtime, transferRequest, wait) - } - - private suspend fun transAndWait( - runtime: AppRuntime, - transferRequest: TransferRequest, - wait: Boolean - ): Boolean { - return withTimeoutOrNull(60_000) { - val service = runtime.getRuntimeService(ITransFileController::class.java, "all") - if(service.transferAsync(transferRequest)) { - if (!wait) { // 如果无需等待直接返回 - return@withTimeoutOrNull true - } - suspendCancellableCoroutine { continuation -> - GlobalScope.launch { - lateinit var processor: IHttpCommunicatorListener - while ( - //service.findProcessor( - // transferRequest.keyForTransfer // uin + uniseq - //) != null - service.containsProcessor(runtime.currentAccountUin, transferRequest.mUniseq) - // 如果上传处理器依旧存在,说明没有上传成功 - && service.isWorking.get() - ) { - processor = service.findProcessor(runtime.currentAccountUin, transferRequest.mUniseq) - delay(100) - } - if (processor is BaseTransProcessor && processor.file != null) { - val fileMsg = processor.file - LogCenter.log("[OldBDH] 资源上传结束(fileId = ${fileMsg.fileID}, fileKey = ${fileMsg.fileKey}, path = ${fileMsg.filePath})") - } - continuation.resume(true) - } - // 实现取消上传器 - // 目前没什么用 - continuation.invokeOnCancellation { - continuation.resume(false) - } - } - } else true - } ?: false - } - - companion object { - const val SEND_MSG_BUSINESS_TYPE_AIO_ALBUM_PIC = 1031 - const val SEND_MSG_BUSINESS_TYPE_AIO_KEY_WORD_PIC = 1046 - const val SEND_MSG_BUSINESS_TYPE_AIO_QZONE_PIC = 1045 - const val SEND_MSG_BUSINESS_TYPE_ALBUM_PIC = 1007 - const val SEND_MSG_BUSINESS_TYPE_BLESS = 1056 - const val SEND_MSG_BUSINESS_TYPE_CAPTURE_PIC = 1008 - const val SEND_MSG_BUSINESS_TYPE_COMMEN_FALSH_PIC = 1040 - const val SEND_MSG_BUSINESS_TYPE_CUSTOM = 1006 - const val SEND_MSG_BUSINESS_TYPE_DOUTU_PIC = 1044 - const val SEND_MSG_BUSINESS_TYPE_FALSH_PIC = 1039 - const val SEND_MSG_BUSINESS_TYPE_FAST_IMAGE = 1037 - const val SEND_MSG_BUSINESS_TYPE_FORWARD_EDIT = 1048 - const val SEND_MSG_BUSINESS_TYPE_FORWARD_PIC = 1009 - const val SEND_MSG_BUSINESS_TYPE_FULL_SCREEN_ESSENCE = 1057 - const val SEND_MSG_BUSINESS_TYPE_GALEERY_PIC = 1041 - const val SEND_MSG_BUSINESS_TYPE_GAME_CENTER_STRATEGY = 1058 - const val SEND_MSG_BUSINESS_TYPE_HOT_PIC = 1042 - const val SEND_MSG_BUSINESS_TYPE_MIXED_PICS = 1043 - const val SEND_MSG_BUSINESS_TYPE_PIC_AIO_ALBUM = 1052 - const val SEND_MSG_BUSINESS_TYPE_PIC_CAMERA = 1050 - const val SEND_MSG_BUSINESS_TYPE_PIC_FAV = 1053 - const val SEND_MSG_BUSINESS_TYPE_PIC_SCREEN = 1027 - const val SEND_MSG_BUSINESS_TYPE_PIC_SHARE = 1030 - const val SEND_MSG_BUSINESS_TYPE_PIC_TAB_CAMERA = 1051 - const val SEND_MSG_BUSINESS_TYPE_QQPINYIN_SEND_PIC = 1038 - const val SEND_MSG_BUSINESS_TYPE_RECOMMENDED_STICKER = 1047 - const val SEND_MSG_BUSINESS_TYPE_RELATED_EMOTION = 1054 - const val SEND_MSG_BUSINESS_TYPE_SHOWLOVE = 1036 - const val SEND_MSG_BUSINESS_TYPE_SOGOU_SEND_PIC = 1034 - const val SEND_MSG_BUSINESS_TYPE_TROOP_BAR = 1035 - const val SEND_MSG_BUSINESS_TYPE_WLAN_RECV_NOTIFY = 1055 - const val SEND_MSG_BUSINESS_TYPE_ZHITU_PIC = 1049 - const val SEND_MSG_BUSINESS_TYPE_ZPLAN_EMOTICON_GIF = 1060 - const val SEND_MSG_BUSINESS_TYPE_ZPLAN_PIC = 1059 - - const val VIDEO_FORMAT_AFS = 7 - const val VIDEO_FORMAT_AVI = 1 - const val VIDEO_FORMAT_MKV = 4 - const val VIDEO_FORMAT_MOD = 9 - const val VIDEO_FORMAT_MOV = 8 - const val VIDEO_FORMAT_MP4 = 2 - const val VIDEO_FORMAT_MTS = 11 - const val VIDEO_FORMAT_RM = 6 - const val VIDEO_FORMAT_RMVB = 5 - const val VIDEO_FORMAT_TS = 10 - const val VIDEO_FORMAT_WMV = 3 - - const val BUSI_TYPE_GUILD_VIDEO = 4601 - const val BUSI_TYPE_MULTI_FORWARD_VIDEO = 1010 - const val BUSI_TYPE_PUBACCOUNT_PERM_VIDEO = 1009 - const val BUSI_TYPE_PUBACCOUNT_TEMP_VIDEO = 1007 - const val BUSI_TYPE_SHORT_VIDEO = 1 - const val BUSI_TYPE_SHORT_VIDEO_PTV = 2 - const val BUSI_TYPE_VIDEO = 0 - const val BUSI_TYPE_VIDEO_EMOTICON_PIC = 1022 - const val BUSI_TYPE_VIDEO_EMOTICON_VIDEO = 1021 - - const val TRANSFILE_TYPE_PIC = 1 - const val TRANSFILE_TYPE_PIC_EMO = 65538 - const val TRANSFILE_TYPE_PIC_THUMB = 65537 - const val TRANSFILE_TYPE_PISMA = 49 - const val TRANSFILE_TYPE_RAWPIC = 131075 - - const val TRANSFILE_TYPE_PROFILE_COVER = 35 - const val TRANSFILE_TYPE_PTT = 2 - const val TRANSFILE_TYPE_PTT_SLICE_TO_TEXT = 327696 - const val TRANSFILE_TYPE_QQHEAD_PIC = 131074 - - internal fun createMessageUniseq(time: Long = System.currentTimeMillis()): Long { - var uniseq = (time / 1000).toInt().toLong() - uniseq = uniseq shl 32 or abs(Random.nextInt()).toLong() - return uniseq - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/NtV2RichMediaSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/NtV2RichMediaSvc.kt deleted file mode 100644 index f355ad7..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/NtV2RichMediaSvc.kt +++ /dev/null @@ -1,526 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.transfile - -import android.graphics.BitmapFactory -import androidx.exifinterface.media.ExifInterface -import com.tencent.mobileqq.qroute.QRoute -import com.tencent.qqnt.aio.adapter.api.IAIOPttApi -import com.tencent.qqnt.kernel.nativeinterface.CommonFileInfo -import com.tencent.qqnt.kernel.nativeinterface.Contact -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgElement -import com.tencent.qqnt.kernel.nativeinterface.PicElement -import com.tencent.qqnt.kernel.nativeinterface.PttElement -import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil -import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo -import com.tencent.qqnt.kernel.nativeinterface.VideoElement -import kotlinx.atomicfu.atomic -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.qqinterface.servlet.transfile.data.TryUpPicData -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.tools.hex2ByteArray -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.utils.AudioUtils -import moe.fuqiuluo.shamrock.utils.FileUtils -import moe.fuqiuluo.shamrock.utils.MediaType -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import moe.fuqiuluo.shamrock.xposed.helper.msgService -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.auto.toByteArray -import protobuf.oidb.TrpcOidb -import protobuf.oidb.cmd0x11c5.ClientMeta -import protobuf.oidb.cmd0x11c5.CodecConfigReq -import protobuf.oidb.cmd0x11c5.CommonHead -import protobuf.oidb.cmd0x11c5.DownloadExt -import protobuf.oidb.cmd0x11c5.DownloadReq -import protobuf.oidb.cmd0x11c5.FileInfo -import protobuf.oidb.cmd0x11c5.FileType -import protobuf.oidb.cmd0x11c5.IndexNode -import protobuf.oidb.cmd0x11c5.MultiMediaReqHead -import protobuf.oidb.cmd0x11c5.NtV2RichMediaReq -import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp -import protobuf.oidb.cmd0x11c5.SceneInfo -import protobuf.oidb.cmd0x11c5.UploadInfo -import protobuf.oidb.cmd0x11c5.UploadReq -import protobuf.oidb.cmd0x11c5.UploadRsp -import protobuf.oidb.cmd0x11c5.VideoDownloadExt -import protobuf.oidb.cmd0x388.Cmd0x388ReqBody -import protobuf.oidb.cmd0x388.Cmd0x388RspBody -import protobuf.oidb.cmd0x388.TryUpImgReq -import java.io.File -import kotlin.coroutines.resume -import kotlin.math.roundToInt -import kotlin.random.Random -import kotlin.random.nextUInt -import kotlin.random.nextULong -import kotlin.time.Duration - -internal object NtV2RichMediaSvc: BaseSvc() { - private val requestIdSeq = atomic(2L) - - private fun fetchGroupResUploadTo(): String { - return ShamrockConfig.getUpResGroup().ifEmpty { "100000000" } - } - - suspend fun tryUploadResourceByNt( - chatType: Int, - elementType: Int, - resources: ArrayList, - timeout: Duration, - retryCnt: Int = 5 - ): Result> { - return internalTryUploadResourceByNt(chatType, elementType, resources, timeout).onFailure { - if (retryCnt > 0) { - return tryUploadResourceByNt(chatType, elementType, resources, timeout, retryCnt - 1) - } - } - } - - /** - * 批量上传图片 - */ - private suspend fun internalTryUploadResourceByNt( - chatType: Int, - elementType: Int, - resources: ArrayList, - timeout: Duration - ): Result> { - require(resources.size in 1 .. 10) { "imageFiles.size() must be in 1 .. 10" } - - val messages = resources.map { file -> - val elem = MsgElement() - elem.elementType = elementType - when(elementType) { - MsgConstant.KELEMTYPEPIC -> { - val pic = PicElement() - pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) - val msgService = NTServiceFetcher.kernelService.msgService!! - val originalPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - 2, 0, pic.md5HexStr, file.name, 1, 0, null, "", true - ) - ) - if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( - originalPath - ) != file.length() - ) { - val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - 2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true - ) - ) - QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) - QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath) - } - val options = BitmapFactory.Options() - options.inJustDecodeBounds = true - BitmapFactory.decodeFile(file.absolutePath, options) - val exifInterface = ExifInterface(file.absolutePath) - val orientation = exifInterface.getAttributeInt( - ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_UNDEFINED - ) - if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) { - pic.picWidth = options.outWidth - pic.picHeight = options.outHeight - } else { - pic.picWidth = options.outHeight - pic.picHeight = options.outWidth - } - pic.sourcePath = file.absolutePath - pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath) - pic.original = true - pic.picType = FileUtils.getPicType(file) - elem.picElement = pic - } - MsgConstant.KELEMTYPEPTT -> { - require(resources.size == 1) // 语音只能单个上传 - var pttFile = file - val ptt = PttElement() - when (AudioUtils.getMediaType(pttFile)) { - MediaType.Silk -> { - ptt.formatType = MsgConstant.KPTTFORMATTYPESILK - ptt.duration = QRoute.api(IAIOPttApi::class.java) - .getPttFileDuration(pttFile.absolutePath) - } - MediaType.Amr -> { - ptt.duration = AudioUtils.getDurationSec(pttFile) - ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR - } - MediaType.Pcm -> { - val result = AudioUtils.pcmToSilk(pttFile) - ptt.duration = (result.second * 0.001).roundToInt() - pttFile = result.first - ptt.formatType = MsgConstant.KPTTFORMATTYPESILK - } - - else -> { - val result = AudioUtils.audioToSilk(pttFile) - ptt.duration = runCatching { - QRoute.api(IAIOPttApi::class.java) - .getPttFileDuration(result.second.absolutePath) - }.getOrElse { - result.first - } - pttFile = result.second - ptt.formatType = MsgConstant.KPTTFORMATTYPESILK - } - } - ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(pttFile.absolutePath) - val msgService = NTServiceFetcher.kernelService.msgService!! - val originalPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - MsgConstant.KELEMTYPEPTT, 0, ptt.md5HexStr, file.name, 1, 0, null, "", true - ) - ) - if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != pttFile.length()) { - QQNTWrapperUtil.CppProxy.copyFile(pttFile.absolutePath, originalPath) - } - if (originalPath != null) { - ptt.filePath = originalPath - } else { - ptt.filePath = pttFile.absolutePath - } - ptt.canConvert2Text = true - ptt.fileId = 0 - ptt.fileUuid = "" - ptt.text = "" - ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD - ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE - elem.pttElement = ptt - } - MsgConstant.KELEMTYPEVIDEO -> { - require(resources.size == 1) // 视频只能单个上传 - val video = VideoElement() - video.videoMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) - val msgService = NTServiceFetcher.kernelService.msgService!! - val originalPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - 5, 2, video.videoMd5, file.name, 1, 0, null, "", true - ) - ) - val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( - RichMediaFilePathInfo( - 5, 1, video.videoMd5, file.name, 2, 0, null, "", true - ) - ) - if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( - originalPath - ) != file.length() - ) { - QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) - AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!) - } - video.fileTime = AudioUtils.getVideoTime(file) - video.fileSize = file.length() - video.fileName = file.name - video.fileFormat = FileTransfer.VIDEO_FORMAT_MP4 - video.thumbSize = QQNTWrapperUtil.CppProxy.getFileSize(thumbPath).toInt() - val options = BitmapFactory.Options() - BitmapFactory.decodeFile(thumbPath, options) - video.thumbWidth = options.outWidth - video.thumbHeight = options.outHeight - video.thumbMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(thumbPath) - video.thumbPath = hashMapOf(0 to thumbPath) - elem.videoElement = video - } - - /*MsgConstant.KELEMTYPEFILE -> { - require(resources.size == 1) // 文件只能单个上传 - val fileElement = FileElement() - fileElement.fileMd5 = "" - fileElement.fileName = file.name - fileElement.filePath = file.absolutePath - fileElement.fileSize = file.length() - fileElement.picWidth = 0 - fileElement.picHeight = 0 - fileElement.videoDuration = 0 - fileElement.picThumbPath = HashMap() - fileElement.expireTime = 0L - fileElement.fileSha = "" - fileElement.fileSha3 = "" - fileElement.file10MMd5 = "" - when (TransfileHelper.getExtensionId(file.name)) { - 0 -> { - val wh = QRoute.api(IMsgUtilApi::class.java) - .getPicSizeByPath(file.absolutePath) - fileElement.picWidth = wh.first - fileElement.picHeight = wh.second - fileElement.picThumbPath[750] = file.absolutePath - } - 2 -> { - val thumbPic = FileUtils.getFileByMd5(MD5.genFileMd5Hex(file.absolutePath)) - withContext(Dispatchers.IO) { - val fileOutputStream = FileOutputStream(thumbPic) - val retriever = MediaMetadataRetriever() - retriever.setDataSource(fileElement.filePath) - retriever.frameAtTime?.compress(Bitmap.CompressFormat.JPEG, 60, fileOutputStream) - fileOutputStream.flush() - fileOutputStream.close() - } - val options = BitmapFactory.Options() - BitmapFactory.decodeFile(thumbPic.absolutePath, options) - fileElement.picHeight = options.outHeight - fileElement.picWidth = options.outWidth - fileElement.picThumbPath = hashMapOf(750 to thumbPic.absolutePath) - } - } - elem.fileElement = fileElement - }*/ - else -> throw IllegalArgumentException("unsupported elementType: $elementType") - } - return@map elem - } - if (messages.isEmpty()) { - return Result.failure(Exception("no valid image files")) - } - val contact = when(chatType) { - MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, TicketSvc.getUin()) - else -> Contact(chatType, fetchGroupResUploadTo(), null) - } - val result = mutableListOf() - withTimeoutOrNull(timeout) { - suspendCancellableCoroutine { - val uniseq = MessageHelper.generateMsgId(chatType) - RichMediaUploadHandler.registerListener(uniseq.qqMsgId) upload@{ - if (uniseq.qqMsgId == msgId) { - result.add(commonFileInfo) - } - if (result.size == resources.size) { - it.resume(true) - return@upload true - } - return@upload false - } - MessageHelper.sendMessageWithMsgId( - contact = contact, - message = ArrayList(messages), - uniseq = uniseq.qqMsgId - ) { _, _ -> - if (contact.chatType == MsgConstant.KCHATTYPEGROUP && contact.peerUid == "100000000") { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val msgService = sessionService.msgService - msgService.deleteMsg(contact, arrayListOf(uniseq.qqMsgId), null) - } - } - it.invokeOnCancellation { - RichMediaUploadHandler.removeListener(uniseq.qqMsgId) - } - } - } - - if (result.isEmpty()) { - return Result.failure(Exception("upload failed")) - } - - return Result.success(result) - } - - /** - * 获取NT图片的RKEY - */ - suspend fun getNtPicRKey( - fileId: String, - md5: String, - sha: String, - fileSize: ULong, - width: UInt, - height: UInt, - sceneBuilder: suspend SceneInfo.() -> Unit - ): Result { - runCatching { - val req = NtV2RichMediaReq( - head = MultiMediaReqHead( - commonHead = CommonHead( - requestId = requestIdSeq.incrementAndGet().toULong(), - cmd = 200u - ), - sceneInfo = SceneInfo( - requestType = 2u, - businessType = 1u, - ).apply { - sceneBuilder() - }, - clientMeta = ClientMeta(2u) - ), - download = DownloadReq( - IndexNode( - FileInfo( - fileSize = fileSize, - md5 = md5.lowercase(), - sha1 = sha.lowercase(), - name = "${md5}.jpg", - fileType = FileType( - fileType = 1u, - picFormat = 1000u, - videoFormat = 0u, - voiceFormat = 0u - ), - width = width, - height = height, - time = 0u, - original = 1u - ), - fileUuid = fileId, - storeId = 1u, - uploadTime = 0u, - ttl = 0u, - subType = 0u, - storeAppId = 0u - ), - DownloadExt( - video = VideoDownloadExt( - busiType = 0u, - subBusiType = 0u, - msgCodecConfig = CodecConfigReq( - platformChipinfo = "", - osVer = "", - deviceName = "" - ), - flag = 1u - ) - ) - ) - ).toByteArray() - val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 4549, 200, req, true)?.slice(4) - buffer?.decodeProtobuf()?.buffer?.decodeProtobuf()?.download?.rkeyParam?.let { - return Result.success(it) - } - }.onFailure { - return Result.failure(it) - } - return Result.failure(Exception("unable to get c2c nt pic")) - } - - suspend fun requestUploadNtPic( - file: File, - md5: String, - sha: String, - name: String, - width: UInt, - height: UInt, - retryCnt: Int, - sceneBuilder: suspend SceneInfo.() -> Unit - ): Result { - return runCatching { - requestUploadNtPic(file, md5, sha, name, width, height, sceneBuilder).getOrThrow() - }.onFailure { - if (retryCnt > 0) { - return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, sceneBuilder) - } - } - } - - private suspend fun requestUploadNtPic( - file: File, - md5: String, - sha: String, - name: String, - width: UInt, - height: UInt, - sceneBuilder: suspend SceneInfo.() -> Unit - ): Result { - val req = NtV2RichMediaReq( - head = MultiMediaReqHead( - commonHead = CommonHead( - requestId = requestIdSeq.incrementAndGet().toULong(), - cmd = 100u - ), - sceneInfo = SceneInfo( - requestType = 2u, - businessType = 1u, - ).apply { - sceneBuilder() - }, - clientMeta = ClientMeta(2u) - ), - upload = UploadReq( - listOf(UploadInfo( - FileInfo( - fileSize = file.length().toULong(), - md5 = md5, - sha1 = sha, - name = name, - fileType = FileType( - fileType = 1u, - picFormat = 1000u, - videoFormat = 0u, - voiceFormat = 0u - ), - width = width, - height = height, - time = 0u, - original = 1u - ), - subFileType = 0u - )), - tryFastUploadCompleted = true, - srvSendMsg = false, - clientRandomId = Random.nextULong(), - compatQMsgSceneType = 1u, - clientSeq = Random.nextUInt(), - noNeedCompatMsg = false - ) - ).toByteArray() - val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3_000)?.slice(4) - ?: return Result.failure(Exception("no response: timeout")) - val rspBuffer = buffer.decodeProtobuf().buffer - val rsp = rspBuffer.decodeProtobuf() - if (rsp.upload == null) { - return Result.failure(Exception("unable to request upload nt pic: ${rsp.head}")) - } - return Result.success(rsp.upload!!) - } - - /** - * 使用OldBDH获取图片上传状态以及图片上传服务器 - */ - suspend fun requestUploadGroupPic( - groupId: ULong, - md5: String, - fileSize: ULong, - width: UInt, - height: UInt, - ): Result { - return runCatching { - val rspBuffer = sendBufferAW("ImgStore.GroupPicUp", true, Cmd0x388ReqBody( - netType = 3, - subCmd = 1, - msgTryUpImg = arrayListOf( - TryUpImgReq( - groupCode = groupId.toLong(), - srcUin = TicketSvc.getLongUin(), - fileMd5 = md5.hex2ByteArray(), - fileSize = fileSize.toLong(), - fileName = "$md5.jpg", - srcTerm = 2, - platformType = 9, - buType = 212, - picWidth = width.toInt(), - picHeight = height.toInt(), - picType = 1000, - buildVer = "1.0.0", - originalPic = 1, - fileIndex = byteArrayOf(), - srvUpload = 0 - ) - ), - ).toByteArray())!! - val rsp = rspBuffer.decodeProtobuf() - .msgTryUpImgRsp!!.first() - TryUpPicData( - uKey = rsp.ukey, - exist = rsp.fileExist, - fileId = rsp.fileId.toULong(), - upIp = rsp.upIp, - upPort = rsp.upPort - ) - } - } -} - diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichMediaUploadHandler.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichMediaUploadHandler.kt deleted file mode 100644 index e5f01ae..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichMediaUploadHandler.kt +++ /dev/null @@ -1,27 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.transfile - -import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo - -internal object RichMediaUploadHandler { - private val listeners by lazy { - mutableMapOf Boolean>() - } - - fun registerListener(key: Long, value: FileTransNotifyInfo.() -> Boolean) { - listeners[key] = value - } - - fun removeListener(key: Long) { - listeners.remove(key) - } - - fun notify(info: FileTransNotifyInfo): Boolean { - listeners[info.msgId]?.let { - if (it(info)) { - listeners.remove(info.msgId) - return true - } - } - return false - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt deleted file mode 100644 index f37a6cc..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt +++ /dev/null @@ -1,431 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) -package moe.fuqiuluo.qqinterface.servlet.transfile - -import com.tencent.mobileqq.pb.ByteStringMicro -import com.tencent.mobileqq.transfile.FileMsg -import com.tencent.mobileqq.transfile.api.IProtoReqManager -import com.tencent.mobileqq.transfile.protohandler.RichProto -import com.tencent.mobileqq.transfile.protohandler.RichProtoProc -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.serialization.ExperimentalSerializationApi -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc.getNtPicRKey -import moe.fuqiuluo.shamrock.helper.ContactHelper -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.tools.hex2ByteArray -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.tools.toHexString -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.symbols.decodeProtobuf -import mqq.app.MobileQQ -import protobuf.auto.toByteArray -import protobuf.oidb.cmd0x11c5.C2CUserInfo -import protobuf.oidb.cmd0x11c5.ChannelUserInfo -import protobuf.oidb.cmd0x11c5.GroupUserInfo -import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo -import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq -import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody -import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody -import tencent.im.cs.cmd0x346.cmd0x346 -import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 -import tencent.im.oidb.cmd0xe37.cmd0xe37 -import tencent.im.oidb.oidb_sso -import kotlin.coroutines.resume - -private const val GPRO_PIC = "gchat.qpic.cn" -private const val MULTIMEDIA_DOMAIN = "multimedia.nt.qq.com.cn" -private const val C2C_PIC = "c2cpicdw.qpic.cn" - -internal object RichProtoSvc: BaseSvc() { - suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String { - val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, Oidb0xfc2ReqBody( - msgCmd = 1200, - msgBusType = 4202, - msgChannelInfo = Oidb0xfc2ChannelInfo( - guildId = peerId.toULong(), - channelId = channelId.toULong() - ), - msgTerminalType = 2, - msgApplyDownloadReq = Oidb0xfc2MsgApplyDownloadReq( - fieldId = fileId, - supportEncrypt = 0 - ) - ).toByteArray()) ?: return "" - val body = oidb_sso.OIDBSSOPkg() - body.mergeFrom(buffer.slice(4)) - body.bytes_bodybuffer - .get().toByteArray() - .decodeProtobuf() - .msgApplyDownloadRsp?.let { - it.msgDownloadInfo?.let { - return "https://${it.downloadDomain}${it.downloadUrl}&fname=$fileId&isthumb=0" - } - } - return "" - } - - suspend fun getGroupFileDownUrl( - peerId: Long, - fileId: String, - bizId: Int = 102 - ): String { - val buffer = sendOidbAW("OidbSvcTrpcTcp.0x6d6_2", 1750, 2, oidb_0x6d6.ReqBody().apply { - download_file_req.set(oidb_0x6d6.DownloadFileReqBody().apply { - uint64_group_code.set(peerId) - uint32_app_id.set(3) - uint32_bus_id.set(bizId) - str_file_id.set(fileId) - }) - }.toByteArray()) ?: return "" - val body = oidb_sso.OIDBSSOPkg() - body.mergeFrom(buffer.slice(4)) - val result = oidb_0x6d6.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) - if (body.uint32_result.get() != 0 - || result.download_file_rsp.int32_ret_code.get() != 0) { - return "" - } - - val domain = if (!result.download_file_rsp.str_download_dns.has()) - ("https://" + result.download_file_rsp.str_download_ip.get()) - else ("http://" + result.download_file_rsp.str_download_dns.get().toByteArray().decodeToString()) - val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString() - val appId = MobileQQ.getMobileQQ().appId - val version = PlatformUtils.getQQVersion(MobileQQ.getContext()) - - return "$domain/ftn_handler/$downloadUrl/?fname=$fileId&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk" - } - - suspend fun getC2CFileDownUrl( - fileId: String, - subId: String, - retryCnt: Int = 0 - ): String { - val buffer = sendOidbAW("OidbSvc.0xe37_1200", 3639, 1200, cmd0xe37.Req0xe37().apply { - bytes_cmd_0x346_req_body.set(ByteStringMicro.copyFrom(cmd0x346.ReqBody().apply { - uint32_cmd.set(1200) - uint32_seq.set(1) - msg_apply_download_req.set(cmd0x346.ApplyDownloadReq().apply { - uint64_uin.set(app.longAccountUin) - bytes_uuid.set(ByteStringMicro.copyFrom(fileId.toByteArray())) - uint32_owner_type.set(2) - str_fileidcrc.set(subId) - - }) - uint32_business_id.set(3) - uint32_client_type.set(104) - uint32_flag_support_mediaplatform.set(1) - msg_extension_req.set(cmd0x346.ExtensionReq().apply { - uint32_download_url_type.set(1) - }) - }.toByteArray())) - }.toByteArray()) - - if (buffer == null) { - if (retryCnt < 5) { - return getC2CFileDownUrl(fileId, subId, retryCnt + 1) - } - return "" - } else { - val body = oidb_sso.OIDBSSOPkg() - body.mergeFrom(buffer.slice(4)) - val result = cmd0x346.RspBody().mergeFrom(cmd0xe37.Resp0xe37().mergeFrom( - body.bytes_bodybuffer.get().toByteArray() - ).bytes_cmd_0x346_rsp_body.get().toByteArray()) - if (body.uint32_result.get() != 0 || - result.msg_apply_download_rsp.int32_ret_code.has() && result.msg_apply_download_rsp.int32_ret_code.get() != 0) { - return "" - } - - val oldData = result.msg_apply_download_rsp.msg_download_info - //val newData = result[14, 40] NTQQ 文件信息 - - val domain = if (oldData.str_download_dns.has()) ("https://" + oldData.str_download_dns.get()) else ("http://" + oldData.rpt_str_downloadip_list.get().first()) - val params = oldData.str_download_url.get() - val appId = MobileQQ.getMobileQQ().appId - val version = PlatformUtils.getQQVersion(MobileQQ.getContext()) - - return "$domain$params&isthumb=0&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk" - } - } - - suspend fun getGroupPicDownUrl( - originalUrl: String, - md5: String, - peer: String = "", - fileId: String = "", - sha: String = "", - fileSize: ULong = 0uL, - width: UInt = 0u, - height: UInt = 0u - ): String { - val isNtServer = originalUrl.startsWith("/download") - val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC - if (originalUrl.isNotEmpty()) { - if (isNtServer && !originalUrl.contains("rkey=")) { - getNtPicRKey( - fileId = fileId, - md5 = md5, - sha = sha, - fileSize = fileSize, - width = width, - height = height - ) { - sceneType = 2u - grp = GroupUserInfo(peer.toULong()) - }.onSuccess { - return "https://$domain$originalUrl$it" - }.onFailure { - LogCenter.log("getGroupPicDownUrl: ${it.stackTraceToString()}", Level.WARN) - } - } - return "https://$domain$originalUrl" - } - return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2" - } - - suspend fun getC2CPicDownUrl( - originalUrl: String, - md5: String, - peer: String = "", - fileId: String = "", - sha: String = "", - fileSize: ULong = 0uL, - width: UInt = 0u, - height: UInt = 0u, - storeId: Int = 0 - ): String { - val isNtServer = storeId == 1 || originalUrl.startsWith("/download") - val domain = if (isNtServer) MULTIMEDIA_DOMAIN else C2C_PIC - if (originalUrl.isNotEmpty()) { - if (fileId.isNotEmpty()) getNtPicRKey( - fileId = fileId, - md5 = md5, - sha = sha, - fileSize = fileSize, - width = width, - height = height - ) { - sceneType = 1u - c2c = C2CUserInfo( - accountType = 2u, - uid = ContactHelper.getUidByUinAsync(peer.toLong()) - ) - }.onSuccess { - if (isNtServer && !originalUrl.contains("rkey=")) { - return "https://$domain$originalUrl$it" - } - }.onFailure { - LogCenter.log("getC2CPicDownUrl: ${it.stackTraceToString()}", Level.WARN) - } - if (isNtServer && !originalUrl.contains("rkey=")) { - return "https://$domain$originalUrl&rkey=" - } - return "https://$domain$originalUrl" - } - return "https://$domain/offpic_new/0/0-0-${md5}/0?term=2" - } - - suspend fun getGuildPicDownUrl( - originalUrl: String, - md5: String, - peer: String = "", - subPeer: String = "", - fileId: String = "", - sha: String = "", - fileSize: ULong = 0uL, - width: UInt = 0u, - height: UInt = 0u - ): String { - val isNtServer = originalUrl.startsWith("/download") - val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC - if (originalUrl.isNotEmpty()) { - if (isNtServer && !originalUrl.contains("rkey=")) { - getNtPicRKey( - fileId = fileId, - md5 = md5, - sha = sha, - fileSize = fileSize, - width = width, - height = height - ) { - sceneType = 3u - channel = ChannelUserInfo(peer.toULong(), subPeer.toULong(), 1u) - }.onSuccess { - return "https://$domain$originalUrl$it" - }.onFailure { - LogCenter.log("getGuildPicDownUrl: ${it.stackTraceToString()}", Level.WARN) - } - return "https://$domain$originalUrl&rkey=" - } - return "https://$domain$originalUrl" - } - return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2" - } - - suspend fun getC2CVideoDownUrl( - peerId: String, - md5: ByteArray, - fileUUId: String - ): String { - return suspendCancellableCoroutine { - val runtime = AppRuntimeFetcher.appRuntime - val richProtoReq = RichProto.RichProtoReq() - val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq() - downReq.selfUin = runtime.currentAccountUin - downReq.peerUin = peerId - downReq.secondUin = peerId - downReq.uinType = FileMsg.UIN_BUDDY - downReq.agentType = 0 - downReq.chatType = 1 - downReq.troopUin = peerId - downReq.clientType = 2 - downReq.fileId = fileUUId - downReq.md5 = md5 - downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO - downReq.subBusiType = 0 - downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4 - downReq.downType = 1 - downReq.sceneType = 1 - richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> - if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) { - LogCenter.log("requestDownPrivateVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN) - it.resume("") - } else { - val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp - val url = StringBuilder() - url.append(videoDownResp.mIpList.random().getServerUrl("http://")) - url.append(videoDownResp.mUrl) - it.resume(url.toString()) - } - } - richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW - richProtoReq.reqs.add(downReq) - richProtoReq.protoReqMgr = runtime.getRuntimeService(IProtoReqManager::class.java, "all") - RichProtoProc.procRichProtoReq(richProtoReq) - } - } - - suspend fun getGroupVideoDownUrl( - peerId: String, - md5: ByteArray, - fileUUId: String - ): String { - return suspendCancellableCoroutine { - val runtime = AppRuntimeFetcher.appRuntime - val richProtoReq = RichProto.RichProtoReq() - val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq() - downReq.selfUin = runtime.currentAccountUin - downReq.peerUin = peerId - downReq.secondUin = peerId - downReq.uinType = FileMsg.UIN_TROOP - downReq.agentType = 0 - downReq.chatType = 1 - downReq.troopUin = peerId - downReq.clientType = 2 - downReq.fileId = fileUUId - downReq.md5 = md5 - downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO - downReq.subBusiType = 0 - downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4 - downReq.downType = 1 - downReq.sceneType = 1 - richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> - if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) { - LogCenter.log("requestDownGroupVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN) - it.resume("") - } else { - val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp - val url = StringBuilder() - url.append(videoDownResp.mIpList.random().getServerUrl("http://")) - url.append(videoDownResp.mUrl) - it.resume(url.toString()) - } - } - richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW - richProtoReq.reqs.add(downReq) - richProtoReq.protoReqMgr = runtime.getRuntimeService(IProtoReqManager::class.java, "all") - RichProtoProc.procRichProtoReq(richProtoReq) - } - } - - suspend fun getC2CPttDownUrl( - peerId: String, - fileUUId: String - ): String { - return suspendCancellableCoroutine { - val runtime = AppRuntimeFetcher.appRuntime - val richProtoReq = RichProto.RichProtoReq() - val pttDownReq: RichProto.RichProtoReq.C2CPttDownReq = RichProto.RichProtoReq.C2CPttDownReq() - pttDownReq.selfUin = runtime.currentAccountUin - pttDownReq.peerUin = peerId - pttDownReq.secondUin = peerId - pttDownReq.uinType = FileMsg.UIN_BUDDY - pttDownReq.busiType = 1002 - pttDownReq.uuid = fileUUId - pttDownReq.storageSource = "pttcenter" - pttDownReq.isSelfSend = false - - pttDownReq.voiceType = 1 - pttDownReq.downType = 1 - richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> - if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) { - LogCenter.log("requestDownPrivateVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN) - it.resume("") - } else { - val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.C2CPttDownResp - val url = StringBuilder() - url.append(pttDownResp.downloadUrl) - url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${PlatformUtils.getQQVersion(MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk") - it.resume(url.toString()) - } - } - richProtoReq.protoKey = RichProtoProc.C2C_PTT_DW - richProtoReq.reqs.add(pttDownReq) - richProtoReq.protoReqMgr = runtime.getRuntimeService(IProtoReqManager::class.java, "all") - RichProtoProc.procRichProtoReq(richProtoReq) - } - } - - suspend fun getGroupPttDownUrl( - peerId: String, - md5: ByteArray, - groupFileKey: String - ): String { - return suspendCancellableCoroutine { - val runtime = AppRuntimeFetcher.appRuntime - val richProtoReq = RichProto.RichProtoReq() - val groupPttDownReq: RichProto.RichProtoReq.GroupPttDownReq = RichProto.RichProtoReq.GroupPttDownReq() - groupPttDownReq.selfUin = runtime.currentAccountUin - groupPttDownReq.peerUin = peerId - groupPttDownReq.secondUin = peerId - groupPttDownReq.uinType = FileMsg.UIN_TROOP - groupPttDownReq.groupFileID = 0 - groupPttDownReq.groupFileKey = groupFileKey - groupPttDownReq.md5 = md5 - groupPttDownReq.voiceType = 1 - groupPttDownReq.downType = 1 - richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> - if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) { - LogCenter.log("requestDownGroupVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN) - it.resume("") - } else { - val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.GroupPttDownResp - val url = StringBuilder() - url.append("http://") - url.append(pttDownResp.domainV4V6) - url.append(pttDownResp.urlPath) - url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${ - PlatformUtils.getQQVersion( - MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk") - it.resume(url.toString()) - } - } - richProtoReq.protoKey = RichProtoProc.GRP_PTT_DW - richProtoReq.reqs.add(groupPttDownReq) - richProtoReq.protoReqMgr = runtime.getRuntimeService(IProtoReqManager::class.java, "all") - RichProtoProc.procRichProtoReq(richProtoReq) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/Transfer.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/Transfer.kt deleted file mode 100644 index d1b7c4a..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/Transfer.kt +++ /dev/null @@ -1,142 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.transfile - -import com.tencent.mobileqq.data.MessageForShortVideo -import com.tencent.mobileqq.data.MessageRecord -import com.tencent.mobileqq.transfile.FileMsg -import com.tencent.mobileqq.transfile.TransferRequest -import moe.fuqiuluo.shamrock.utils.MD5 -import java.io.File -import moe.fuqiuluo.qqinterface.servlet.transfile.data.ResourceType.* -import moe.fuqiuluo.qqinterface.servlet.transfile.data.ContactType -import moe.fuqiuluo.qqinterface.servlet.transfile.data.PictureResource -import moe.fuqiuluo.qqinterface.servlet.transfile.data.Resource -import moe.fuqiuluo.qqinterface.servlet.transfile.data.ResourceType -import moe.fuqiuluo.qqinterface.servlet.transfile.data.TransTarget -import moe.fuqiuluo.qqinterface.servlet.transfile.data.VideoResource -import moe.fuqiuluo.qqinterface.servlet.transfile.data.VoiceResource - -internal object Transfer: FileTransfer() { - private val ROUTE = mapOf Boolean>>( - ContactType.TROOP to mapOf( - Picture to { uploadGroupPic(id, (it as PictureResource).src, mRec) }, - Voice to { uploadGroupVoice(id, (it as VoiceResource).src) }, - Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) }, - - ), - ContactType.PRIVATE to mapOf( - Picture to { uploadC2CPic(id, (it as PictureResource).src, mRec) }, - Voice to { uploadC2CVoice(id, (it as VoiceResource).src) }, - Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) }, - ) - ) - - suspend fun uploadC2CVideo( - userId: String, - file: File, - thumb: File, - wait: Boolean = true - ): Boolean { - return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_C2C, BUSI_TYPE_SHORT_VIDEO, wait) { - it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4 - it.mRec = MessageForShortVideo().also { - it.busiType = BUSI_TYPE_SHORT_VIDEO - } - it.mThumbPath = thumb.absolutePath - it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath) - } - } - - suspend fun uploadGroupVideo( - groupId: String, - file: File, - thumb: File, - wait: Boolean = true - ): Boolean { - return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_TROOP, BUSI_TYPE_SHORT_VIDEO, wait) { - it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4 - it.mRec = MessageForShortVideo().also { - it.busiType = BUSI_TYPE_SHORT_VIDEO - } - it.mThumbPath = thumb.absolutePath - it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath) - } - } - - suspend fun uploadC2CVoice( - userId: String, - file: File, - wait: Boolean = true - ): Boolean { - return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) { - it.mPttUploadPanel = 3 - it.mPttCompressFinish = true - it.mIsPttPreSend = true - } - } - - suspend fun uploadGroupVoice( - groupId: String, - file: File, - wait: Boolean = true - ): Boolean { - return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) { - it.mPttUploadPanel = 3 - it.mPttCompressFinish = true - it.mIsPttPreSend = true - } - } - - suspend fun uploadC2CPic( - peerId: String, - file: File, - record: MessageRecord? = null, - wait: Boolean = true - ): Boolean { - return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) { - val picUpExtraInfo = TransferRequest.PicUpExtraInfo() - picUpExtraInfo.mIsRaw = false - picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY - it.mPicSendSource = 8 - it.mExtraObj = picUpExtraInfo - it.mIsPresend = true - it.delayShowProgressTimeInMs = 2000 - it.mRec = record - } - } - - suspend fun uploadGroupPic( - groupId: String, - file: File, - record: MessageRecord? = null, - wait: Boolean = true - ): Boolean { - return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) { - val picUpExtraInfo = TransferRequest.PicUpExtraInfo() - picUpExtraInfo.mIsRaw = false - picUpExtraInfo.mUinType = FileMsg.UIN_TROOP - it.mPicSendSource = 8 - it.delayShowProgressTimeInMs = 2000 - it.mExtraObj = picUpExtraInfo - it.mRec = record - } - } - - operator fun get(contactType: ContactType, resourceType: ResourceType): suspend TransTarget.(Resource) -> Boolean { - return (ROUTE[contactType] ?: error("unsupported contact type: $contactType"))[resourceType] - ?: error("Unsupported resource type: $resourceType") - } -} - -internal suspend infix fun TransferTaskBuilder.trans(res: Resource): Boolean { - return Transfer[contact.type, res.type](contact, res) -} - -internal class TransferTaskBuilder { - lateinit var contact: TransTarget -} - -internal infix fun Transfer.with(contact: TransTarget): TransferTaskBuilder { - return TransferTaskBuilder().also { - it.contact = contact - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/Contact.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/Contact.kt deleted file mode 100644 index 382242f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/Contact.kt +++ /dev/null @@ -1,29 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.transfile.data - -import com.tencent.mobileqq.data.MessageRecord - -internal enum class ContactType { - TROOP, - PRIVATE, -} - -internal interface TransTarget { - val id: String - val type: ContactType - - val mRec: MessageRecord? -} - -internal class Troop( - override val id: String, - override val mRec: MessageRecord? = null -): TransTarget { - override val type: ContactType = ContactType.TROOP -} - -internal class Private( - override val id: String, - override val mRec: MessageRecord? = null -): TransTarget { - override val type: ContactType = ContactType.PRIVATE -} diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/Resource.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/Resource.kt deleted file mode 100644 index f8d82fe..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/Resource.kt +++ /dev/null @@ -1,31 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.transfile.data - -import java.io.File - -internal enum class ResourceType { - Picture, - Video, - Voice -} - -internal interface Resource { - val type: ResourceType -} - -internal data class PictureResource( - val src: File -): Resource { - override val type = ResourceType.Picture -} - -internal data class VideoResource( - val src: File, val thumb: File -): Resource { - override val type = ResourceType.Video -} - -internal data class VoiceResource( - val src: File -): Resource { - override val type = ResourceType.Voice -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/RichMedia.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/RichMedia.kt deleted file mode 100644 index 18b3ef2..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/data/RichMedia.kt +++ /dev/null @@ -1,13 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.transfile.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class TryUpPicData( - @SerialName("ukey") val uKey: ByteArray, - @SerialName("exist") val exist: Boolean, - @SerialName("file_id") val fileId: ULong, - @SerialName("up_ip") var upIp: ArrayList? = null, - @SerialName("up_port") var upPort: ArrayList? = null, -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ActiveRPC.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ActiveRPC.kt new file mode 100644 index 0000000..282c5bf --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ActiveRPC.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.config + +data object ActiveRPC: ConfigKey() { + override fun name(): String = "active_rpc" + + override fun default(): Boolean = false +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/AliveReply.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/AliveReply.kt new file mode 100644 index 0000000..bebcac6 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/AliveReply.kt @@ -0,0 +1,6 @@ +package moe.fuqiuluo.shamrock.config + +object AliveReply: ConfigKey() { + override fun name() = "alive_reply" + override fun default() = false +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/AntiJvmTrace.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/AntiJvmTrace.kt new file mode 100644 index 0000000..a0398f1 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/AntiJvmTrace.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.config + +object AntiJvmTrace: ConfigKey() { + override fun default() = false + + override fun name() = "anti_jvm_trace" +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/B2Mode.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/B2Mode.kt new file mode 100644 index 0000000..77d789c --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/B2Mode.kt @@ -0,0 +1,6 @@ +package moe.fuqiuluo.shamrock.config + +object B2Mode: ConfigKey() { + override fun name() = "b2_mode" + override fun default() = false +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ConfigKey.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ConfigKey.kt new file mode 100644 index 0000000..b3e2d67 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ConfigKey.kt @@ -0,0 +1,12 @@ +package moe.fuqiuluo.shamrock.config + +abstract class ConfigKey { + abstract fun name(): String + + abstract fun default(): T + + companion object { + + } +} + diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/DebugMode.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/DebugMode.kt new file mode 100644 index 0000000..d7a1213 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/DebugMode.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.config + +object DebugMode: ConfigKey() { + override fun name(): String = "debug" + + override fun default(): Boolean = false +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt new file mode 100644 index 0000000..e723c80 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt @@ -0,0 +1,6 @@ +package moe.fuqiuluo.shamrock.config + +object EnableOldBDH: ConfigKey() { + override fun name() = "enable_old_bdh" + override fun default() = false +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableSelfMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableSelfMessage.kt new file mode 100644 index 0000000..447c5e5 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableSelfMessage.kt @@ -0,0 +1,6 @@ +package moe.fuqiuluo.shamrock.config + +object EnableSelfMessage: ConfigKey() { + override fun name() = "enable_self_message" + override fun default() = false +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ForceTablet.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ForceTablet.kt new file mode 100644 index 0000000..3c7e42e --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ForceTablet.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.config + +data object ForceTablet: ConfigKey() { + override fun name(): String = "force_tablet" + + override fun default(): Boolean = false +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/PassiveRPC.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/PassiveRPC.kt new file mode 100644 index 0000000..6465917 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/PassiveRPC.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.config + +data object PassiveRPC: ConfigKey() { + override fun name(): String = "passive_rpc" + + override fun default(): Boolean = false +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/RPCAddress.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/RPCAddress.kt new file mode 100644 index 0000000..79411a1 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/RPCAddress.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.config + +data object RPCAddress: ConfigKey() { + override fun name(): String = "rpc_address" + + override fun default(): String = "" +} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/RPCPort.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/RPCPort.kt new file mode 100644 index 0000000..86c828f --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/RPCPort.kt @@ -0,0 +1,8 @@ +package moe.fuqiuluo.shamrock.config + +data object RPCPort: ConfigKey() { + override fun name(): String = "rpc_port" + + override fun default(): Int = 5700 +} + diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ResourceGroup.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ResourceGroup.kt new file mode 100644 index 0000000..6a329b3 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ResourceGroup.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.config + +data object ResourceGroup: ConfigKey() { + override fun name(): String = "resource_group" + + override fun default(): String = "883536416" +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt new file mode 100644 index 0000000..f03c5f8 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt @@ -0,0 +1,52 @@ +package moe.fuqiuluo.shamrock.config + +import android.content.Intent +import mqq.app.MobileQQ +import java.util.Properties + +private val configDir = MobileQQ.getContext().getExternalFilesDir(null)!! + .parentFile!!.resolve("Tencent/Shamrock").also { + if (!it.exists()) it.mkdirs() + } +private val configFile = configDir.resolve("config.properties") + +private val configKeys = setOf( + ActiveRPC, + ForceTablet, + PassiveRPC, + ResourceGroup, + RPCAddress, + RPCPort +) + +internal object ShamrockConfig: Properties() { + init { + if (configFile.exists()) configFile.inputStream().use { + load(it) + } + } + + internal inline operator fun get(key: ConfigKey): Type { + return when(Type::class) { + Int::class -> getProperty(key.name()).toInt() as Type ?: key.default() + Long::class -> getProperty(key.name()).toLong() as Type ?: key.default() + String::class -> getProperty(key.name()) as Type ?: key.default() + Boolean::class -> getProperty(key.name()).toBoolean() as Type ?: key.default() + else -> throw IllegalArgumentException("Unsupported type") + } + } + + fun updateConfig(intent: Intent? = null) { + intent?.let { + for (key in configKeys) { + val value = intent.getStringExtra(key.name()) + if (value != null) setProperty(key.name(), value) + } + } + configFile.outputStream().use { + store(it, "Shamrock Config ${System.currentTimeMillis()}") + } + } + + private fun readResolve(): Any = ShamrockConfig +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/ContactHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/ContactHelper.kt index 8bab209..4cc9ac0 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/ContactHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/ContactHelper.kt @@ -1,10 +1,7 @@ package moe.fuqiuluo.shamrock.helper -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import kotlinx.coroutines.suspendCancellableCoroutine -import moe.fuqiuluo.qqinterface.servlet.FriendSvc -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher +import moe.fuqiuluo.shamrock.internals.NTServiceFetcher import kotlin.coroutines.resume internal object ContactHelper { @@ -32,24 +29,4 @@ internal object ContactHelper { } }[peerId]!! } - - /** - * 检查联系人是否可用, 每次都刷新,性能有损耗 - */ - suspend fun checkContactAvailable(chatType: Int, peerId: String): Boolean { - return when(chatType) { - MsgConstant.KCHATTYPEGROUP -> { - GroupSvc.getGroupList(true).getOrNull()?.find { - it.troopcode == peerId - } != null - } - - MsgConstant.KCHATTYPEC2C -> { - FriendSvc.getFriendList(true).getOrNull()?.find { - it.uin == peerId - } != null - } - else -> error("unknown chat type: $chatType") - } - } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/Exceptions.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/Exceptions.kt index f5fc24b..af2b2d7 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/Exceptions.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/Exceptions.kt @@ -6,10 +6,14 @@ internal class ParamsException(key: String): InternalMessageMakerError("Lack of internal class IllegalParamsException(key: String): InternalMessageMakerError("Illegal param $key") -internal object ActionMsgException: InternalMessageMakerError("action msg") +internal object ActionMsgException: InternalMessageMakerError("action msg") { + private fun readResolve(): Any = ActionMsgException +} internal class LogicException(why: String) : InternalMessageMakerError(why) -internal object ErrorTokenException : InternalMessageMakerError("access_token error") +internal object ErrorTokenException : InternalMessageMakerError("access_token error") { + private fun readResolve(): Any = ErrorTokenException +} internal class SendMsgException(why: String) : InternalMessageMakerError(why) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LocalCacheHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LocalCacheHelper.kt index 9783f55..5b86ba5 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LocalCacheHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LocalCacheHelper.kt @@ -1,23 +1,2 @@ package moe.fuqiuluo.shamrock.helper -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import moe.fuqiuluo.shamrock.utils.FileUtils -import mqq.app.MobileQQ -import java.io.File - -internal object LocalCacheHelper: BaseSvc() { - // 获取外部储存data目录 - private val dataDir = MobileQQ.getContext().getExternalFilesDir(null)!! - .parentFile!!.resolve("Tencent") - - fun getCurrentPttPath(): File { - return dataDir.resolve("MobileQQ/${app.currentAccountUin}/ptt").also { - if (!it.exists()) it.mkdirs() - } - } - - fun getCachePttFile(md5: String): File { - val file = FileUtils.getFileByMd5(md5) - return if (file.exists()) file else getCurrentPttPath().resolve("$md5.amr") - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LogCenter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LogCenter.kt index 469bd9b..cd6f168 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LogCenter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LogCenter.kt @@ -8,9 +8,10 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.xposed.hooks.toast -import moe.fuqiuluo.shamrock.xposed.helper.internal.DataRequester +import moe.fuqiuluo.shamrock.config.DebugMode +import moe.fuqiuluo.shamrock.config.ShamrockConfig +import moe.fuqiuluo.shamrock.tools.toast +import moe.fuqiuluo.shamrock.xposed.helper.AppTalker import mqq.app.MobileQQ import java.io.File import java.util.Date @@ -51,19 +52,18 @@ internal object LogCenter { private val format = SimpleDateFormat("[HH:mm:ss] ") fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) { - if (!ShamrockConfig.isDebug() && level == Level.DEBUG) { + if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) { return } if (toast) { MobileQQ.getContext().toast(string) } - // 把日志广播到主进程 GlobalScope.launch(Dispatchers.Default) { - DataRequester.request("send_message", bodyBuilder = { + AppTalker.talk("send_message") { put("string", string) put("level", level.id) - }) + } } if (!LogFile.exists()) { @@ -79,7 +79,7 @@ internal object LogCenter { level: Level = Level.INFO, toast: Boolean = false ) { - if (!ShamrockConfig.isDebug() && level == Level.DEBUG) { + if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) { return } @@ -89,10 +89,10 @@ internal object LogCenter { } // 把日志广播到主进程 GlobalScope.launch(Dispatchers.Default) { - DataRequester.request("send_message", bodyBuilder = { + AppTalker.talk("send_message") { put("string", log) put("level", level.id) - }) + } } if (!LogFile.exists()) { @@ -103,10 +103,6 @@ internal object LogCenter { LogFile.appendText(format) } -// fun getAllLog(): File { -// return LogFile -// } - fun getLogLines(start: Int, recent: Boolean = false): List { val logData = LogFile.readLines() val index = if(start > logData.size || start < 0) 0 else start diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt deleted file mode 100644 index 0b990e4..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt +++ /dev/null @@ -1,431 +0,0 @@ -package moe.fuqiuluo.shamrock.helper - -import com.tencent.mobileqq.qroute.QRoute -import com.tencent.qqnt.kernel.nativeinterface.Contact -import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgElement -import com.tencent.qqnt.msg.api.IMsgService -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonObject -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.qqinterface.servlet.msg.maker.ElemMaker -import moe.fuqiuluo.qqinterface.servlet.msg.maker.NtMsgElementMaker -import moe.fuqiuluo.shamrock.helper.db.MessageDB -import moe.fuqiuluo.shamrock.helper.db.MessageMapping -import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult -import moe.fuqiuluo.shamrock.tools.* -import protobuf.message.Elem -import protobuf.message.RichText -import kotlin.coroutines.resume -import kotlin.math.abs -import kotlin.time.Duration.Companion.seconds - -internal object MessageHelper { - suspend fun sendMessageWithoutMsgId( - chatType: Int, - peerId: String, - message: String, - callback: IOperateCallback, - fromId: String = peerId - ): SendMsgResult { - val uniseq = generateMsgId(chatType) - val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, decodeCQCode(message)).also { - if (it.second.isEmpty() && !it.first) { - error("消息合成失败,请查看日志或者检查输入。") - } else if (it.second.isEmpty()) { - return uniseq.copy(msgHashId = 0, msgTime = System.currentTimeMillis()) - } - }.second.filter { - it.elementType != -1 - } as ArrayList - return sendMessageWithoutMsgId(chatType, peerId, msg, fromId, callback) - } - - suspend fun resendMsg( - chatType: Int, - peerId: String, - fromId: String, - msgId: Long, - retryCnt: Int, - msgHashId: Int - ): Result { - val contact = generateContact(chatType, peerId, fromId) - return resendMsg(contact, msgId, retryCnt, msgHashId) - } - - suspend fun resendMsg(contact: Contact, msgId: Long, retryCnt: Int, msgHashId: Int): Result { - if (retryCnt < 0) return Result.failure(SendMsgException("消息发送超时次数过多")) - val service = QRoute.api(IMsgService::class.java) - val result = withTimeoutOrNull(15.seconds) { - val resendRet = suspendCancellableCoroutine { - service.resendMsg(contact, msgId) { result, _ -> - it.resume(result) - } - } - if (resendRet != 0 && - resendRet != 4 // 使用OldBDH 100%触发 - ) { - resendMsg(contact, msgId, retryCnt - 1, msgHashId) - } else { - Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis())) - } - } - return result ?: resendMsg(contact, msgId, retryCnt - 1, msgHashId) - } - - @OptIn(DelicateCoroutinesApi::class) - suspend fun sendMessageWithoutMsgId( - chatType: Int, - peerId: String, - message: JsonArray, - fromId: String = peerId, - callback: IOperateCallback - ): Result { - val uniseq = generateMsgId(chatType) - val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also { - if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") - }.second.filter { - it.elementType != -1 - } as ArrayList - - // ActionMsg No Care - if (msg.isEmpty()) { - return Result.success(uniseq.copy(msgTime = System.currentTimeMillis())) - } - - val totalSize = msg.filter { - it.elementType == MsgConstant.KELEMTYPEPIC || - it.elementType == MsgConstant.KELEMTYPEPTT || - it.elementType == MsgConstant.KELEMTYPEVIDEO - }.map { - (it.picElement?.fileSize ?: 0) + (it.pttElement?.fileSize - ?: 0) + (it.videoElement?.fileSize ?: 0) - }.reduceOrNull { a, b -> a + b } ?: 0 - val estimateTime = (totalSize / (300 * 1024)) * 1000 + 2000 - - lateinit var sendResult: SendMsgResult // msgTime to msgHash - val sendRet = withTimeoutOrNull>(estimateTime) { - suspendCancellableCoroutine { - GlobalScope.launch { - sendResult = sendMessageWithoutMsgId(chatType, peerId, msg, fromId) { code, message -> - callback.onResult(code, message) - it.resume(code to message) - } - } - } - } - - if (sendRet?.first != 0) { - //return Result.failure(SendMsgException(sendRet?.second ?: "发送消息超时")) - return Result.success(uniseq.copy(isTimeout = true)) - } - return Result.success(sendResult) - } - - suspend fun sendMessageWithoutMsgId( - chatType: Int, - peerId: String, - message: ArrayList, - fromId: String = peerId, - callback: IOperateCallback - ): SendMsgResult { - return sendMessageWithoutMsgId(generateContact(chatType, peerId, fromId), message, callback) - } - - fun sendMessageWithoutMsgId( - contact: Contact, - message: ArrayList, - callback: IOperateCallback - ): SendMsgResult { - val uniseq = generateMsgId(contact.chatType) - val nonMsg: Boolean = message.isEmpty() - return if (!nonMsg) { - val service = QRoute.api(IMsgService::class.java) - if (callback is MsgSvc.MessageCallback) { - callback.msgHash = uniseq.msgHashId - } - - service.sendMsg( - contact, - uniseq.qqMsgId, - message, - callback - ) - - uniseq.copy(msgTime = System.currentTimeMillis()) - } else { - uniseq.copy(msgHashId = 0, msgTime = System.currentTimeMillis()) - } - } - - suspend fun sendMessageWithMsgId( - chatType: Int, - peerId: String, - message: JsonArray, - callback: IOperateCallback, - fromId: String = peerId - ): SendMsgResult { - val uniseq = generateMsgId(chatType) - val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also { - if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") - }.second.filter { - it.elementType != -1 - } as ArrayList - val contact = generateContact(chatType, peerId, fromId) - val nonMsg: Boolean = message.isEmpty() - return if (!nonMsg) { - val service = QRoute.api(IMsgService::class.java) - if (callback is MsgSvc.MessageCallback) { - callback.msgHash = uniseq.msgHashId - } - - service.sendMsg( - contact, - uniseq.qqMsgId, - msg, - callback - ) - uniseq.copy(msgTime = System.currentTimeMillis()) - } else { - uniseq.copy(msgHashId = 0, msgTime = System.currentTimeMillis()) - } - } - - fun sendMessageWithMsgId( - contact: Contact, - message: ArrayList, - uniseq: Long, - callback: IOperateCallback - ): SendMsgResult { - val nonMsg: Boolean = message.isEmpty() - if (!nonMsg) { - val service = QRoute.api(IMsgService::class.java) - service.sendMsg( - contact, - uniseq, - message, - callback - ) - } - return SendMsgResult( - msgTime = if (nonMsg) 0 else System.currentTimeMillis(), - msgHashId = 0, - qqMsgId = uniseq - ) - } - - suspend fun sendMessageNoCb( - chatType: Int, - peerId: String, - message: JsonArray, - fromId: String = peerId - ): SendMsgResult { - val uniseq = generateMsgId(chatType) - val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also { - if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") - }.second.filter { - it.elementType != -1 - } as ArrayList - val contact = generateContact(chatType, peerId, fromId) - return if (!message.isEmpty()) { - val service = QRoute.api(IMsgService::class.java) - return suspendCancellableCoroutine { - service.sendMsg(contact, uniseq.qqMsgId, msg) { _, _ -> - it.resume(uniseq.copy(msgTime = System.currentTimeMillis())) - } - } - } else { - uniseq.copy(msgHashId = 0, msgTime = 0) - } - } - - suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact { - val peerId = when (chatType) { - MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> { - ContactHelper.getUidByUinAsync(id.toLong()) - } - - else -> id - } - return if (chatType == MsgConstant.KCHATTYPEGUILD) { - Contact(chatType, subId, peerId) - } else { - Contact(chatType, peerId, subId) - } - } - - fun obtainMessageTypeByDetailType(detailType: String): Int { - return when (detailType) { - "troop", "group" -> MsgConstant.KCHATTYPEGROUP - "private" -> MsgConstant.KCHATTYPEC2C - "less" -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN - "guild" -> MsgConstant.KCHATTYPEGUILD - else -> error("不支持的消息来源类型") - } - } - - fun obtainDetailTypeByMsgType(msgType: Int): String { - return when (msgType) { - MsgConstant.KCHATTYPEGROUP -> "group" - MsgConstant.KCHATTYPEC2C -> "private" - MsgConstant.KCHATTYPEGUILD -> "guild" - MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN -> "less" - else -> error("不支持的消息来源类型") - } - } - - suspend fun messageArrayToMsgElements( - chatType: Int, - msgId: Long, - peerId: String, - messageList: JsonArray - ): Pair> { - val msgList = arrayListOf() - var hasActionMsg = false - messageList.forEach { - val msg = it.jsonObject - val maker = NtMsgElementMaker[msg["type"].asString] - if (maker != null) { - try { - val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject - maker(chatType, msgId, peerId, data).onSuccess { msgElem -> - msgList.add(msgElem) - }.onFailure { - if (it.javaClass != ActionMsgException::class.java) { - throw it - } else { - hasActionMsg = true - } - } - } catch (e: Throwable) { - LogCenter.log(e.stackTraceToString(), Level.ERROR) - } - } else { - LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR) - return false to arrayListOf() - } - } - return hasActionMsg to msgList - } - - suspend fun messageArrayToRichText( - chatType: Int, - msgId: Long, - peerId: String, - messageList: JsonArray - ): Result> { - val elemMaker = ElemMaker() - messageList.forEach { element -> - val msg = element.asJsonObject - val maker = ElemMaker[msg["type"].asString] - if (maker != null) { - try { - val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject - maker(elemMaker, chatType, msgId, peerId, data) - } catch (e: Throwable) { - if (e.javaClass != ActionMsgException::class.java) { - LogCenter.log(e.stackTraceToString(), Level.ERROR) - } - } - } else { - LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR) - } - } - return Result.success(elemMaker.getDesc() to elemMaker.getRich()) - } - - fun generateMsgIdHash(chatType: Int, msgId: Long): Int { - val key = when (chatType) { - MsgConstant.KCHATTYPEGROUP -> "grp$msgId" - MsgConstant.KCHATTYPEC2C -> "c2c$msgId" - MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> "tmpgrp$msgId" - MsgConstant.KCHATTYPEGUILD -> "guild$msgId" - else -> error("不支持的消息来源类型 | generateMsgIdHash: $chatType") - } - return abs(key.hashCode()) - } - - fun generateMsgId(chatType: Int): SendMsgResult { - val msgId = createMessageUniseq(chatType, System.currentTimeMillis()) - val hashCode: Int = generateMsgIdHash(chatType, msgId) - return SendMsgResult(hashCode, msgId, 0) - } - - fun getMsgMappingByHash(hash: Int): MessageMapping? { - val db = MessageDB.getInstance() - return db.messageMappingDao().queryByMsgHashId(hash) - } - - fun getMsgMappingBySeq(chatType: Int, peerId: String, msgSeq: Int): MessageMapping? { - val db = MessageDB.getInstance() - return db.messageMappingDao().queryByMsgSeq(chatType, peerId, msgSeq) - } - - fun removeMsgByHashCode(hashCode: Int) { - MessageDB.getInstance() - .messageMappingDao() - .deleteByMsgHash(hashCode) - } - - fun saveMsgMapping( - hash: Int, - qqMsgId: Long, - time: Long, - chatType: Int, - peerId: String, - subPeerId: String, - msgSeq: Int, - subChatType: Int = chatType - ) { - val database = MessageDB.getInstance() - val mapping = MessageMapping(hash, qqMsgId, chatType, subChatType, peerId, time, msgSeq, subPeerId) - database.messageMappingDao().insert(mapping) - } - - fun saveMsgMappingNotExist( - hash: Int, - qqMsgId: Long, - time: Long, - chatType: Int, - peerId: String, - subPeerId: String, - msgSeq: Int, - subChatType: Int = chatType - ) { - val database = MessageDB.getInstance() - val mapping = MessageMapping(hash, qqMsgId, chatType, subChatType, peerId, time, msgSeq, subPeerId) - database.messageMappingDao().insertNotExist(mapping) - } - - external fun createMessageUniseq(chatType: Int, time: Long): Long - - fun decodeCQCode(code: String): JsonArray { - val arrayList = ArrayList() - val msgList = nativeDecodeCQCode(code) - msgList.forEach { - val params = hashMapOf() - it.forEach { (key, value) -> - if (key != "_type") { - params[key] = value.json - } - } - val data = mapOf( - "type" to it["_type"]!!.json, - "data" to JsonObject(params) - ) - arrayList.add(data.json) - } - return arrayList.jsonArray - } - - private external fun nativeDecodeCQCode(code: String): List> - external fun nativeEncodeCQCode(segment: List>): String -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MusicHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MusicHelper.kt deleted file mode 100644 index f221723..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MusicHelper.kt +++ /dev/null @@ -1,97 +0,0 @@ -package moe.fuqiuluo.shamrock.helper - -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText -import kotlinx.serialization.json.Json -import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo -import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc -import moe.fuqiuluo.shamrock.tools.GlobalClient -import moe.fuqiuluo.shamrock.tools.asInt -import moe.fuqiuluo.shamrock.tools.asJsonArray -import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull -import moe.fuqiuluo.shamrock.tools.asJsonObject -import moe.fuqiuluo.shamrock.tools.asString -import moe.fuqiuluo.shamrock.tools.asStringOrNull -import moe.fuqiuluo.shamrock.utils.MD5 - -internal object MusicHelper { - suspend fun tryShare163MusicById(chatType: Int, peerId: Long, msgId: Long, id: String): Boolean { - try { - val respond = GlobalClient.get("https://music.163.com/api/song/detail/?id=$id&ids=[$id]") - val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songs"].asJsonArray.first().asJsonObject - val name = songInfo["name"].asString - val title = songInfo["name"].asString - val singerName = songInfo["artists"].asJsonArray.first().asJsonObject["name"].asString - val previewUrl = songInfo["album"].asJsonObject["picUrl"].asString - val playUrl = "https://music.163.com/song/media/outer/url?id=$id.mp3" - val jumpUrl = "https://music.163.com/#/song?id=$id" - ArkMsgSvc.tryShareMusic( - chatType, - peerId, - msgId, - ArkAppInfo.NetEaseMusic, - title.ifBlank { name }, - singerName, - jumpUrl, - previewUrl, - playUrl - ) - return true - } catch (e: Throwable) { - LogCenter.log(e.stackTraceToString(), Level.ERROR) - } - return false - } - - suspend fun tryShareQQMusicById(chatType: Int, peerId: Long, msgId: Long, id: String): Boolean { - try { - val respond = GlobalClient.get("https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:$id},%22module%22:%22music.pf_song_detail_svr%22}}") - val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songinfo"].asJsonObject - if (songInfo["code"].asInt != 0) { - LogCenter.log("获取QQ音乐($id)的歌曲信息失败。") - return false - } else { - val data = songInfo["data"].asJsonObject - val trackInfo = data["track_info"].asJsonObject - val mid = trackInfo["mid"].asString - val previewMid = trackInfo["album"].asJsonObject["mid"].asString - val singerMid = trackInfo["singer"].asJsonArrayOrNull?.let { - it[0].asJsonObject["mid"].asStringOrNull - } ?: "" - val name = trackInfo["name"].asString - val title = trackInfo["title"].asString - val singerName = trackInfo["singer"].asJsonArray.first().asJsonObject["name"].asString - val vs = trackInfo["vs"].asJsonArrayOrNull?.let { - it[0].asStringOrNull - } ?: "" - val code = MD5.getMd5Hex("${mid}q;z(&l~sdf2!nK".toByteArray()).substring(0 .. 4).uppercase() - val playUrl = "http://c6.y.qq.com/rsc/fcgi-bin/fcg_pyq_play.fcg?songid=&songmid=$mid&songtype=1&fromtag=50&uin=&code=$code" - val previewUrl = if (vs.isNotEmpty()) { - "http://y.gtimg.cn/music/photo_new/T062R150x150M000$vs}.jpg" - } else if (previewMid.isNotEmpty()) { - "http://y.gtimg.cn/music/photo_new/T002R150x150M000$previewMid.jpg" - } else if (singerMid.isNotEmpty()){ - "http://y.gtimg.cn/music/photo_new/T001R150x150M000$singerMid.jpg" - } else { - "" - } - val jumpUrl = "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=${mid}&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" - ArkMsgSvc.tryShareMusic( - chatType, - peerId, - msgId, - ArkAppInfo.QQMusic, - title.ifBlank { name }, - singerName, - jumpUrl, - previewUrl, - playUrl - ) - return true - } - } catch (e: Throwable) { - LogCenter.log(e.stackTraceToString(), Level.ERROR) - } - return false - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/TroopHonorHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/TroopHonorHelper.kt index 76f2c76..eb3622a 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/TroopHonorHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/TroopHonorHelper.kt @@ -1,6 +1,5 @@ package moe.fuqiuluo.shamrock.helper -import moe.fuqiuluo.shamrock.remote.service.data.GroupMemberHonor object TroopHonorHelper { data class Honor( @@ -60,4 +59,13 @@ object TroopHonorHelper { else -> flag shr 4 } and 3 } + + data class GroupMemberHonor( + val uin: Long, + val honorUrl: String, + val honorIconUrl: String, + val honorLevel: Int, + val honorId: Int, + val honorName: String + ) } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/NTServiceFetcher.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt similarity index 88% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/NTServiceFetcher.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt index 471a184..a6e3885 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/NTServiceFetcher.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt @@ -1,4 +1,4 @@ -package moe.fuqiuluo.shamrock.xposed.helper +package moe.fuqiuluo.shamrock.internals import com.tencent.qqnt.kernel.api.IKernelService import com.tencent.qqnt.kernel.api.impl.MsgService @@ -8,11 +8,9 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.remote.service.PacketReceiver -import moe.fuqiuluo.shamrock.remote.service.listener.AioListener -import moe.fuqiuluo.shamrock.remote.service.listener.PrimitiveListener import moe.fuqiuluo.shamrock.tools.hookMethod import moe.fuqiuluo.shamrock.utils.PlatformUtils +import qq.service.internals.msgService internal object NTServiceFetcher { private lateinit var iKernelService: IKernelService @@ -28,9 +26,6 @@ internal object NTServiceFetcher { val curHash = service.hashCode() + msgService.hashCode() if (isInitForNt(curHash)) return - PacketHandler.initPacketHandler() - PacketReceiver.init() - LogCenter.log("Fetch kernel service successfully: $curKernelHash,$curHash,${PlatformUtils.isMainProcess()}") curKernelHash = curHash this.iKernelService = service @@ -38,7 +33,6 @@ internal object NTServiceFetcher { initNTKernelListener(msgService) antiBackgroundMode(sessionService) - //hookGuildListener(sessionService) } } @@ -64,13 +58,13 @@ internal object NTServiceFetcher { try { LogCenter.log("Register MSG listener successfully.") - msgService.addMsgListener(AioListener) + //msgService.addMsgListener(AioListener) // 接口缺失 暂不使用 //groupService.addKernelGroupListener(GroupEventListener) //LogCenter.log("Register Group listener successfully.") - PrimitiveListener.registerListener() + //PrimitiveListener.registerListener() } catch (e: Throwable) { LogCenter.log(e.stackTraceToString(), Level.WARN) } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/HTTPServer.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/HTTPServer.kt deleted file mode 100644 index 59ff336..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/HTTPServer.kt +++ /dev/null @@ -1,174 +0,0 @@ -package moe.fuqiuluo.shamrock.remote - -import io.ktor.server.application.Application -import io.ktor.server.application.install -import io.ktor.server.engine.ApplicationEngine -import io.ktor.server.engine.ApplicationEngineEnvironmentBuilder -import io.ktor.server.engine.applicationEngineEnvironment -import io.ktor.server.engine.connector -import io.ktor.server.engine.embeddedServer -import io.ktor.server.engine.sslConnector -import io.ktor.server.netty.Netty -import io.ktor.server.routing.routing -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import moe.fuqiuluo.shamrock.remote.api.* -import moe.fuqiuluo.shamrock.remote.config.* -import moe.fuqiuluo.shamrock.remote.plugin.Auth -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.xposed.helper.internal.DataRequester -import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader -import org.slf4j.LoggerFactory -import java.security.KeyStore - - -internal object HTTPServer { - @JvmStatic - var isServiceStarted = false - internal var startTime = 0L - - private val actionMutex = Mutex() - private lateinit var server: ApplicationEngine - internal var currServerPort: Int = 0 - - private fun Application.configModule() { - install(Auth).let { - val token = ShamrockConfig.getToken() - if (token.isBlank()) { - LogCenter.log("未配置Token,将不进行鉴权。", Level.WARN) - } else { - LogCenter.log("配置Token: $token", Level.INFO) - } - } - contentNegotiation() - statusPages() - routing { - echoVersion() - obtainFrameworkInfo() - registerBDH() - userAction() - messageAction() - troopAction() - friendAction() - ticketActions() - fetchRes() - showLog() - profileRouter() - weatherAction() - otherAction() - guildAction() - testAction() - requestRouter() - fav() - if (ShamrockConfig.isDev()) { - qsign() - obtainProtocolData() - } - } - -// intercept(ApplicationCallPipeline.Plugins) { -// call.response.headers.appendIfAbsent("Content-Type", ContentType.Application.Json.toString()) -// } - } - - private fun ApplicationEngineEnvironmentBuilder.configSSL() { - try { - val keyStoreFile = ShamrockConfig.getKeyStorePath() - val pwd = ShamrockConfig.sslPwd().also { - if (it == null || it.isEmpty()) { - LogCenter.log("SSL 密码未填写。", Level.ERROR) - return - } - } - val privatePwd = ShamrockConfig.sslPrivatePwd().also { - if (it.isNullOrEmpty()) { - LogCenter.log("SSL Private密码未填写。", Level.ERROR) - return - } - } - val keyAlias = ShamrockConfig.sslAlias().also { - if (it.isNullOrEmpty()) { - LogCenter.log("SSL Alias未填写。", Level.ERROR) - return - } - } - - val keyStore = KeyStore.getInstance("BKS") - keyStore.load(null, pwd) - - keyStoreFile?.inputStream().use { - keyStore.load(it, pwd) - } - - sslConnector( - keyStore = keyStore, - keyAlias = keyAlias!!, - keyStorePassword = { pwd!! }, - privateKeyPassword = { privatePwd!!.toCharArray() }) { - this.port = ShamrockConfig.getSslPort() - this.keyStorePath = keyStoreFile - LogCenter.log("SSL 配置成功,端口: ${this.port}", Level.INFO) - } - } catch (e: Exception) { - LogCenter.log("SSL 配置错误: ${e.stackTraceToString()}", Level.ERROR) - } - } - - suspend fun start(port: Int) { - if (isServiceStarted) return - actionMutex.withLock { - val environment = applicationEngineEnvironment { - log = LoggerFactory.getLogger("ktor.application") - connector { - this.port = port - } - if (ShamrockConfig.ssl()) - configSSL() - module { configModule() } - } - server = embeddedServer(Netty, environment) - server.start(wait = false) - } - startTime = System.currentTimeMillis() - isServiceStarted = true - currServerPort = port - LogCenter.log("Start HTTP Server: http://0.0.0.0:$currServerPort/") - DataRequester.request("success", values = mapOf( - "port" to currServerPort, - "voice" to NativeLoader.isVoiceLoaded - )) - } - - fun isActive(): Boolean { - return server.application.isActive - } - - fun changePort(port: Int) { - if (currServerPort == port && isServiceStarted) return - GlobalScope.launch { - stop() - start(port) - } - } - - suspend fun stop() { - actionMutex.withLock { - server.stop() - isServiceStarted = false - } - } - - fun restart() { - if(!isServiceStarted) return - val post = currServerPort - GlobalScope.launch { - stop() - start(post) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/ActionManager.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/ActionManager.kt deleted file mode 100644 index 62d5ddc..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/ActionManager.kt +++ /dev/null @@ -1,177 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action - -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import moe.fuqiuluo.shamrock.remote.structures.EmptyObject -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.tools.json - -internal object ActionManager { - val actionMap = mutableMapOf() - - init { - initManager() - } - - operator fun get(action: String): IActionHandler? { - return actionMap[action] - } -} - -internal abstract class IActionHandler { - protected abstract suspend fun internalHandle(session: ActionSession): String - - open val requiredParams: Array = arrayOf() - - suspend fun handle(session: ActionSession): String { - requiredParams.forEach { - if (!session.has(it)) { - return noParam(it, session.echo) - } - } - return internalHandle(session) - } - - protected fun ok( - msg: String = "", - echo: JsonElement - ): String { - return resultToString(true, Status.Ok, EmptyObject, msg, echo = echo) - } - - protected inline fun ok(data: T, echo: JsonElement, msg: String = ""): String { - return resultToString(true, Status.Ok, data!!, msg, echo = echo) - } - - protected fun noParam(paramName: String, echo: JsonElement): String { - return failed(Status.BadParam, "lack of [$paramName]", echo) - } - - protected fun badParam(why: String, echo: JsonElement): String { - return failed(Status.BadParam, why, echo) - } - - protected fun error(why: String, echo: JsonElement, arrayResult: Boolean = false): String { - return failed(Status.InternalHandlerError, why, echo, arrayResult) - } - - protected fun logic(why: String, echo: JsonElement, arraayResult: Boolean = false): String { - return failed(Status.LogicError, why, echo, arraayResult) - } - - protected fun failed(status: Status, msg: String, echo: JsonElement, arrResult: Boolean = false): String { - return resultToString(false, status, if (arrResult) EmptyJsonArray else EmptyJsonObject, msg, echo = echo) - } -} - -internal class ActionSession { - private val params: JsonObject - internal val echo: JsonElement - - constructor( - values: Map, - echo: JsonElement = EmptyJsonString - ) { - val map = hashMapOf() - values.forEach { (key, value) -> - if (value != null) { - when (value) { - is String -> map[key] = value.json - is Number -> map[key] = value.json - is Char -> map[key] = JsonPrimitive(value.code.toByte()) - is Boolean -> map[key] = value.json - is JsonObject -> map[key] = value - is JsonArray -> map[key] = value - else -> error("unsupported type: ${value::class.java}") - } - } - } - this.echo = echo - this.params = JsonObject(map) - } - - constructor( - params: JsonObject, - echo: JsonElement - ) { - this.echo = echo - this.params = params - } - - fun getLong(key: String): Long { - return params[key].asLong - } - - fun getLongOrNull(key: String): Long? { - return params[key].asLongOrNull - } - - fun getInt(key: String): Int { - return params[key].asInt - } - - fun getIntOrNull(key: String): Int? { - return params[key].asIntOrNull - } - - fun isString(key: String): Boolean { - val element = params[key] - return element is JsonPrimitive && element.isString - } - - fun isArray(key: String): Boolean { - val element = params[key] - return element is JsonArray - } - - fun isObject(key: String): Boolean { - val element = params[key] - return element is JsonObject - } - - fun getString(key: String): String { - return params[key].asString - } - - fun getStringOrNull(key: String): String? { - return params[key].asStringOrNull - } - - fun getBoolean(key: String): Boolean { - return params[key].asBoolean - } - - fun getBooleanOrDefault(key: String, default: Boolean? = null): Boolean { - return params[key].asBooleanOrNull ?: default as Boolean - } - - fun getJsonElement(key: String): JsonElement { - return params[key]!! - } - - fun getJsonElementOrNull(key: String): JsonElement? { - return params[key] - } - - fun getObject(key: String): JsonObject { - return params[key].asJsonObject - } - - fun getObjectOrNull(key: String): JsonObject? { - return params[key].asJsonObjectOrNull - } - - fun getArray(key: String): JsonArray { - return params[key].asJsonArray - } - - fun getArrayOrNull(key: String): JsonArray? { - return params[key].asJsonArrayOrNull - } - - fun has(key: String) = params.containsKey(key) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/AdaptShareJson.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/AdaptShareJson.kt deleted file mode 100644 index b3dfe3e..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/AdaptShareJson.kt +++ /dev/null @@ -1,44 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.ark.LightAppSvc -import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("adapt_share_json") -internal object AdaptShareJson: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - //val json = if(session.isString("json")) session.getString("json") - //else session.getJsonElement("json").toString() - val cover = session.getString("cover") - val desc = session.getString("desc") - val url = session.getStringOrNull("url") ?: "" - return invoke(cover, desc, url, session.echo) - } - - suspend operator fun invoke(cover: String, desc: String, url: String, echo: JsonElement = EmptyJsonString): String { - /* - ArkMsgSvc.tryShareJsonMessage(json).onSuccess { - return ok(SignArkMessageResult(it), echo = echo) - }.onFailure { - return error(it.message ?: it.toString(), echo) - }*/ - LightAppSvc.adaptShareJumpUrl(ArkAppInfo.DanMaKu, cover, desc, url).onSuccess { - return ok(AdaptShareInfo(it), echo = echo) - }.onFailure { - return error(it.message ?: it.toString(), echo) - } - return logic("logic error", echo) - } - - override val requiredParams: Array = arrayOf("cover", "desc") - - @Serializable - data class AdaptShareInfo( - val result: String - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/BanTroopMember.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/BanTroopMember.kt deleted file mode 100644 index f2b45a4..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/BanTroopMember.kt +++ /dev/null @@ -1,34 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_ban") -internal object BanTroopMember: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val userId = session.getLong("user_id") - val duration = session.getIntOrNull("duration") ?: (30 * 60) - - return invoke(groupId, userId, duration, session.echo) - } - - operator fun invoke( - groupId: Long, - userId: Long, - duration: Int = 30 * 60, - echo: JsonElement = EmptyJsonString - ): String { - if (!GroupSvc.isAdmin(groupId)) { - return logic("You are not the administrator of the group.", echo) - } - GroupSvc.banMember(groupId, userId, duration) - return ok("成功", echo) - } - - override val requiredParams: Array = arrayOf("group_id", "user_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CleanCache.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CleanCache.kt deleted file mode 100644 index 8fca2fa..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CleanCache.kt +++ /dev/null @@ -1,29 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.utils.FileUtils -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.MMKVFetcher -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("clean_cache") -internal object CleanCache: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return invoke(session.echo) - } - - operator fun invoke(echo: JsonElement = EmptyJsonString): String { - FileUtils.clearCache() - MMKVFetcher.mmkvWithId("hash2id") - .clear() - MMKVFetcher.mmkvWithId("id2id") - .clear() - MMKVFetcher.mmkvWithId("seq2id") - .clear() - MMKVFetcher.mmkvWithId("audio2silk") - .clear() - return ok("成功", echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ClearMsgs.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ClearMsgs.kt deleted file mode 100644 index abebb2c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ClearMsgs.kt +++ /dev/null @@ -1,32 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("clear_msgs", ["clear_messages"]) -internal object ClearMsgs: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val msgType = session.getString("message_type") - val peerId = session.getString(if (msgType == "group") "group_id" else "user_id") - return invoke(msgType, peerId, session.echo) - } - - suspend operator fun invoke( - msgType: String, - peerId: String, - echo: JsonElement = EmptyJsonString - ): String { - val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType) - val contact = MessageHelper.generateContact(chatType, peerId, "") - NTServiceFetcher.kernelService.wrapperSession.msgService.clearMsgRecords(contact, null) - return ok(echo = echo) - } - - override val requiredParams: Array - get() = arrayOf("message_type") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CreateGroupFileFolder.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CreateGroupFileFolder.kt deleted file mode 100644 index 167ab11..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CreateGroupFileFolder.kt +++ /dev/null @@ -1,28 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.FileSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("create_group_file_folder") -internal object CreateGroupFileFolder: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val folderName = session.getString("name") - val echo = session.echo - return invoke(groupId, folderName, echo) - } - - suspend operator fun invoke(groupId: Long, folderName: String, echo: JsonElement = EmptyJsonString): String { - val result = FileSvc.createFileFolder(groupId, folderName) - if (result.isFailure) { - return ok(msg = result.exceptionOrNull()?.message ?: "无法创建群文件夹", echo = echo) - } - return ok(data = result.getOrThrow(), msg = "成功", echo = echo) - } - - override val requiredParams: Array = arrayOf("group_id", "name") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CreateGuildRole.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CreateGuildRole.kt deleted file mode 100644 index 981fe19..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/CreateGuildRole.kt +++ /dev/null @@ -1,42 +0,0 @@ -@file:Suppress("UNCHECKED_CAST") - -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -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.asString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("create_guild_role") -internal object CreateGuildRole: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id").toULong() - val name = session.getString("name") - val color = session.getLong("color") - val initialUsers = session.getArray("initial_users").map { - it.asString.toULong() - } - return invoke(guildId, color, name, initialUsers, session.echo) - } - - suspend operator fun invoke(guildId: ULong, color: Long, name: String, initialUsers: List, echo: JsonElement = EmptyJsonString): String { - val result = GProSvc.createGuildRole(guildId, name, color, initialUsers as ArrayList).onFailure { - return error(it.message ?: "Unknown error", echo) - }.getOrThrow() - return ok(data = CreateGuildRoleResult( - result.roleId.toULong() - ), echo = echo) - } - - override val requiredParams: Array = arrayOf("guild_id", "color", "name", "initial_users") - - @Serializable - data class CreateGuildRoleResult( - @SerialName("role_id") val roleId: ULong - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteEssenceMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteEssenceMessage.kt deleted file mode 100644 index 89cd482..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteEssenceMessage.kt +++ /dev/null @@ -1,34 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("delete_essence_msg", ["delete_essence_message"]) -internal object DeleteEssenceMessage: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val messageId = session.getInt("message_id") - return invoke(messageId, session.echo) - } - - suspend operator fun invoke(messageId: Int, echo: JsonElement = EmptyJsonString): String { - val msg = MsgSvc.getMsg(messageId).onFailure { - return logic("Obtain msg failed, please check your msg_id.", echo) - }.getOrThrow() - val (success, tip) = GroupSvc.deleteEssenceMessage( - if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0, - msg.msgSeq, - msg.msgRandom - ) - return if (success) { - ok("成功", echo) - } else { - logic(tip, echo) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGroupFile.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGroupFile.kt deleted file mode 100644 index 58eec21..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGroupFile.kt +++ /dev/null @@ -1,27 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.FileSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("delete_group_file") -internal object DeleteGroupFile: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val fileId = session.getString("file_id") - val busid = session.getInt("busid") - return invoke(groupId, fileId, busid, session.echo) - } - - suspend operator fun invoke(groupId: Long, fileId: String, bizId: Int, echo: JsonElement = EmptyJsonString): String { - if(!FileSvc.deleteGroupFile(groupId, bizId, fileId)) { - return error("删除失败", echo = echo) - } - return ok("成功", echo) - } - - override val requiredParams: Array = arrayOf("group_id", "file_id", "busid") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGroupFolder.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGroupFolder.kt deleted file mode 100644 index 300732d..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGroupFolder.kt +++ /dev/null @@ -1,26 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.FileSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("delete_group_folder") -internal object DeleteGroupFolder: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val folderId = session.getString("folder_id") - return invoke(groupId, folderId, session.echo) - } - - suspend operator fun invoke(groupId: Long, folderId: String, echo: JsonElement = EmptyJsonString): String { - if(!FileSvc.deleteGroupFolder(groupId, folderId)) { - return error(why = "删除群文件夹失败", echo = echo) - } - return ok(msg = "成功", echo = echo) - } - - override val requiredParams: Array = arrayOf("group_id", "folder_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGuildRole.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGuildRole.kt deleted file mode 100644 index 754368c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteGuildRole.kt +++ /dev/null @@ -1,24 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("delete_guild_role") -internal object DeleteGuildRole: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id").toULong() - val roleId = session.getString("role_id").toULong() - return invoke(guildId, roleId, session.echo) - } - - operator fun invoke(guildId: ULong, roleId: ULong, echo: JsonElement = EmptyJsonString): String { - GProSvc.deleteGuildRole(guildId, roleId) - return ok("success", echo = echo) - } - - override val requiredParams: Array = arrayOf("guild_id", "role_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteMessage.kt deleted file mode 100644 index 646d202..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DeleteMessage.kt +++ /dev/null @@ -1,23 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("delete_message", ["delete_msg"]) -internal object DeleteMessage: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val hashCode = session.getString("message_id").toInt() - return invoke(hashCode, session.echo) - } - - suspend operator fun invoke(msgHash: Int, echo: JsonElement = EmptyJsonString): String { - MsgSvc.recallMsg(msgHash) - return ok("成功", echo) - } - - override val requiredParams: Array = arrayOf("message_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DownloadFile.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DownloadFile.kt deleted file mode 100644 index 17cc096..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/DownloadFile.kt +++ /dev/null @@ -1,140 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import android.util.Base64 -import kotlinx.serialization.Serializable -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.tools.asString -import moe.fuqiuluo.shamrock.utils.DownloadUtils -import moe.fuqiuluo.shamrock.utils.FileUtils -import moe.fuqiuluo.shamrock.utils.MD5 -import moe.fuqiuluo.symbols.OneBotHandler -import java.io.File - -@OneBotHandler("download_file") -internal object DownloadFile: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val url = session.getStringOrNull("url") - val name = session.getStringOrNull("name") - val b64 = session.getStringOrNull("base64") - val rootDir = session.getStringOrNull("root") - val threadCnt = session.getIntOrNull("thread_cnt") ?: 3 - val headers = if (session.has("headers")) (if (session.isArray("headers")) { - session.getArray("headers").map { - it.asString - } - } else { - session.getString("headers").split("\r\n") - }) else emptyList() - return invoke(url, b64, threadCnt, headers, name, rootDir, session.echo) - } - - suspend operator fun invoke( - url: String?, - base64: String?, - threadCnt: Int, - headers: List, - name: String?, - root: String?, - echo: JsonElement = EmptyJsonString - ): String { - if (url != null) { - val headerMap = mutableMapOf( - "User-Agent" to "Shamrock" - ) - headers.forEach { - val pair = it.split("=") - if (pair.size >= 2) { - val (k, v) = pair - headerMap[k] = v - } - } - return invoke(url, threadCnt, headerMap, name, root, echo) - } else if (base64 != null) { - return invoke(base64, name, root, echo) - } else { - return noParam("url/base64", echo) - } - } - - operator fun invoke( - base64: String, - name: String?, - root: String?, - echo: JsonElement - ): String { - kotlin.runCatching { - val bytes = Base64.decode(base64, Base64.DEFAULT) - FileUtils.getTmpFile("cache").also { - it.writeBytes(bytes) - } - }.onSuccess { - var tmp = if (name == null) - FileUtils.renameByMd5(it) - else it.parentFile!!.resolve(name).also { target -> - it.renameTo(target) - it.delete() - } - if (root != null) { - tmp = File(root).resolve(name ?: tmp.name).also { - tmp.renameTo(it) - } - } - return ok(data = DownloadResult( - file = tmp.absolutePath, - md5 = MD5.genFileMd5Hex(tmp.absolutePath) - ), msg = "成功", echo = echo) - }.onFailure { - return logic("Base64格式错误", echo) - } - return logic("未知错误", echo) - } - - suspend operator fun invoke( - url: String, - threadCnt: Int, - headers: Map, - name: String?, - root: String?, - echo: JsonElement = EmptyJsonString - ): String { - return kotlin.runCatching { - var tmp = FileUtils.getTmpFile("cache") - if(!DownloadUtils.download( - urlAdr = url, - dest = tmp, - headers = headers, - threadCount = threadCnt - )) { - return error("下载失败 (0x1)", echo) - } - tmp = if (name == null) { - FileUtils.renameByMd5(tmp) - } else { - val newFile = tmp.parentFile!!.resolve(name) - tmp.renameTo(newFile) - newFile - } - if (root != null) { - tmp = File(root).resolve(name ?: tmp.name).also { - tmp.renameTo(it) - } - } - ok(data = DownloadResult( - file = tmp.absolutePath, - md5 = MD5.genFileMd5Hex(tmp.absolutePath) - ), msg = "成功", echo = echo) - }.getOrElse { - logic(it.stackTraceToString(), echo) - } - } - - @Serializable - data class DownloadResult( - val file: String, - val md5: String - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavAddImageMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavAddImageMsg.kt deleted file mode 100644 index 9f3a23d..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavAddImageMsg.kt +++ /dev/null @@ -1,159 +0,0 @@ -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.decodeFromByteArray -import kotlinx.serialization.json.JsonElement - -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 -import moe.fuqiuluo.symbols.OneBotHandler -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.fav.WeiyunComm - -@OneBotHandler("fav.add_image_msg", ["fav.add_image_message"]) -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.getFileByMd5(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 resp = data.decodeProtobuf() - .resp!!.fastUploadResourceResp!!.picResultList!!.first() - val picInfo = resp.picInfo!! - picUrl = picInfo.uri - picId = picInfo.picId - md5 = picInfo.name - } 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 resp = data.decodeProtobuf().resp!! - itemId = resp.addRichMediaResp!!.cid - } - - System.gc() - } - - return ok(PicInfo( - picUrl = picUrl, - picId = picId, - id = itemId - ), echo) - } - - override val requiredParams: Array = 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 - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavAddTextMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavAddTextMsg.kt deleted file mode 100644 index ae15c39..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavAddTextMsg.kt +++ /dev/null @@ -1,74 +0,0 @@ -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.decodeFromByteArray -import kotlinx.serialization.json.JsonElement - -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.DeflateTools -import moe.fuqiuluo.symbols.OneBotHandler -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.fav.WeiyunComm - -@OneBotHandler("fav.add_text_msg", ["fav.add_text_message"]) -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 resp = data.decodeProtobuf().resp!!.addRichMediaResp!! - ok(data = QFavItem(resp.cid), echo) - } else { - logic(it.mErrDesc, echo) - } - } - return ok("请求已提交", echo) - } - - override val requiredParams: Array = arrayOf("user_id", "nick", "content") - - @Serializable - private data class QFavItem( - @SerialName("id") val id: String - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavGetItemContent.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavGetItemContent.kt deleted file mode 100644 index 83d2d93..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavGetItemContent.kt +++ /dev/null @@ -1,65 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) - -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.io.core.ByteReadPacket -import kotlinx.io.core.discardExact -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.json.JsonElement - -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.DeflateTools -import moe.fuqiuluo.symbols.OneBotHandler -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.fav.WeiyunComm - -@OneBotHandler("fav.get_item_content") -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 resp = data.decodeProtobuf().resp!! - return ok(ItemContent( - resp.getFavContentResp!!.content!!.joinToString("") { - String(it.richMedia!!.rawData!!) - } - ), echo) - } - - override val requiredParams: Array = arrayOf("id") - - @Serializable - private data class ItemContent( - @SerialName("content") val content: String - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavGetItemList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavGetItemList.kt deleted file mode 100644 index 020b2f8..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/FavGetItemList.kt +++ /dev/null @@ -1,113 +0,0 @@ -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.decodeFromByteArray -import kotlinx.serialization.json.JsonElement - -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.DeflateTools -import moe.fuqiuluo.symbols.OneBotHandler -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.fav.WeiyunComm - -@OneBotHandler("fav.get_item_list") -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 resp = data.decodeProtobuf().resp!!.getFavListResp!! - - val itemList = arrayListOf() - val rawItemList = resp.collections!! - rawItemList.forEach { - val itemId = it.cid - val author = it.author!! - val authorType = author.type.toInt() - val authorId = author.numId.toLong() - val authorName = author.strId - val groupName: String - val groupId: Long - if (authorType == 2) { - groupName = author.groupName - groupId = author.groupId.toLong() - } else { - groupName = "" - groupId = 0L - } - val clientVersion = it.srcAppVer - val time = it.createTime.toLong() - itemList.add(Item( - id = itemId, - authorType = authorType, - author = authorId, - authorName = authorName, - groupName = groupName, - groupId = groupId, - clientVersion = clientVersion, - time = time - )) - } - - return ok(ItemList(itemList), echo) - } - - override val requiredParams: Array = arrayOf("category", "start_pos", "page_size") - - @Serializable - private data class ItemList( - val items: List - ) - - @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 - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCSRF.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCSRF.kt deleted file mode 100644 index 6462125..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCSRF.kt +++ /dev/null @@ -1,31 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.Credentials -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_csrf_token") -internal object GetCSRF: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val domain = session.getStringOrNull("domain") - ?: return invoke(session.echo) - return invoke(domain, session.echo) - } - - suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String { - val uin = TicketSvc.getUin() - val pskey = TicketSvc.getPSKey(uin, domain) - ?: return invoke(echo) - return ok(Credentials(bkn = TicketSvc.getCSRF(pskey)), echo) - } - - operator fun invoke(echo: JsonElement = EmptyJsonString): String { - val uin = TicketSvc.getUin() - val pskey = TicketSvc.getPSKey(uin) - return ok(Credentials(bkn = TicketSvc.getCSRF(pskey)), echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCookies.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCookies.kt deleted file mode 100644 index 174ca1c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCookies.kt +++ /dev/null @@ -1,32 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.Credentials -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_cookies", ["get_cookie"]) -internal object GetCookies: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val domain = session.getStringOrNull("domain") - ?: return invoke(session.echo) - return invoke(domain, session.echo) - } - - operator fun invoke(echo: JsonElement = EmptyJsonString): String { - return ok(Credentials( - cookie = TicketSvc.getCookie(), - bigDataTicket = TicketSvc.getBigdataTicket() - ), echo) - } - - suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String { - return ok(Credentials( - cookie = TicketSvc.getCookie(domain), - bigDataTicket = TicketSvc.getBigdataTicket() - ), echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCredentials.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCredentials.kt deleted file mode 100644 index 468b3fb..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetCredentials.kt +++ /dev/null @@ -1,40 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.Credentials -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_credentials") -internal object GetCredentials: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val domain = session.getStringOrNull("domain") - ?: return invoke(session.echo) - return invoke(domain, session.echo) - } - - operator fun invoke(echo: JsonElement = EmptyJsonString): String { - val uin = TicketSvc.getUin() - val skey = TicketSvc.getRealSkey(uin) - val pskey = TicketSvc.getPSKey(uin) - return ok( - Credentials( - bkn = TicketSvc.getCSRF(pskey), - cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey;" - ), echo) - } - - suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String { - val uin = TicketSvc.getUin() - val skey = TicketSvc.getRealSkey(uin) - val pskey = TicketSvc.getPSKey(uin, domain) ?: "" - val pt4token = TicketSvc.getPt4Token(uin, domain) ?: "" - return ok(Credentials( - bkn = TicketSvc.getCSRF(pskey), - cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token;" - ), echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetDeviceBattery.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetDeviceBattery.kt deleted file mode 100644 index a4b0076..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetDeviceBattery.kt +++ /dev/null @@ -1,19 +0,0 @@ -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.EmptyJsonString -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_device_battery") -internal object GetDeviceBattery: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return invoke(session.echo) - } - - operator fun invoke(echo: JsonElement = EmptyJsonString): String { - return ok(PlatformUtils.getDeviceBattery(), echo = echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetEssenceMessageList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetEssenceMessageList.kt deleted file mode 100644 index 9a78f0a..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetEssenceMessageList.kt +++ /dev/null @@ -1,29 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_essence_msg_list", ["get_essence_message_list"]) -internal object GetEssenceMessageList: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val page = session.getIntOrNull("page") ?: 0 - val pageSize = session.getIntOrNull("page_size") ?: 20 - return invoke(groupId, page, pageSize, session.echo) - } - - suspend operator fun invoke(groupId: Long, page: Int = 0, pageSize: Int = 20, echo: JsonElement = EmptyJsonString): String { - if (page < 0 || pageSize > 50) { - return badParam("参数不正确:page_size不得大于50", echo) - } - val essenceMessageList = GroupSvc.getEssenceMessageList(groupId, page, pageSize) - if (essenceMessageList.isSuccess) { - return ok(essenceMessageList.getOrNull(), echo) - } - return logic(essenceMessageList.exceptionOrNull()?.message ?: "", echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt deleted file mode 100644 index cb980e5..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt +++ /dev/null @@ -1,30 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.GetForwardMsgResult -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_forward_msg", ["get_forward_message"]) -internal object GetForwardMsg : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val id = session.getString("id") - return invoke(id, session.echo) - } - - suspend operator fun invoke( - resId: String, - echo: JsonElement = EmptyJsonString - ): String { - return ok( - data = GetForwardMsgResult( - msgs = MsgSvc.getForwardMsg(resId).getOrElse { return logic(it.toString(), echo = echo) }), - echo = echo - ) - } - - override val requiredParams: Array = arrayOf("id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetFriendList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetFriendList.kt deleted file mode 100644 index ff9bfef..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetFriendList.kt +++ /dev/null @@ -1,37 +0,0 @@ -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.remote.service.data.FriendEntry -import moe.fuqiuluo.shamrock.remote.service.data.PlatformType -import moe.fuqiuluo.qqinterface.servlet.FriendSvc -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_friend_list") -internal object GetFriendList: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", false)) - return invoke(refresh, session.echo) - } - - suspend operator fun invoke(refresh: Boolean, echo: JsonElement = EmptyJsonString): String { - val friendList = FriendSvc.getFriendList(refresh).onFailure { - return error(it.message ?: "unknown error", echo, arrayResult = true) - }.getOrThrow() - return ok(friendList.map { friend -> - FriendEntry( - id = friend.uin.toLong(), - name = friend.name, - displayName = friend.remark, - remark = friend.remark, - age = friend.age, - gender = friend.gender, - groupId = friend.groupid, - platformType = PlatformType.valueOf(friend.iTermType), - termType = friend.iTermType - ) - }, echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetFriendSystemMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetFriendSystemMsg.kt deleted file mode 100644 index 9807467..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetFriendSystemMsg.kt +++ /dev/null @@ -1,45 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_friend_system_msg", ["get_friend_system_message"]) -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) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGProChannelList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGProChannelList.kt deleted file mode 100644 index 5825a65..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGProChannelList.kt +++ /dev/null @@ -1,38 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -import moe.fuqiuluo.qqinterface.servlet.structures.GProChannelInfo -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_guild_channel_list") -internal object GetGProChannelList: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id") - val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", false)) - return invoke(guildId.toULong(), refresh, echo = session.echo) - } - - suspend operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String { - val result = withTimeoutOrNull(5000) { - GProSvc.getChannelList(guildId, refresh) - } ?: return error("timeout", echo) - result.onFailure { - return error(it.message ?: "unable to fetch channel list", echo) - } - return ok(GuildChannelListResult(result.getOrThrow()), echo, "success") - } - - override val requiredParams: Array = arrayOf("guild_id") - - @Serializable - data class GuildChannelListResult( - @SerialName("channel_list") val channelList: List - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupFileSystemInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupFileSystemInfo.kt deleted file mode 100644 index 4091b5b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupFileSystemInfo.kt +++ /dev/null @@ -1,22 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.FileSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_file_system_info") -internal object GetGroupFileSystemInfo: 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 { - return ok(data = FileSvc.getGroupFileSystemInfo(groupId), echo) - } - - override val requiredParams: Array = arrayOf("group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupFileUrl.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupFileUrl.kt deleted file mode 100644 index 50e1a6e..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupFileUrl.kt +++ /dev/null @@ -1,24 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.FileSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_file_url") -internal object GetGroupFileUrl: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val fileId = session.getString("file_id") - val busid = session.getInt("busid") - return invoke(groupId, fileId, busid, session.echo) - } - - suspend operator fun invoke(groupId: Long, fileId: String, busid: Int, echo: JsonElement = EmptyJsonString): String { - return ok(data = FileSvc.getGroupFileInfo(groupId, fileId, busid), echo = echo) - } - - override val requiredParams: Array = arrayOf("group_id", "file_id", "busid") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupMsgHistory.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupMsgHistory.kt deleted file mode 100644 index c3b182f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupMsgHistory.kt +++ /dev/null @@ -1,29 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import moe.fuqiuluo.shamrock.helper.db.MessageDB -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_msg_history", ["get_group_message_history"]) -internal object GetGroupMsgHistory: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val cnt = session.getIntOrNull("count") ?: 20 - val startId = session.getIntOrNull("message_id")?.let { - if (it == 0) return@let 0L - MessageDB.getInstance() - .messageMappingDao() - .queryByMsgHashId(it)?.qqMsgId - } ?: session.getIntOrNull("message_seq")?.let { - if (it == 0) return@let 0L - MessageDB.getInstance() - .messageMappingDao() - .queryByMsgSeq(MsgConstant.KCHATTYPEGROUP, groupId.toString(), it)?.qqMsgId - } ?: 0L - return GetHistoryMsg("group", groupId.toString(), cnt, startId, session.echo) - } - - override val requiredParams: Array = arrayOf("group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupNotice.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupNotice.kt deleted file mode 100644 index 54dd5cc..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupNotice.kt +++ /dev/null @@ -1,25 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("_get_group_notice", ["get_group_notice"]) -internal object GetGroupNotice: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - return invoke(groupId, session.echo) - } - - suspend operator fun invoke(groupId: Long, echo: JsonElement = EmptyJsonString): String { - val announcements = GroupSvc.getGroupAnnouncements(groupId) - if (announcements.isSuccess) { - return ok(announcements.getOrNull(), echo) - } - return logic(announcements.exceptionOrNull()?.message ?: "", echo) - - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRemainAtAllRemain.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRemainAtAllRemain.kt deleted file mode 100644 index b837da0..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRemainAtAllRemain.kt +++ /dev/null @@ -1,29 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_at_all_remain") -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 = arrayOf("group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRootFiles.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRootFiles.kt deleted file mode 100644 index 80b8a1b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRootFiles.kt +++ /dev/null @@ -1,25 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.FileSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_root_files") -internal object GetGroupRootFiles : 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 { - return ok( - FileSvc.getGroupRootFiles(groupId).getOrElse { return error(why = "获取失败: $it", echo = echo) }, - echo = echo - ) - } - - override val requiredParams: Array = arrayOf("group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupSubFiles.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupSubFiles.kt deleted file mode 100644 index cad77f9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupSubFiles.kt +++ /dev/null @@ -1,26 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.FileSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_files_by_folder") -internal object GetGroupSubFiles : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val folderId = session.getString("folder_id") - return invoke(groupId, folderId, session.echo) - } - - suspend operator fun invoke(groupId: Long, folderId: String, echo: JsonElement = EmptyJsonString): String { - return ok( - FileSvc.getGroupFiles(groupId, folderId).getOrElse { return error(why = "获取失败: $it", echo = echo) }, - echo = echo - ) - } - - override val requiredParams: Array = arrayOf("group_id", "folder_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupSystemMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupSystemMsg.kt deleted file mode 100644 index 82b69e5..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupSystemMsg.kt +++ /dev/null @@ -1,65 +0,0 @@ -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.remote.service.data.GroupRequest -import moe.fuqiuluo.shamrock.remote.service.data.GroupSystemMessage -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_system_msg", ["get_group_system_message"]) -internal object GetGroupSystemMsg: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return invoke(echo = session.echo) - } - - suspend operator fun invoke(echo: JsonElement = EmptyJsonString): String { - val list = GroupSvc.requestGroupSystemMsgNew(20) - val riskList = GroupSvc.requestGroupSystemMsgNew(20, 2) - val msgs = GroupSystemMessage( - invited = mutableListOf(), - join = mutableListOf() - ) - (list + riskList).forEach { - when(it.msg.group_msg_type.get()) { - 22, 1 -> { - // join 进群消息 - msgs.join += GroupRequest ( - msgSeq = it.msg_seq.get(), - invitorUin = it.msg.action_uin.get(), - invitorNick = it.msg.action_uin_nick.get(), - groupId = it.msg.group_code.get(), - groupName = it.msg.group_name.get(), - checked = it.msg.msg_decided.get().isNotBlank(), - actor = it.msg.actor_uin.get(), - requesterUin = it.req_uin.get(), - requesterNick = it.msg.req_uin_nick.get(), - message = it.msg.msg_additional.get(), - flag = "${it.msg_seq.get()};${it.msg.group_code.get()};${it.req_uin.get()}" - ) - } - 2 -> { - // invite 别人邀请我 - msgs.invited += GroupRequest ( - msgSeq = it.msg_seq.get(), - invitorUin = it.msg.action_uin.get(), - invitorNick = it.msg.action_uin_nick.get(), - groupId = it.msg.group_code.get(), - groupName = it.msg.group_name.get(), - checked = it.msg.msg_decided.get().isNotBlank(), - actor = it.msg.actor_uin.get(), - requesterUin = 0, - requesterNick = "", - message = it.msg.msg_additional.get(), - flag = "${it.msg_seq.get()};${it.msg.group_code.get()};${it.req_uin.get()}" - ) - } - - else -> {} - } - } - return ok(msgs, echo = echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildFeeds.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildFeeds.kt deleted file mode 100644 index 84fabd5..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildFeeds.kt +++ /dev/null @@ -1,38 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler -import protobuf.guild.StFeed - -@OneBotHandler("get_guild_feeds") -internal object GetGuildFeeds: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id").toULong() - val channelId = session.getStringOrNull("channel_id")?.toULong() ?: 0uL - val from = session.getIntOrNull("from") ?: 0 - return invoke(guildId, channelId, from, session.echo) - } - - suspend operator fun invoke(guildId: ULong, channelId: ULong, startIndex: Int, echo: JsonElement = EmptyJsonString): String { - val result = GProSvc.getGuildFeeds(guildId, channelId, startIndex).getOrElse { - GProSvc.getGuildFeeds(guildId, 0uL, startIndex).onFailure { - return error(it.message ?: "server error", echo) - }.getOrThrow() - } - if (result.vecFeed == null) { - return error("server error", echo) - } - return ok(GetGuildFeedsResult(result.isFinish == 1, result.vecFeed!!), echo = echo) - } - - @Serializable - data class GetGuildFeedsResult( - val isFinish: Boolean, - val feeds: List - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildList.kt deleted file mode 100644 index 91f4b94..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildList.kt +++ /dev/null @@ -1,35 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -import moe.fuqiuluo.qqinterface.servlet.structures.GuildInfo -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.EmptyJsonArray -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import moe.fuqiuluo.symbols.OneBotHandler -import mqq.app.MobileQQ - -@OneBotHandler("get_guild_list") -internal object GetGuildList : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val oldSdk = session.getBooleanOrDefault("old_sdk", false) - val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", false)) - return invoke(refresh, oldSdk, echo = session.echo) - } - - operator fun invoke(refresh: Boolean = true, oldSdk: Boolean, echo: JsonElement = EmptyJsonString): String { - val result = GProSvc.getGuildList(refresh, oldSdk) - return ok(GuildListResult(result), echo, "success") - } - - @Serializable - data class GuildListResult( - @SerialName("guild_list") var guildList: List = arrayListOf() - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMemberList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMemberList.kt deleted file mode 100644 index 3265dfc..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMemberList.kt +++ /dev/null @@ -1,79 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) - -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.encodeToByteArray -import kotlinx.serialization.json.JsonElement - -import moe.fuqiuluo.qqinterface.servlet.GProSvc -import moe.fuqiuluo.qqinterface.servlet.structures.GetGuildMemberListNextToken -import moe.fuqiuluo.qqinterface.servlet.structures.GuildMemberInfo -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.hex2ByteArray -import moe.fuqiuluo.shamrock.tools.toHexString -import moe.fuqiuluo.symbols.OneBotHandler -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.auto.toByteArray - -@OneBotHandler("get_guild_member_list") -internal object GetGuildMemberList: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id") - val all = session.getBooleanOrDefault("all", false) - return invoke(guildId.toULong(), all, session.getStringOrNull("next_token") ?: "", session.echo) - } - - suspend operator fun invoke(guildId: ULong, all: Boolean, nextTokenStr: String, echo: JsonElement = EmptyJsonString): String { - val curNextToken = if (nextTokenStr.isEmpty()) null else nextTokenStr.hex2ByteArray().decodeProtobuf() - val result = GProSvc.getGuildMemberList( - guildId = guildId, - fetchAll = all, - startIndex = curNextToken?.startIndex ?: 0, - roleIndex = curNextToken?.roleIndex ?: 1, - count = 50 - ) - result.onFailure { - return error(it.message ?: "unable to fetch guild member list", echo) - } - val nextToken = result.getOrThrow().first - val members = arrayListOf() - result.getOrThrow().second.forEach { - it.members.forEach { user -> - members.add(GuildMemberInfo( - tinyId = user.tinyId, - title = user.memberName, - nickname = user.nickName, - roleId = user.roleManagementTag.roleId, - roleName = user.roleManagementTag.tagName, - roleColor = user.roleManagementTag.color, - joinTime = user.joinTime, - robotType = user.robotType, - type = user.type, - inBlack = user.inBlack, - platform = user.platform - )) - } - } - return ok(GetGuildMemberListResult( - members = members, - finish = nextToken.finish, - nextToken = nextToken.toByteArray().toHexString(), - ), echo) - } - - override val requiredParams: Array = arrayOf("guild_id") - - @Serializable - data class GetGuildMemberListResult( - @SerialName("members") val members: List, - @SerialName("next_token") val nextToken: String, - @SerialName("finished") val finish: Boolean - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMemberProfile.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMemberProfile.kt deleted file mode 100644 index f97a66b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMemberProfile.kt +++ /dev/null @@ -1,78 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -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.ifNullOrEmpty -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_guild_member_profile") -internal object GetGuildMemberProfile: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id").toULong() - val userId = session.getLong("user_id").toULong() - return invoke(guildId, userId, session.echo) - } - - suspend operator fun invoke(guildId: ULong, userId: ULong, echo: JsonElement = EmptyJsonString): String { - val userResult = GProSvc.getUserGuildInfo(guildId, userId).onFailure { - return error(it.message ?: "unable to fetch guild member info", echo) - }.getOrThrow() - val roles = GProSvc.fetchGuildMemberRoles(guildId, userId).onFailure { - return error(it.message ?: "unable to fetch guild member roles", echo) - }.getOrThrow() - - return ok(GetGuildMemberInfo( - tinyId = userResult.memberTinyid, - nickname = userResult.nickName ?: "", - avatarUrl = userResult.url ?: "", - joinTime = userResult.joinTime, - roles = roles.map { - RoleInfo( - roleId = it.roleId.toString(), - roleName = it.name.ifNullOrEmpty(it.levelDsc.ifNullOrEmpty(it.displayTagName ?: ""))!!, - color = it.color, - permission = it.rolePermissions.permissionList.map { - Permission( - rootId = it.rootId, - childIds = it.childIds ?: emptyList() - ) - }, - type = it.type, - displayName = it.displayTagName ?: "" - ) - } - ), echo = echo) - } - - override val requiredParams: Array = arrayOf("guild_id", "user_id") - - @Serializable - data class GetGuildMemberInfo( - @SerialName("tiny_id") val tinyId: ULong, - @SerialName("nickname") val nickname: String, - @SerialName("avatar_url") val avatarUrl: String, - @SerialName("join_time") val joinTime: ULong, - @SerialName("roles") val roles: List - ) - - @Serializable - data class RoleInfo( - @SerialName("role_id") val roleId: String, - @SerialName("role_name") val roleName: String, - @SerialName("color") val color: Long, - @SerialName("permission") val permission: List, - @SerialName("type") val type: Int, - @SerialName("display_name") val displayName: String - ) - - @Serializable - data class Permission( - @SerialName("root_id") val rootId: Int, - @SerialName("child_ids") val childIds: List - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMetaByGuest.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMetaByGuest.kt deleted file mode 100644 index b7d0898..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildMetaByGuest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.protobuf.ProtoNumber -import moe.fuqiuluo.qqinterface.servlet.GProSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_guild_meta_by_guest") -internal object GetGuildMetaByGuest: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id").toULong() - return invoke(guildId, session.echo) - } - - suspend operator fun invoke(guildId: ULong, echo: JsonElement = EmptyJsonString): String { - val result = GProSvc.getGuildInfo(guildId) - result.onFailure { - return error(it.message ?: "unable to fetch guild info", echo) - } - val info = result.getOrThrow() - if (info.meta == null) { - return error("unable to fetch guild meta", echo) - } - val meta = info.meta!! - return ok(GetGuildMetaByGuestResponse( - guildId = info.guildId, - guildName = meta.name ?: "", - guildProfile = meta.profile ?: "", - createTime = meta.createTime, - maxMemberCount = meta.maxMemberCount, - maxRobotCount = meta.robotMaxNum, - maxAdminCount = meta.adminMaxNum, - memberCount = meta.memberCount, - ownerId = meta.ownerId, - guildDisplayId = meta.displayId ?: "" - ), echo) - } - - override val requiredParams: Array = arrayOf("guild_id") - - @Serializable - data class GetGuildMetaByGuestResponse( - @SerialName("guild_id") val guildId: ULong, - @SerialName("guild_name") val guildName: String, - @SerialName("guild_profile") val guildProfile: String, - @SerialName("create_time") val createTime: Long, - @SerialName("max_member_count") val maxMemberCount: Long, - @SerialName("max_robot_count") val maxRobotCount: Int, - @SerialName("max_admin_count") val maxAdminCount: Int, - @SerialName("member_count") val memberCount: Long, - @SerialName("owner_id") val ownerId: ULong, - @SerialName("guild_display_id") val guildDisplayId: String - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildRoles.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildRoles.kt deleted file mode 100644 index 830fd0d..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildRoles.kt +++ /dev/null @@ -1,61 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildMemberProfile.Permission -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_guild_roles") -internal object GetGuildRoles: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id").toULong() - return invoke(guildId, session.echo) - } - - suspend operator fun invoke(guildId: ULong, echo: JsonElement = EmptyJsonString): String { - val result = GProSvc.getGuildRoles(guildId).onFailure { - return error(it.message ?: "unable to fetch guild roles", echo) - }.getOrThrow() - return ok(GetGuildRolesResult(result.map { - GuildRole( - color = it.color, - disabled = it.bHoist, - independent = it.isChannelRole, - maxCount = it.memberLimit, - memberCount = it.count, - owned = it.isNotSort, - roleId = it.roleId, - roleName = it.name, - permission = it.rolePermissions.permissionList.map { - Permission(it.rootId, it.childIds) - }, - ) - }), echo = echo) - } - - override val requiredParams: Array = arrayOf("guild_id") - - @Serializable - data class GetGuildRolesResult( - @SerialName("roles") val roles: List - ) - - @Serializable - data class GuildRole( - @SerialName("argb_color") val color: Long, - @SerialName("disabled") val disabled: Boolean, - @SerialName("independent") val independent: Boolean, - @SerialName("max_count") val maxCount: Int, - @SerialName("member_count") val memberCount: Int, - @SerialName("owned") val owned: Boolean, - @SerialName("role_id") val roleId: Long, - @SerialName("role_name") val roleName: String, - @SerialName("permission") val permission: List, - ) - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildServiceProfile.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildServiceProfile.kt deleted file mode 100644 index af98a28..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGuildServiceProfile.kt +++ /dev/null @@ -1,39 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -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.symbols.OneBotHandler - -@OneBotHandler("get_guild_service_profile") -internal object GetGuildServiceProfile : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return invoke(echo = session.echo) - } - - suspend operator fun invoke(echo: JsonElement = EmptyJsonString): String { - val result = GProSvc.getSelfGuildInfo() - result.onFailure { - return error(it.message ?: "unable to fetch self guild info", echo) - } - val info = result.getOrThrow() - //LogCenter.log(info.toString()) - return ok(GuildServiceProfile( - nickName = info.nickName ?: info.memberName ?: "", - tinyId = info.memberTinyid, - avatarUrl = info.url ?: "" - ), echo = echo) - } - - @Serializable - data class GuildServiceProfile( - @SerialName("nickname") val nickName: String, - @SerialName("tiny_id") val tinyId: ULong, - @SerialName("avatar_url") val avatarUrl: String, - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt deleted file mode 100644 index e6bfc60..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt +++ /dev/null @@ -1,133 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgRecord -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.qqinterface.servlet.msg.toSegments -import moe.fuqiuluo.qqinterface.servlet.msg.toListMap -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.helper.db.MessageDB -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail -import moe.fuqiuluo.shamrock.remote.service.data.MessageSender -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import moe.fuqiuluo.symbols.OneBotHandler -import java.util.ArrayList -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -@OneBotHandler("get_history_msg", ["get_history_message"]) -internal object GetHistoryMsg : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val msgType = session.getString("message_type") - val peerId = session.getLong(if (msgType == "group") "group_id" else "user_id").toString() - val cnt = session.getIntOrNull("count") ?: 20 - - val startId = session.getIntOrNull("message_id")?.let { - if (it == 0) return@let 0L - MessageDB.getInstance() - .messageMappingDao() - .queryByMsgHashId(it)?.qqMsgId - } ?: session.getIntOrNull("message_seq")?.let { - if (it == 0) return@let 0L - MessageDB.getInstance() - .messageMappingDao() - .queryByMsgSeq(MessageHelper.obtainMessageTypeByDetailType(msgType), peerId, it)?.qqMsgId - } ?: 0L - - return invoke(msgType, peerId, cnt, startId, echo = session.echo) - } - - suspend operator fun invoke( - msgType: String, - peerId: String, - cnt: Int, - startMsgId: Long = 0, - echo: JsonElement = EmptyJsonString - ): String { - val msgService = NTServiceFetcher.kernelService.wrapperSession.msgService - val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType) - val contact = MessageHelper.generateContact(chatType, peerId) - val result = suspendCoroutine { - msgService.getMsgs(contact, startMsgId, cnt, true) { code, why, msgs -> - it.resume(GetMsgResult(code, why, msgs)) - } - } - if (result.code != 0) { - return logic(result.msg ?: "获取历史消息失败", echo = echo) - } - - val msgList = ArrayList().apply { - addAll(result.data!!.map { msg -> - val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId) - MessageDetail( - time = msg.msgTime.toInt(), - msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), - msgId = msgHash, - qqMsgId = msg.msgId, - msgSeq = msg.msgSeq, - realId = msg.msgSeq, - sender = MessageSender( - msg.senderUin, msg.sendNickName, "unknown", 0, msg.senderUid, msg.senderUid - ), - message = msg.elements.toSegments( - msg.chatType, - if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(), - msg.channelId ?: msg.peerUin.toString() - ).toListMap(), - peerId = msg.peerUin, - groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0, - targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0 - ) - }) - if (startMsgId != 0L) { - val msg = MsgSvc.getMsgByQMsgId(chatType, peerId, startMsgId).onFailure { - return logic("Obtain msg failed, please check your msg_id.", echo) - }.getOrThrow() - add(MessageDetail( - time = msg.msgTime.toInt(), - msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), - msgId = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId), - qqMsgId = msg.msgId, - msgSeq = msg.msgSeq, - realId = msg.msgSeq, - sender = MessageSender( - msg.senderUin, msg.sendNickName - .ifEmpty { msg.sendMemberName } - .ifEmpty { msg.sendRemarkName } - .ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid, msg.senderUid - ), - message = msg.elements.toSegments( - chatType, - if (chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(), - msg.channelId ?: peerId - ).toListMap(), - peerId = msg.peerUin, - groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0, - targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0 - ) - ) - } - } - - return ok(data = GetHistoryMsgResult(msgList), echo = echo) - } - - override val requiredParams: Array = arrayOf("message_type") - - @Serializable - data class GetHistoryMsgResult( - @SerialName("messages") val msgs: List - ) - - data class GetMsgResult( - val code: Int, - val msg: String?, - val data: ArrayList? - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHttpCookies.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHttpCookies.kt deleted file mode 100644 index 4af7af6..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHttpCookies.kt +++ /dev/null @@ -1,29 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.Credentials -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_http_cookies") -internal object GetHttpCookies : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val appid = session.getString("appid") - val daid = session.getString("daid") - val jumpurl = session.getString("jumpurl") - return invoke(appid, daid, jumpurl, session.echo) - } - - suspend operator fun invoke( - appid: String, - daid: String, - jumpurl: String, - echo: JsonElement = EmptyJsonString - ): String { - val ck = TicketSvc.GetHttpCookies(appid, daid, jumpurl) ?: "" - return ok(Credentials(cookie = ck), echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetImage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetImage.kt deleted file mode 100644 index be2c23b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetImage.kt +++ /dev/null @@ -1,73 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc -import moe.fuqiuluo.shamrock.helper.db.ImageDB -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_image", ["get_img"]) -internal object GetImage: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val echo = session.echo - val file = session.getString("file") - return invoke(file, echo) - } - - suspend operator fun invoke(file: String, echo: JsonElement = EmptyJsonString): String { - val fileMd5 = file - .replace("{", "") - .replace("}", "") - .replace("-", "") - .split(".")[0].uppercase().trim() - if (fileMd5.length != 32) { - return badParam("图片缓存文件名不合法", echo = echo) - } - - val image = ImageDB.getInstance().imageMappingDao().queryByFileName(fileMd5) - ?: return logic("只能查询已缓存的图片", echo = echo) - - return ok(GetImageResult( - image.size, - image.fileName, - when(image.chatType) { - MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl( - originalUrl = "", - md5 = fileMd5, - fileSize = image.size.toULong(), - sha = "", - fileId = image.fileId, - width = 100u, - height = 100u, - peer = TicketSvc.getUin() - ) - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl( - originalUrl = "", - md5 = fileMd5, - fileSize = image.size.toULong(), - sha = "", - fileId = image.fileId, - storeId = image.storeId, - width = 100u, - height = 100u, - peer = TicketSvc.getUin() - ) - else -> error("Not supported chat type: ${image.chatType}, convertMsgElementsToMsgSegment::Pic") - } - ), echo = echo) - } - - override val requiredParams: Array = arrayOf("file") - - @Serializable - data class GetImageResult( - val size: Long, - val filename: String, - val url: String - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetLatestEvents.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetLatestEvents.kt deleted file mode 100644 index 41b1f0d..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetLatestEvents.kt +++ /dev/null @@ -1,17 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.structures.EmptyObject -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_latest_events") -internal object GetLatestEvents: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return resultToString( - true, Status.Ok, listOf(), echo = session.echo - ) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetLoginInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetLoginInfo.kt deleted file mode 100644 index 1e6e5f6..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetLoginInfo.kt +++ /dev/null @@ -1,33 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.mobileqq.app.QQAppInterface -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.structures.StdAccount -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.symbols.OneBotHandler -import mqq.app.MobileQQ - -@OneBotHandler("get_login_info") -internal object GetLoginInfo: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return invoke(session.echo) - } - - operator fun invoke(echo: JsonElement = EmptyJsonString): String { - val accounts = MobileQQ.getMobileQQ().allAccounts - val runtime = AppRuntimeFetcher.appRuntime - val curUin = runtime.currentAccountUin - val account = accounts.firstOrNull { it.uin == curUin } - return if (account == null || !account.isLogined) { - error("当前不处于已登录状态", echo = echo) - } else { - ok( - StdAccount( - curUin.toLong(),if (runtime is QQAppInterface) runtime.currentNickname else "unknown" - ), echo = echo) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetModelShow.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetModelShow.kt deleted file mode 100644 index dc37988..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetModelShow.kt +++ /dev/null @@ -1,31 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.CardSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_model_show") -internal object GetModelShow: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val uin = session.getLongOrNull("user_id") - return if (uin == null) { - invoke(session.echo) - } else { - invoke(uin, session.echo) - } - } - - suspend operator fun invoke(echo: JsonElement = EmptyJsonString): String { - return ok(CardSvc.getModelShow(), echo) - } - - suspend operator fun invoke(uin: Long, echo: JsonElement = EmptyJsonString): String { - if (uin == 0L) { - return invoke(echo) - } - return ok(CardSvc.getModelShow(uin), echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetModelShowList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetModelShowList.kt deleted file mode 100644 index 49963e7..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetModelShowList.kt +++ /dev/null @@ -1,115 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.request.parameter -import io.ktor.client.statement.bodyAsText -import io.ktor.client.statement.request -import io.ktor.http.HttpStatusCode -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.GlobalClient -import moe.fuqiuluo.shamrock.tools.GlobalJson -import moe.fuqiuluo.shamrock.tools.json -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("_get_model_show") -internal object GetModelShowList : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return invoke(session.getString("model"), session.echo) - } - - suspend operator fun invoke(model: String, echo: JsonElement = EmptyJsonString): String { - val ts = System.currentTimeMillis() / 1000 - val csrf = TicketSvc.getCSRF(TicketSvc.getUin(), "vip.qq.com") - - val req = mapOf( - "13030" to mapOf( - "req" to mapOf( - "lUin" to TicketSvc.getUin().toLong(), - "sModel" to model.replace("+", "%20"), - "iAppType" to 0, - "sIMei" to "", - "bShowInfo" to true, - "sModelShow" to "", - "bRecoverDefault" to false - ) - ) - ).json.toString() - - val resp = GlobalClient.get("https://proxy.vip.qq.com/cgi-bin/srfentry.fcgi") { - parameter("ts", ts) - parameter("daid", 18) - parameter("g_tk", csrf) - parameter("pt4_token", "") - parameter("data", req) - val cookie = TicketSvc.getCookie("vip.qq.com") - header("Cookie", cookie) - } - - if (resp.status != HttpStatusCode.OK) { - LogCenter.log({ "unable to fetch model show list: ${resp.request.url} => ${resp.status}" }, Level.DEBUG) - return error("unable to fetch model show list: ${resp.status}", echo) - } - - val json = kotlin.runCatching { - GlobalJson.decodeFromString(resp.bodyAsText()) - }.onFailure { - it.printStackTrace() - }.getOrNull() - - if (json?.resp == null) { - return error("unable to fetch model show list", echo) - } - - return ok(GetModelListResp(json.resp.data.rsp.vItemList.map { - Model(it.sModelShow, it.bNeedPay) - }), echo) - } - - override val requiredParams: Array = arrayOf("model") - - @Serializable - data class GetModelListResp( - @SerialName("variants") val resp: List - ) - - @Serializable - data class Model( - @SerialName("model_show") val model: String, - @SerialName("need_pay") val needPay: Boolean - ) - - @Serializable - data class ModelShowStruct( - @SerialName("13030") val resp: ModelGetResp? = null - ) - - @Serializable - data class ModelGetResp( - @SerialName("data") val data: ModelGetData - ) - - @Serializable - data class ModelGetData( - @SerialName("rsp") val rsp: ModelGetRsp - ) - - @Serializable - data class ModelGetRsp( - @SerialName("vItemList") val vItemList: List - ) - - @Serializable - data class ModelGetItem( - @SerialName("sModelShow") val sModelShow: String, - @SerialName("bNeedPay") val bNeedPay: Boolean - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt deleted file mode 100644 index c7dfbe8..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt +++ /dev/null @@ -1,56 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import kotlinx.serialization.json.JsonElement -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.data.MessageDetail -import moe.fuqiuluo.shamrock.remote.service.data.MessageSender -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.qqinterface.servlet.msg.toSegments -import moe.fuqiuluo.qqinterface.servlet.msg.toListMap -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_message", ["get_msg"]) -internal object GetMsg: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val hashCode = session.getIntOrNull("message_id") - ?: session.getInt("msg_id") - return invoke(hashCode, session.echo) - } - - suspend operator fun invoke(msgHash: Int, echo: JsonElement = EmptyJsonString): String { - val msg = MsgSvc.getMsg(msgHash).onFailure { - return logic("Obtain msg failed, please check your msg_id.", echo) - }.getOrThrow() - return ok(MessageDetail( - time = msg.msgTime.toInt(), - msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), - msgId = msgHash, - qqMsgId = msg.msgId, - msgSeq = msg.msgSeq, - realId = msg.msgSeq, - sender = MessageSender( - msg.senderUin, msg.sendNickName - .ifEmpty { msg.sendMemberName } - .ifEmpty { msg.sendRemarkName } - .ifEmpty { msg.peerName }, "unknown", - 0, - msg.senderUid, - msg.senderUid - ), - message = msg.elements.toSegments( - msg.chatType, - if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(), - msg.channelId ?: msg.peerUin.toString() - ).toListMap(), - peerId = msg.peerUin, - groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0, - targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0 - ), echo) - } - - override val requiredParams: Array = arrayOf("message_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetNotJoinedGroupInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetNotJoinedGroupInfo.kt deleted file mode 100644 index 5c60fcb..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetNotJoinedGroupInfo.kt +++ /dev/null @@ -1,25 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_not_joined_group_info") -internal object GetNotJoinedGroupInfo: 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 { - GroupSvc.getNotJoinedGroupInfo(groupId = groupId).onSuccess { - return ok(it, echo = echo) - }.exceptionOrNull()?.let { - return error(it.message ?: "无法获取群信息", echo = echo) - } - return logic("Unable to obtain group information", echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetOnlineClients.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetOnlineClients.kt deleted file mode 100644 index 666c997..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetOnlineClients.kt +++ /dev/null @@ -1,38 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.QSafeSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("_get_online_clients") -internal object GetOnlineClients: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return invoke(session.echo) - } - - suspend operator fun invoke(echo: JsonElement = EmptyJsonString): String { - val clients = QSafeSvc.getOnlineClients() - ?: return logic("获取在线设备信息失败", echo, arraayResult = true) - return ok(clients.map { - DevInfo(it.iAppId, it.strDeviceName, it.strDeviceTypeInfo, it.iLoginTime, - it.iLoginPlatform, it.strLoginLocation - ) - }, echo) - } - - @Serializable - data class DevInfo( - @SerialName("app_id") val appId: Long, - @SerialName("device_name") val deviceName: String?, - @SerialName("device_kind") val deviceType: String?, - @SerialName("login_time") val loginTime: Long?, - @SerialName("login_platform") val loginPlatform: Long?, - @SerialName("location") val location: String?, - @SerialName("guid") val guid: String? = "" - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetProfileCard.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetProfileCard.kt deleted file mode 100644 index 053040a..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetProfileCard.kt +++ /dev/null @@ -1,78 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.mobileqq.data.Card -import moe.fuqiuluo.qqinterface.servlet.CardSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.shamrock.remote.service.data.VipInfo -import moe.fuqiuluo.shamrock.remote.service.data.VipType -import moe.fuqiuluo.shamrock.remote.service.data.profile.Location -import moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileCard -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_user_info", ["get_profile_card"]) -internal object GetProfileCard: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val uin = session.getLong("user_id") - val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", false)) - - var card: Card? = CardSvc.getProfileCard(uin).getOrNull() - if (refresh || !card.ok()) { - card = CardSvc.refreshAndGetProfileCard(uin).getOrNull() - } - if (!card.ok()) { - return logic("get profilecard error, please check your user_id or network", session.echo) - } - requireNotNull(card) - - return resultToString(true, Status.Ok, ProfileCard( - uin = card.uin.toLong(), - name = card.strNick, - mail = card.strShowName ?: card.strEmail ?: "", - remark = card.strReMark.let { if (it.isNullOrEmpty()) card.strAutoRemark else it }, - findMethod = card.addSrcName, - displayName = card.strContactName, - maxVoteCnt = card.bAvailVoteCnt, - haveVoteCnt = card.bHaveVotedCnt, - vipList = arrayListOf().apply { - if (card.bQQVipOpen == 1.toByte()) { - add(VipInfo(VipType.QQ_VIP, card.iQQVipLevel, card.iQQVipType, 0)) - } - if (card.bSuperQQOpen == 1.toByte()) { - add(VipInfo(VipType.SUPER_QQ, card.iSuperQQLevel, card.iSuperQQType, 0)) - } - if (card.bSuperVipOpen == 1.toByte()) { - add(VipInfo(VipType.SUPER_VIP, card.iSuperVipLevel, card.iSuperVipType, card.lSuperVipTemplateId)) - } - if (card.bHollywoodVipOpen == 1.toByte()) { - add(VipInfo(VipType.QQ_VIDEO, card.iHollywoodVipLevel, card.iHollywoodVipType, 0)) - } - if (card.bBigClubVipOpen == 1.toByte()) { - add(VipInfo(VipType.BIG_VIP, card.iBigClubVipLevel, card.iBigClubVipType, card.lBigClubTemplateId)) - } - if (card.isYellowDiamond || card.isSuperYellowDiamond) { - add(VipInfo(VipType.YELLOW_VIP, card.yellowLevel, 0, 0)) - } - }, - hobbyEntry = card.hobbyEntry, - level = card.iQQLevel, - birthday = card.lBirthday, - loginDay = card.lLoginDays, - voteCnt = card.lVoteCount, - qid = card.qid, - schoolVerified = card.schoolVerifiedFlag, - location = Location( - card.strCity, card.strCompany, card.strCountry, card.strProvince, card.strHometownDesc, card.strSchool - ), - cookie = card.vCookies - ), echo = session.echo) - } - - override val requiredParams: Array = arrayOf("user_id") - - private fun Card?.ok(): Boolean { - return this != null && !strNick.isNullOrBlank() - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetProhibitedMemberList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetProhibitedMemberList.kt deleted file mode 100644 index 55baff1..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetProhibitedMemberList.kt +++ /dev/null @@ -1,29 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_prohibited_member_list") -internal object GetProhibitedMemberList: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupCode = session.getLong("group_id") - return invoke(groupCode, session.echo) - } - - suspend operator fun invoke( - groupCode: Long, - echo: JsonElement = EmptyJsonString - ): String { - val result = GroupSvc.getProhibitedMemberList(groupCode) - if (result.isFailure) { - return error(result.exceptionOrNull()?.message ?: "获取禁言列表失败", echo, arrayResult = true) - } - return ok(result.getOrThrow(), echo) - } - - override val requiredParams: Array = arrayOf("group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetRecord.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetRecord.kt deleted file mode 100644 index e5bb42e..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetRecord.kt +++ /dev/null @@ -1,42 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.helper.LocalCacheHelper -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.OutResource -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.utils.AudioUtils -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_record") -internal object GetRecord: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val file = session.getString("file") - .replace(regex = "[{}\\-]".toRegex(), replacement = "") - .replace(" ", "") - .split(".")[0].lowercase() - val format = session.getString("out_format") - return invoke(file, format, session.echo) - } - - operator fun invoke(file: String, format: String, echo: JsonElement = EmptyJsonString): String { - val pttFile = LocalCacheHelper.getCachePttFile(file) - return if(pttFile.exists()) { - val isSilk = AudioUtils.isSilk(pttFile) - val audioFile = when(format) { - "amr" -> AudioUtils.audioToAmr(pttFile, isSilk) - else -> AudioUtils.audioToFormat(pttFile, isSilk, format) - } - ok( - OutResource( - audioFile.toString(), - url = "/res/${audioFile.nameWithoutExtension}" - ), echo) - } else { - error("not found record file from cache", echo) - } - } - - override val requiredParams: Array = arrayOf("file", "out_format") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetSelfInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetSelfInfo.kt deleted file mode 100644 index bea66cc..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetSelfInfo.kt +++ /dev/null @@ -1,24 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.mobileqq.app.QQAppInterface -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.shamrock.remote.service.data.UserDetail -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_self_info") -internal object GetSelfInfo: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - //val accounts = MobileQQ.getMobileQQ().allAccounts - val runtime = AppRuntimeFetcher.appRuntime as QQAppInterface - val curUin = runtime.currentAccountUin - //val account = accounts.firstOrNull { it.uin == curUin } - - return resultToString(true, Status.Ok, UserDetail( - curUin.toLong(), runtime.currentNickname, runtime.currentNickname - ), echo = session.echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetStatus.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetStatus.kt deleted file mode 100644 index b552483..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetStatus.kt +++ /dev/null @@ -1,23 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.shamrock.remote.service.data.BotStatus -import moe.fuqiuluo.shamrock.remote.service.data.Self -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_status", ["status"]) -internal object GetStatus: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val runtime = AppRuntimeFetcher.appRuntime - val curUin = runtime.currentAccountUin - return resultToString(true, Status.Ok, listOf( - BotStatus( - Self("qq", curUin.toLong()), runtime.isLogin, status = "正常", good = runtime.isLogin - ) - ), echo = session.echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetStrangerInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetStrangerInfo.kt deleted file mode 100644 index f4d07d8..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetStrangerInfo.kt +++ /dev/null @@ -1,160 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.mobileqq.data.Card -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.toHexString -import moe.fuqiuluo.qqinterface.servlet.CardSvc -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_stranger_info", ["_get_stranger_info"]) -internal object GetStrangerInfo: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val userId = session.getLong("user_id") - return invoke(userId, session.echo) - } - - suspend operator fun invoke(userId: Long, echo: JsonElement = EmptyJsonString): String { - val info = CardSvc.refreshAndGetProfileCard(userId).onFailure { - return logic("unable to fetch stranger info", echo) - }.getOrThrow() - - return ok(info.run { - StrangerInfo( - uid = userId, - nickname = strNick, - age = age, - sex = when(shGender) { - Card.FEMALE -> "female" - Card.MALE -> "male" - else -> "unknown" - }, - level = iQQLevel, - loginDays = lLoginDays, - qid = qid ?: "", - vote = lVoteCount, - wzryHonor = wzryHonorInfo?.toHexString(), - ext = StrangerInfoExt( - addSrcId, addSrcName, addSubSrcId, - - allowCalInteractive, allowClick, allowPeopleSee, authState, - - bBigClubVipOpen, bHollywoodVipOpen, bQQVipOpen, bSuperQQOpen, bSuperVipOpen, - - bVoted, babyQSwitch, bindPhoneInfo, cardId, cardType, category, clothesId, - - coverUrl, declaration, defaultCardId, diyComplicatedInfo, diyDefaultText, - - diyText, diyTextDegree, diyTextFontId, diyTextHeight, diyTextWidth, - - diyTextLocX, diyTextLocY, dressUpIsOn, encId, enlargeQzonePic, - - extendFriendEntryAddFriend, extendFriendEntryContact, extendFriendFlag, - - extendFriendQuestion, extendFriendVoiceDuration, favoriteSource, feedPreviewTime, - - fontId, fontType, qidBgUrl, qidColor, qidLogoUrl, qqCardIsOn, schoolId, - - schoolName, schoolVerifiedFlag, showPublishButton, singer, songDuration, - - songId, songName - ) - ) - }, echo) - } - - override val requiredParams: Array = arrayOf("user_id") - - @Serializable - data class StrangerInfo( - @SerialName("user_id") val uid: Long, - @SerialName("nickname") val nickname: String, - @SerialName("age") val age: Byte, - @SerialName("sex") val sex: String, - @SerialName("level") val level: Int, - @SerialName("login_days") val loginDays: Long, - @SerialName("qid") val qid: String?, - @SerialName("vote") val vote: Long, - @SerialName("wzry_honor") val wzryHonor: String?, - @SerialName("ext") val ext: StrangerInfoExt - ) - - @Serializable - data class StrangerInfoExt( - @SerialName("add_src_id") val addSrcId: Long, - @SerialName("add_src_name") val addSrcName: String?, - @SerialName("add_sub_src_id") val addSubSrcId: Long, - - @SerialName("allow_cal_interactive") val allowCalInteractive: Boolean, - @SerialName("allow_click") val allowClick: Boolean, - @SerialName("allow_people_see") val allowPeopleSee: Boolean, - @SerialName("auth_state") val authState: Long, - - @SerialName("big_club_vip_open") val bBigClubVipOpen: Byte, - @SerialName("hollywood_vip_open") val bHollywoodVipOpen: Byte, - @SerialName("qq_vip_open") val bQQVipOpen: Byte, - @SerialName("super_qq_open") val bSuperQQOpen: Byte, - @SerialName("super_vip_open") val bSuperVipOpen: Byte, - - @SerialName("voted") val bVoted: Byte, - @SerialName("baby_q_switch") val babyQSwitch: Boolean, - - @SerialName("bind_phone_info") val bindPhoneInfo: String?, - - @SerialName("card_id") val cardId: Long, - @SerialName("card_type") val cardType: Int, - @SerialName("category") val category: Int, - @SerialName("clothes_id") val clothesId: Int, - - @SerialName("cover_url") val coverUrl: String?, - @SerialName("declaration") val declaration: String?, - @SerialName("default_card_id") val defaultCardId: Int, - - @SerialName("diy_complicated_info") val diyComplicatedInfo: String?, - @SerialName("diy_default_text") val diyDefaultText: String?, - @SerialName("diy_text") val diyText: String?, - @SerialName("diy_text_degree") val diyTextDegree: Float, - @SerialName("diy_text_font_id") val diyTextFontId: Int, - @SerialName("diy_text_height") val diyTextHeight: Float, - @SerialName("diy_text_width") val diyTextWidth: Float, - @SerialName("diy_text_loc_x") val diyTextLocX: Float, - @SerialName("diy_text_loc_y") val diyTextLocY: Float, - - @SerialName("dress_up_is_on") val dressUpIsOn: Boolean, - @SerialName("enc_id") val encId: String?, - @SerialName("enlarge_qzone_pic") val enlargeQzonePic: Int, - @SerialName("extend_friend_entry_add_friend") val extendFriendEntryAddFriend: Short, - @SerialName("extend_friend_entry_contact") val extendFriendEntryContact: Short, - @SerialName("extend_friend_flag") val extendFriendFlag: Int, - @SerialName("extend_friend_question") val extendFriendQuestion: Short, - @SerialName("extend_friend_voice_duration") val extendFriendVoiceDuration: Int, - @SerialName("favorite_source") val favoriteSource: Int, - @SerialName("feed_preview_time") val feedPreviewTime: Long, - @SerialName("font_id") val fontId: Int, - @SerialName("font_type") val fontType: Int, - - @SerialName("qid_bg_url") val qidBgUrl: String?, - @SerialName("qid_color") val qidColor: String?, - @SerialName("qid_logo_url") val qidLogoUrl: String?, - - @SerialName("qq_card_is_on") val qqCardIsOn: Boolean, - - @SerialName("school_id") val schoolId: String?, - @SerialName("school_name") val schoolName: String?, - @SerialName("school_verified_flag") val schoolVerifiedFlag: Boolean, - - @SerialName("show_publish_button") val showPublishButton: Boolean, - - @SerialName("singer") val singer: String?, - @SerialName("song_dura") val songDuration: Long, - - @SerialName("song_id") val songId: String?, - @SerialName("song_name") val songName: String?, - - - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetSupportedActions.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetSupportedActions.kt deleted file mode 100644 index bd5c9c7..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetSupportedActions.kt +++ /dev/null @@ -1,15 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import moe.fuqiuluo.shamrock.remote.action.ActionManager -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_supported_actions") -internal object GetSupportedActions: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return resultToString(true, Status.Ok, ActionManager.actionMap.keys.toList(), echo = session.echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopHonor.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopHonor.kt deleted file mode 100644 index c3d6ae3..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopHonor.kt +++ /dev/null @@ -1,58 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.GroupAllHonor -import moe.fuqiuluo.shamrock.remote.service.data.GroupMemberHonor -import moe.fuqiuluo.shamrock.remote.service.data.HONOR_GROUP_FIRE -import moe.fuqiuluo.shamrock.remote.service.data.HONOR_GROUP_FLAME -import moe.fuqiuluo.shamrock.remote.service.data.HONOR_HAPPY -import moe.fuqiuluo.shamrock.remote.service.data.HONOR_NEWBIE -import moe.fuqiuluo.shamrock.remote.service.data.HONOR_TALKATIVE -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_honor_info", ["get_troop_honor_info"]) -internal object GetTroopHonor: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", false)) - return invoke(groupId, refresh, session.echo) - } - - suspend operator fun invoke(groupId: Long, refresh: Boolean, echo: JsonElement = EmptyJsonString): String { - val honorInfo = ArrayList() - - GroupSvc.getGroupMemberList(groupId, refresh).onFailure { - return error(it.message ?: "unknown error", echo) - }.onSuccess { memberList -> - memberList.forEach { member -> - GroupSvc.parseHonor(member.honorList).forEach { - val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag) - if (honor != null) { - honor.nick = member.troopnick.ifEmpty { member.friendnick } - honorInfo.add(honor) - } - } - } - } - - return ok(GroupAllHonor( - groupId = groupId, - currentTalkActive = honorInfo.firstOrNull { - it.id == HONOR_TALKATIVE - }, - talkativeList = honorInfo.filter { it.id == HONOR_TALKATIVE }, - performerList = honorInfo.filter { it.id == HONOR_GROUP_FIRE }, - legendList = honorInfo.filter { it.id == HONOR_GROUP_FLAME }, - strongNewbieList = honorInfo.filter { it.id == HONOR_NEWBIE }, - emotionList = honorInfo.filter { it.id == HONOR_HAPPY }, - all = honorInfo - ), echo) - } - - override val requiredParams: Array = arrayOf("group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopInfo.kt deleted file mode 100644 index 6bbcde5..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopInfo.kt +++ /dev/null @@ -1,41 +0,0 @@ -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.remote.service.data.SimpleTroopInfo -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_info") -internal object GetTroopInfo: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", false)) - return invoke(groupId, refresh, session.echo) - } - - suspend operator fun invoke(groupId: Long, refresh: Boolean, echo: JsonElement = EmptyJsonString): String { - val groupInfo = GroupSvc.getGroupInfo(groupId, refresh).getOrNull() - return if ( groupInfo == null || groupInfo.troopuin.isNullOrBlank()) { - logic("Unable to obtain group information", echo) - } else { - ok(SimpleTroopInfo( - groupId = groupInfo.troopuin.toLong(), - groupUin = groupInfo.troopcode.toLong(), - groupName = groupInfo.troopname ?: groupInfo.newTroopName ?: groupInfo.oldTroopName, - groupRemark = groupInfo.troopRemark, - adminList = GroupSvc.getAdminList(groupId, true), - classText = groupInfo.mGroupClassExtText, - isFrozen = groupInfo.mIsFreezed != 0, - maxMember = groupInfo.wMemberMax, - memNum = groupInfo.wMemberNum, - memCount = groupInfo.wMemberNum, - maxNum = groupInfo.wMemberMax, - ), echo) - } - } - - override val requiredParams: Array = arrayOf("group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopList.kt deleted file mode 100644 index 3ad007b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopList.kt +++ /dev/null @@ -1,46 +0,0 @@ -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.remote.service.data.SimpleTroopInfo -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_list") -internal object GetTroopList : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", true)) - return invoke(refresh, session.echo) - } - - suspend operator fun invoke(refresh: Boolean, echo: JsonElement = EmptyJsonString): String { - val troopList = arrayListOf() - GroupSvc.getGroupList(refresh).onFailure { - return error(it.message ?: "unknown error", echo, arrayResult = true) - }.onSuccess { troops -> - troops.forEach { groupInfo -> - if (groupInfo.troopcode.isNullOrEmpty()) return@forEach - - troopList.add( - SimpleTroopInfo( - groupId = groupInfo.troopuin.toLong(), - groupUin = groupInfo.troopcode.toLong(), - groupName = groupInfo.troopname ?: groupInfo.newTroopName - ?: groupInfo.oldTroopName, - groupRemark = groupInfo.troopRemark, - adminList = GroupSvc.getAdminList(groupInfo.troopuin.toLong(), true), - classText = groupInfo.mGroupClassExtText, - isFrozen = groupInfo.mIsFreezed != 0, - maxMember = groupInfo.wMemberMax, - memNum = groupInfo.wMemberNum, - memCount = groupInfo.wMemberNum, - maxNum = groupInfo.wMemberMax - ) - ) - } - } - return ok(troopList, echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopMemberInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopMemberInfo.kt deleted file mode 100644 index 787c46e..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopMemberInfo.kt +++ /dev/null @@ -1,67 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.mobileqq.data.Card -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.remote.service.data.SimpleTroopMemberInfo -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_member_info") -internal object GetTroopMemberInfo : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val userId = session.getLong("user_id") - val groupId = session.getLong("group_id") - val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", false)) - - return invoke(groupId, userId, refresh, session.echo) - } - - suspend operator fun invoke( - groupId: Long, - userId: Long, - refresh: Boolean, - echo: JsonElement = EmptyJsonString - ): String { - val info = GroupSvc.getTroopMemberInfoByUin(groupId, userId, refresh).onFailure { - return error(it.message ?: "unknown error", echo) - }.getOrThrow() - - return ok( - SimpleTroopMemberInfo( - uin = info.memberuin.toLong(), - name = info.friendnick.ifNullOrEmpty(info.autoremark) ?: "", - showName = info.troopnick.ifNullOrEmpty(info.troopColorNick), - cardName = info.troopnick.ifNullOrEmpty(info.troopColorNick), - distance = info.distance, - honor = GroupSvc.parseHonor(info.honorList), - joinTime = info.join_time, - lastActiveTime = info.last_active_time, - uniqueName = info.mUniqueTitle, - groupId = groupId, - nick = info.friendnick.ifNullOrEmpty(info.autoremark) ?: "", - sex = when (info.sex.toShort()) { - Card.FEMALE -> "female" - Card.MALE -> "male" - else -> "unknown" - }, - area = info.alias ?: "", - lastSentTime = info.last_active_time, - level = info.level, - role = GroupSvc.getMemberRole(groupId, userId), - unfriendly = false, - title = info.mUniqueTitle ?: "", - titleExpireTime = info.mUniqueTitleExpire, - cardChangeable = GroupSvc.isAdmin(groupId), - age = info.age.toInt(), - shutUpTimestamp = 0L - ), echo - ) - } - - override val requiredParams: Array = arrayOf("user_id", "group_id") -} - diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopMemberList.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopMemberList.kt deleted file mode 100644 index 21d8fa0..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetTroopMemberList.kt +++ /dev/null @@ -1,79 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.mobileqq.data.Card -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.remote.service.data.SimpleTroopMemberInfo -import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_group_member_list") -internal object GetTroopMemberList : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", false)) - return invoke(groupId, refresh, session.echo) - } - - suspend operator fun invoke( - groupId: Long, - refresh: Boolean, - echo: JsonElement = EmptyJsonString - ): String { - val memberList = GroupSvc.getGroupMemberList(groupId, refresh).onFailure { - return error(it.message ?: "unknown error", echo, arrayResult = true) - }.getOrThrow() - val prohibitedMemberList = GroupSvc.getProhibitedMemberList(groupId) - .getOrDefault(arrayListOf()) - .associate { it.memberUin to it.shutuptimestap.toLong() } - return ok(arrayListOf().apply { - memberList.forEach { info -> - if (info.memberuin != "0") { - add( - SimpleTroopMemberInfo( - uin = info.memberuin.toLong(), - name = info.friendnick.ifNullOrEmpty(info.autoremark) ?: "", - showName = info.troopnick.ifNullOrEmpty(info.troopColorNick), - cardName = info.troopnick.ifNullOrEmpty(info.troopColorNick), - distance = info.distance, - honor = GroupSvc.parseHonor(info.honorList), - joinTime = info.join_time, - lastActiveTime = info.last_active_time, - uniqueName = info.mUniqueTitle, - groupId = groupId, - nick = info.friendnick.ifNullOrEmpty(info.autoremark) ?: "", - sex = when (info.sex.toShort()) { - Card.FEMALE -> "female" - Card.MALE -> "male" - else -> "unknown" - }, - area = info.alias ?: "", - lastSentTime = info.last_active_time, - level = info.level, - role = GroupSvc.getMemberRole(groupId, info.memberuin.toLong()) - /*when { - GroupSvc.getOwner(groupId) - .toString() == info.memberuin -> MemberRole.Owner - info.memberuin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin - else -> MemberRole.Member - }*/, - unfriendly = false, - title = info.mUniqueTitle ?: "", - titleExpireTime = info.mUniqueTitleExpire, - cardChangeable = GroupSvc.isAdmin(groupId), - age = 0, - shutUpTimestamp = prohibitedMemberList[info.memberuin.toLong()] ?: 0L - ) - ) - } - } - }, echo) - } - - override val requiredParams: Array = arrayOf("group_id") -} - diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetUid.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetUid.kt deleted file mode 100644 index eb2ddef..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetUid.kt +++ /dev/null @@ -1,29 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.coroutines.suspendCancellableCoroutine -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.shamrock.tools.asString -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import moe.fuqiuluo.symbols.OneBotHandler -import kotlin.coroutines.resume - -@OneBotHandler("get_uid") -internal object GetUid: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val uinList = session.getArray("uin_list").map { - it.asString.toLong() - } - - val uidMap = suspendCancellableCoroutine { continuation -> - sessionService.uixConvertService.getUid(uinList.toHashSet()) { - continuation.resume(it) - } - } - return resultToString(true, Status.Ok, uidMap, echo = session.echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetUinByUid.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetUinByUid.kt deleted file mode 100644 index 5bdaf3b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetUinByUid.kt +++ /dev/null @@ -1,28 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.coroutines.suspendCancellableCoroutine -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.shamrock.tools.asString -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import moe.fuqiuluo.symbols.OneBotHandler -import kotlin.coroutines.resume - -@OneBotHandler("get_uin_by_uid") -internal object GetUinByUid: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val uidList = session.getArray("uid_list").map { - it.asString - } - val uinMap = suspendCancellableCoroutine { continuation -> - sessionService.uixConvertService.getUin(uidList.toHashSet()) { - continuation.resume(it) - } - } - return resultToString(true, Status.Ok, uinMap, echo = session.echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetVersionInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetVersionInfo.kt deleted file mode 100644 index 6c264db..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetVersionInfo.kt +++ /dev/null @@ -1,30 +0,0 @@ -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.remote.service.data.VersionInfo -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.tools.ShamrockVersion -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_version_info", ["get_version"]) -internal object GetVersionInfo : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return invoke(session.echo) - } - - operator fun invoke(echo: JsonElement = EmptyJsonString): String { - return ok( - VersionInfo( - appFullName = "Shamrock v$ShamrockVersion", - appName = "Shamrock", - appVersion = ShamrockVersion, - impl = "shamrock", - version = ShamrockVersion, - onebotVersion = "11", - ), - echo = echo - ) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetWeather.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetWeather.kt deleted file mode 100644 index 0bd1f46..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetWeather.kt +++ /dev/null @@ -1,36 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_weather") -internal object GetWeather: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - session.getIntOrNull("code")?.let { - return invoke(it, session.echo) - } - session.getString("city").let { - return invoke(it, session.echo) - } - } - - suspend operator fun invoke(code: Int, echo: JsonElement = EmptyJsonString): String { - val result = WeatherSvc.fetchWeatherCard(code) - if (result.isFailure) { - return error("fetch weather failed", echo) - } - return ok(result.getOrThrow(), echo) - } - - suspend operator fun invoke(city: String, echo: JsonElement = EmptyJsonString): String { - val code = WeatherSvc.searchCity(city) - if (code.isFailure || code.getOrThrow().isEmpty()) { - return error("search city failed", echo) - } - return invoke(code.getOrThrow().first().adcode, echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetWeatherCityCode.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetWeatherCityCode.kt deleted file mode 100644 index 9353571..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetWeatherCityCode.kt +++ /dev/null @@ -1,29 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("get_weather_city_code") -internal object GetWeatherCityCode: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val city = session.getString("city") - return invoke(city, session.echo) - } - - suspend operator fun invoke(city: String, echo: JsonElement = EmptyJsonString): String { - val result = WeatherSvc.searchCity(city) - if (result.isFailure) { - return error(result.exceptionOrNull()?.message ?: "unknown error", echo) - } - - val regions = result.getOrThrow() - - return ok(regions, echo) - } - - override val requiredParams: Array = arrayOf("city") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GroupPoke.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GroupPoke.kt deleted file mode 100644 index 6c8a1d3..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GroupPoke.kt +++ /dev/null @@ -1,24 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("poke") -internal object GroupPoke: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val userId = session.getLong("user_id") - return invoke(groupId, userId, session.echo) - } - - operator fun invoke(groupId: Long, userId: Long, echo: JsonElement = EmptyJsonString): String { - GroupSvc.poke(groupId, userId) - return ok("成功", echo) - } - - override val requiredParams: Array = arrayOf("group_id", "user_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/IsBlackListUin.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/IsBlackListUin.kt deleted file mode 100644 index e43ccc9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/IsBlackListUin.kt +++ /dev/null @@ -1,41 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi -import com.tencent.mobileqq.qroute.QRoute -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -@OneBotHandler("is_blacklist_uin") -internal object IsBlackListUin: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val userId = session.getLong("user_id") - return invoke(userId, session.echo) - } - - suspend operator fun invoke(uin: Long, echo: JsonElement = EmptyJsonString): String { - val blacklistApi = QRoute.api(IProfileCardBlacklistApi::class.java) - val isBlack = withTimeoutOrNull(5000) { - suspendCoroutine { continuation -> - blacklistApi.isBlackOrBlackedUin(uin.toString()) { - continuation.resume(it) - } - } - } ?: false - return ok(data = IsBlackListUinResult(isBlack), echo) - } - - override val requiredParams: Array = arrayOf("user_id") - - @Serializable - data class IsBlackListUinResult( - @SerialName("is") val isBlack: Boolean - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/KickTroopMember.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/KickTroopMember.kt deleted file mode 100644 index 1cf2281..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/KickTroopMember.kt +++ /dev/null @@ -1,27 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_kick", ["kick_group_member"]) -internal object KickTroopMember: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val userId = session.getLong("user_id") - val kickMsg = session.getStringOrNull("kick_msg") ?: session.getStringOrNull("kick_message") ?: "" - val rejectAddRequest = session.getBooleanOrDefault("reject_add_request", false) - - return invoke(groupId, userId, rejectAddRequest, kickMsg, session.echo) - } - - operator fun invoke(groupId: Long, userId: Long, rejectAddRequest: Boolean = false, kickMsg: String, echo: JsonElement = EmptyJsonString): String { - GroupSvc.kickMember(groupId, rejectAddRequest, kickMsg, userId) - return ok("成功", echo) - } - - override val requiredParams: Array = arrayOf("group_id", "user_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/LeaveTroop.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/LeaveTroop.kt deleted file mode 100644 index 0d49d2b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/LeaveTroop.kt +++ /dev/null @@ -1,26 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("leave_group", ["set_group_leave"]) -internal object LeaveTroop: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - return invoke(groupId, session.echo) - } - - operator fun invoke(groupId: Long, echo: JsonElement = EmptyJsonString): String { - if (GroupSvc.isOwner(groupId)) { - return error("you are the owner of this group", echo) - } - GroupSvc.resignTroop(groupId) - return ok("成功", echo) - } - - override val requiredParams: Array = arrayOf("group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopMemberName.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopMemberName.kt deleted file mode 100644 index 0cf477d..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopMemberName.kt +++ /dev/null @@ -1,30 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_card") -internal object ModifyTroopMemberName: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val userId = session.getLong("user_id") - val name = session.getStringOrNull("card") ?: "" - return invoke(groupId, userId, name, session.echo) - } - - operator fun invoke(groupId: Long, userId: Long, card: String, echo: JsonElement = EmptyJsonString): String { - if (!GroupSvc.isAdmin(groupId) && userId != TicketSvc.getUin().toLong()) { - return logic("you are not admin", echo) - } - return if(GroupSvc.modifyGroupMemberCard(groupId, userId, card)) - ok("成功", echo) - else error("check if member or group exist", echo) - } - - override val requiredParams: Array = arrayOf("group_id", "user_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopName.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopName.kt deleted file mode 100644 index dd44a05..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopName.kt +++ /dev/null @@ -1,29 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_name") -internal object ModifyTroopName: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val groupName = session.getString("group_name") - - return invoke(groupId, groupName, session.echo) - } - - operator fun invoke(groupId: Long, name: String, echo: JsonElement = EmptyJsonString): String { - return if (GroupSvc.isAdmin(groupId)) { - GroupSvc.modifyTroopName(groupId, name) - ok("成功", echo) - } else { - logic("You are not the administrator of the group", echo) - } - } - - override val requiredParams: Array = arrayOf("group_id", "group_name") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopRemark.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopRemark.kt deleted file mode 100644 index c816c09..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ModifyTroopRemark.kt +++ /dev/null @@ -1,25 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_remark", ["modify_group_remark"]) -internal object ModifyTroopRemark: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val remark = session.getStringOrNull("remark") ?: "" - return invoke(groupId, remark, session.echo) - } - - operator fun invoke(groupId: Long, remark: String, echo: JsonElement = EmptyJsonString): String { - return if(GroupSvc.modifyGroupRemark(groupId, remark)) - ok("成功", echo) - else error("check if member or group exist", echo) - } - - override val requiredParams: Array = arrayOf("group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/QuickOperation.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/QuickOperation.kt deleted file mode 100644 index bbda48f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/QuickOperation.kt +++ /dev/null @@ -1,154 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgRecord -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -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.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.asString -import moe.fuqiuluo.shamrock.tools.json -import moe.fuqiuluo.shamrock.tools.jsonArray -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler(".handle_quick_operation_async") -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 - val atSender = operation["at_sender"].asBooleanOrNull ?: false - val autoReply = operation["auto_reply"].asBooleanOrNull ?: true - val message = operation["reply"] - if (message is JsonPrimitive) { - quicklyReply( - record, - if (autoEscape == true) - listOf( - mapOf( - "type" to "text", - "data" to mapOf( - "text" to message.asString - ) - ) - ).json - else MessageHelper.decodeCQCode(message.asString), - msgHash, - atSender, - autoReply - ) - } else if (message is JsonArray) { - quicklyReply( - record, - message, - msgHash, - atSender, - autoReply - ) - } - } - - if (MsgConstant.KCHATTYPEGROUP == record.chatType) { - if (operation["delete"].asBooleanOrNull == true) { - val duration = operation["delay"].asIntOrNull - if (duration != null) { - GlobalScope.launch { - delay(duration.toLong()) - MsgSvc.recallMsg(msgHash) - } - } else { - MsgSvc.recallMsg(msgHash) - } - } - if (operation["kick"].asBooleanOrNull == true) { - GroupSvc.kickMember(record.peerUin, false, "", record.senderUin) - } - if (operation["ban"].asBooleanOrNull == true) { - val banTime = operation["ban_duration"].asIntOrNull ?: (30 * 60) - if (banTime <= 0) return logic("禁言时间必须大于0", session.echo) - GroupSvc.banMember(record.peerUin, record.senderUin, banTime) - } - } - - - return ok("操作成功", session.echo) - } - - override val requiredParams: Array = arrayOf("context", "operation", "self_id") - - suspend fun quicklyReply( - record: MsgRecord, - message: JsonArray, - msgHash: Int, - atSender: Boolean, - autoReply: Boolean - ) { - val messageList = mutableListOf() - 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, retryCnt = 3) - } - 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), retryCnt = 3) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RenameGroupFolder.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RenameGroupFolder.kt deleted file mode 100644 index 2f902d1..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RenameGroupFolder.kt +++ /dev/null @@ -1,27 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.FileSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("rename_group_folder") -internal object RenameGroupFolder: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val folderId = session.getString("folder_id") - val name = session.getString("name") - return invoke(groupId, folderId, name, session.echo) - } - - suspend operator fun invoke(groupId: Long, folderId: String, name: String, echo: JsonElement = EmptyJsonString): String { - if (!FileSvc.renameFolder(groupId, folderId, name)) { - return error("rename folder failed", echo = echo) - } - return ok("success", echo = echo) - } - - override val requiredParams: Array = arrayOf("group_id", "folder_id", "name") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RequestUploadGroupImage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RequestUploadGroupImage.kt deleted file mode 100644 index 0952a6a..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RequestUploadGroupImage.kt +++ /dev/null @@ -1,29 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("request_upload_group_image") -internal object RequestUploadGroupImage: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val md5 = session.getString("md5").uppercase() - val fileSize = session.getLong("file_size") - val width = session.getInt("width") - val height = session.getInt("height") - val groupId = session.getString("group_id") - NtV2RichMediaSvc.requestUploadGroupPic( - groupId.toULong(), - md5, - fileSize.toULong(), - width.toUInt(), - height.toUInt() - ).onSuccess { - return ok(it, session.echo) - }.onFailure { - return error(it.message ?: it.toString(), session.echo) - } - return logic("request_upload_group_image failed", session.echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RestartMe.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RestartMe.kt deleted file mode 100644 index 500bcfa..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/RestartMe.kt +++ /dev/null @@ -1,18 +0,0 @@ -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.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("restart_me") -internal object RestartMe: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - return invoke(2000, session.echo) - } - - operator fun invoke(duration: Int, echo: JsonElement = EmptyJsonString): String { - return ok("不支持", echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ScanQRCode.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ScanQRCode.kt deleted file mode 100644 index 54936c9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/ScanQRCode.kt +++ /dev/null @@ -1,18 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import android.util.Base64 -import com.tencent.mobileqq.qroute.QRoute -import com.tencent.mobileqq.qrscan.api.IQRCodeApi -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("scan_qrcode") -internal object ScanQRCode: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val qrcode = QRoute.api(IQRCodeApi::class.java) - val picBytes = Base64.decode(session.getString("pic"), Base64.DEFAULT) - qrcode.scanImage(picBytes, 0, picBytes.size) - return qrcode.version - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt deleted file mode 100644 index fd7dcd4..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt +++ /dev/null @@ -1,83 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import kotlinx.serialization.json.* -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.qqinterface.servlet.msg.toJson -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.helper.ParamsException -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.SendForwardMessageResult -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_forward_msg", ["send_forward_message"]) -internal object SendForwardMessage : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type") - try { - val chatType = detailType?.let { - MessageHelper.obtainMessageTypeByDetailType(it) - } ?: run { - if (session.has("user_id")) { - 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, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id") - ?: return noParam("user_id", session.echo) - else -> error("unknown chat type: $chatType") - } - val fromId = session.getStringOrNull("group_id") - val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 - return if (session.isArray("messages")) { - val messages = session.getArray("messages") - invoke(chatType, peerId, fromId ?: peerId, messages, retryCnt, session.echo) - } else { - logic("未知格式合并转发消息", session.echo) - } - } catch (e: ParamsException) { - return noParam(e.message!!, session.echo) - } catch (e: Throwable) { - return logic(e.message ?: e.toString(), session.echo) - } - } - - suspend operator fun invoke( - chatType: Int, - peerId: String, - fromId: String = peerId, - messages: JsonArray, - retryCnt: Int, - echo: JsonElement = EmptyJsonString - ): String { - kotlin.runCatching { - val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt).onFailure { - return error(it.message ?: it.stackTraceToString(), echo) - }.getOrThrow() - val result = MsgSvc.sendToAio(chatType, peerId, listOf(message).toJson(), fromId, retryCnt).onFailure { - return error(it.message ?: it.stackTraceToString(), echo) - }.getOrThrow() - return ok(SendForwardMessageResult( - msgId = result.msgHashId, - resId = message.data["id"] as String - ), echo = echo) - }.onFailure { - return error("合并转发消息失败: $it", echo) - } - return logic("合并转发消息失败(unknown error)", echo) - } - - override val requiredParams: Array = arrayOf("messages") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt deleted file mode 100644 index c15996a..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt +++ /dev/null @@ -1,28 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers; - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_group_forward_msg", ["send_group_forward_message"]) -internal object SendGroupForwardMessage: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 - return if (session.isArray("messages")) { - val messages = session.getArray("messages") - SendForwardMessage( - MsgConstant.KCHATTYPEGROUP, - groupId.toString(), - messages = messages, - retryCnt = retryCnt, - echo = session.echo - ) - } else { - logic("未知格式合并转发消息", session.echo) - } - } - - override val requiredParams: Array = arrayOf("messages", "group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupMessage.kt deleted file mode 100644 index eaae6e9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupMessage.kt +++ /dev/null @@ -1,36 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import moe.fuqiuluo.shamrock.helper.ParamsException -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.jsonArray -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_group_message", ["send_group_msg"]) -internal object SendGroupMessage: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - try{ - val groupId = session.getLong("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.toString(), message, autoEscape, echo = session.echo, retryCnt = retryCnt ?: 5, recallDuration = recallDuration) - } else if (session.isObject("message")) { - val message = session.getObject("message") - SendMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), listOf( message ).jsonArray, session.echo, retryCnt = retryCnt ?: 5, recallDuration = recallDuration) - } else { - val message = session.getArray("message") - SendMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), message, session.echo, retryCnt = retryCnt ?: 5, recallDuration = recallDuration) - } - } catch (e: ParamsException) { - return noParam(e.message!!, session.echo) - } catch (e: Throwable) { - return logic(e.message ?: e.toString(), session.echo) - } - } - - override val requiredParams: Array = arrayOf("message", "group_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupNotice.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupNotice.kt deleted file mode 100644 index 5881eb9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupNotice.kt +++ /dev/null @@ -1,38 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_group_notice", ["send_group_announcement"]) -internal object SendGroupNotice: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val text = session.getString("content") - val image = session.getStringOrNull("image") - return invoke(groupId, text, image, session.echo) - } - - suspend operator fun invoke(groupId: Long, text: String, image: String?, echo: JsonElement = EmptyJsonString): String { - val groupAnnouncementMessageImage = if (image != null) { - GroupSvc.uploadImageTroopNotice(image).onFailure { - LogCenter.log("上传群公告图片失败:${it.message}", Level.WARN) - }.getOrNull() - } else null - val announcements = GroupSvc.addQunNotice(groupId, text, groupAnnouncementMessageImage) - if (announcements.isSuccess) { - return ok(announcements.getOrNull(), echo) - } - return logic(announcements.exceptionOrNull()?.message ?: "", echo) - - } - - override val requiredParams: Array = arrayOf("group_id", "content") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupSign.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupSign.kt deleted file mode 100644 index 70d28dc..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupSign.kt +++ /dev/null @@ -1,35 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.qqinterface.servlet.structures.GuildInfo -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_group_sign") -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(Message(message = ret.getOrNull() ?: ""), echo, "成功") - } else { - logic(ret.exceptionOrNull()?.message ?: "", echo) - } - } - - override val requiredParams: Array = arrayOf("group_id") - - @Serializable - data class Message( - @SerialName("message") var message: String = "" - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGuildMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGuildMessage.kt deleted file mode 100644 index 631f33c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGuildMessage.kt +++ /dev/null @@ -1,112 +0,0 @@ -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 kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.jsonArray -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -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.data.MessageResult -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.tools.json -import moe.fuqiuluo.shamrock.tools.jsonArray -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_guild_message", ["send_guild_msg", "send_guild_channel_msg"]) -internal object SendGuildMessage: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id").toULong() - val channelId = session.getString("channel_id").toULong() - val retryCnt = session.getIntOrNull("retry_cnt") ?: 3 - val recallDuration = session.getLongOrNull("recall_duration") - return if (session.isString("message")) { - val autoEscape = session.getBooleanOrDefault("auto_escape", false) - val message = session.getString("message") - return invoke(guildId, channelId, message, autoEscape, retryCnt, recallDuration, echo = session.echo) - } else if (session.isArray("message")) { - val message = session.getArray("message") - return invoke(guildId, channelId, message, echo = session.echo, retryCnt = retryCnt, recallDuration = recallDuration) - } else { - val message = session.getObject("message") - invoke(guildId, channelId, listOf(message).jsonArray, session.echo, retryCnt, recallDuration = recallDuration) - } - } - - suspend operator fun invoke( - guildId: ULong, - channelId: ULong, - message: String, - autoEscape: Boolean, - retryCnt: Int, - recallDuration: Long?, - echo: JsonElement = EmptyJsonString - ): String { - val result = if (autoEscape) { - MsgSvc.sendToAio(MsgConstant.KCHATTYPEGUILD, guildId.toString(), listOf( - mapOf( - "type" to "text", - "data" to mapOf( - "text" to message - ) - ) - ).json, fromId = channelId.toString(), retryCnt) - } else { - val msg = MessageHelper.decodeCQCode(message) - if (msg.isEmpty()) { - LogCenter.log("CQ码不合法", Level.WARN) - return logic("CQCode is illegal", echo) - } else { - MsgSvc.sendToAio(MsgConstant.KCHATTYPEGUILD, guildId.toString(), msg, fromId = channelId.toString(), retryCnt) - } - } - if (result.isFailure) { - return logic(result.exceptionOrNull()?.message ?: "", echo) - } - val sendMsgResult = result.getOrThrow() - if (sendMsgResult.msgHashId <= 0) { - return logic("send message failed", echo = echo) - } - recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) } - return ok( - MessageResult( - msgId = sendMsgResult.msgHashId, - time = (sendMsgResult.msgTime * 0.001).toLong() - ), echo = echo) - } - - suspend operator fun invoke( - guildId: ULong, channelId: ULong, message: JsonArray, echo: JsonElement = EmptyJsonString, retryCnt: Int, recallDuration: Long?, - ): String { - val result = MsgSvc.sendToAio(MsgConstant.KCHATTYPEGUILD, guildId.toString(), message, fromId = channelId.toString(), retryCnt) - if (result.isFailure) { - return logic(result.exceptionOrNull()?.message ?: "", echo) - } - val sendMsgResult = result.getOrThrow() - if (sendMsgResult.msgHashId <= 0) { - return logic("send message failed", echo = echo) - } - recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) } - return ok(MessageResult( - msgId = sendMsgResult.msgHashId, - time = (sendMsgResult.msgTime * 0.001).toLong() - ), echo) - } - - override val requiredParams: Array = arrayOf("guild_id", "channel_id", "message") - - private fun autoRecall(msgHash: Int, duration: Long) { - GlobalScope.launch(Dispatchers.Default) { - delay(duration) - MsgSvc.recallMsg(msgHash) - } - } - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendLike.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendLike.kt deleted file mode 100644 index b9f413b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendLike.kt +++ /dev/null @@ -1,29 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.VisitorSvc -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.errMsg -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_like") -internal object SendLike: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val times = session.getInt("times") - val uin = session.getLong("user_id") - return invoke(uin, times, session.echo) - } - - suspend operator fun invoke(uin: Long, cnt: Int, echo: JsonElement = EmptyJsonString): String { - val result = VisitorSvc.vote(uin, cnt) - return if(result.isSuccess) { - ok("成功", echo) - } else { - logic(result.errMsg(), echo) - } - } - - override val requiredParams: Array = arrayOf("times", "user_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt deleted file mode 100644 index 6c9ae7c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt +++ /dev/null @@ -1,187 +0,0 @@ -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 -import moe.fuqiuluo.shamrock.helper.ParamsException -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.remote.service.data.MessageResult -import moe.fuqiuluo.shamrock.tools.json -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.tools.jsonArray -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_msg", ["send_message"]) -internal object SendMessage : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type") - try { - val chatType = detailType?.let { - MessageHelper.obtainMessageTypeByDetailType(it) - } ?: run { - if (session.has("user_id")) { - 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.getLongOrNull("group_id") ?: return noParam( - "group_id", - session.echo - ) - - MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id") - ?: return noParam("user_id", session.echo) - - else -> error("unknown chat type: $chatType") - }.toString() - val fromId = when (chatType) { - MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id") - ?: return noParam("group_id", session.echo) - - MsgConstant.KCHATTYPEC2C -> session.getLongOrNull("user_id") ?: return noParam("user_id", session.echo) - else -> error("unknown chat type: $chatType") - }.toString() - val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 - 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, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } else if (session.isArray("message")) { - val message = session.getArray("message") - invoke( - chatType, - peerId, - message, - session.echo, - fromId = fromId, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } else { - val message = session.getObject("message") - invoke( - chatType, - peerId, - listOf(message).jsonArray, - session.echo, - fromId = fromId, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } - } catch (e: ParamsException) { - return noParam(e.message!!, session.echo) - } catch (e: Throwable) { - return logic(e.message ?: e.toString(), session.echo) - } - } - - // 发送文本格式/CQ码类型消息 - suspend operator fun invoke( - chatType: Int, - peerId: String, - message: String, - autoEscape: Boolean, - fromId: String = peerId, - retryCnt: Int, - recallDuration: Long?, - echo: JsonElement = EmptyJsonString - ): String { - val result = if (autoEscape) { - MsgSvc.sendToAio( - chatType, peerId, listOf( - mapOf( - "type" to "text", - "data" to mapOf( - "text" to message - ) - ) - ).json, fromId = fromId, retryCnt - ) - } else { - val msg = MessageHelper.decodeCQCode(message) - if (msg.isEmpty()) { - LogCenter.log("CQ码不合法", Level.WARN) - return logic("CQCode is illegal", echo) - } else { - MsgSvc.sendToAio(chatType, peerId, msg, fromId = fromId, retryCnt) - } - }.getOrElse{ return logic(it.message ?: "", echo)} - if (result.msgHashId <= 0) { - return logic("send message failed", echo = echo) - } - if (recallDuration != null) { - GlobalScope.launch(Dispatchers.Default) { - delay(recallDuration) - MsgSvc.recallMsg(result.msgHashId) - } - } - return ok( - MessageResult( - msgId = result.msgHashId, - time = (result.msgTime * 0.001).toLong() - ), echo = echo - ) - } - - // 消息段格式消息 - suspend operator fun invoke( - 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, retryCnt) - .getOrElse { return logic(it.message ?: "", echo) } - if (result.msgHashId <= 0) { - return logic("send message failed", echo = echo) - } - if (recallDuration != null) { - GlobalScope.launch(Dispatchers.Default) { - delay(recallDuration) - MsgSvc.recallMsg(result.msgHashId) - } - } - return ok( - MessageResult( - msgId = result.msgHashId, - time = (result.msgTime * 0.001).toLong() - ), echo - ) - } - - override val requiredParams: Array = arrayOf("message") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMsgByResid.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMsgByResid.kt deleted file mode 100644 index 1723a16..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMsgByResid.kt +++ /dev/null @@ -1,59 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.atomicfu.atomic -import kotlinx.serialization.json.JsonElement - -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler -import protobuf.auto.toByteArray -import protobuf.message.* -import protobuf.message.element.GeneralFlags -import protobuf.message.routing.C2C -import protobuf.message.routing.Grp -import kotlin.random.Random -import kotlin.random.nextUInt - -@OneBotHandler("send_msg_by_resid", ["send_message_by_resid"]) -internal object SendMsgByResid : IActionHandler() { - private val msgSeq = atomic(1000) - - override suspend fun internalHandle(session: ActionSession): String { - val resId = session.getString("res_id") - val peerId = session.getString("peer_id") - val messageType = session.getString("message_type") - return invoke(peerId, resId, messageType, session.echo) - } - - suspend operator fun invoke(peerId: String, resId: String, messageType: String, echo: JsonElement = EmptyJsonString): String { - val req = PbSendMsgReq( - routingHead = when (messageType) { - "group" -> RoutingHead(grp = Grp(peerId.toUInt())) - "private" -> RoutingHead(c2c = C2C(peerId.toUInt())) - else -> RoutingHead(grp = Grp(peerId.toUInt())) - }, - contentHead = ContentHead(1, 0, 0, 0), - msgBody = MsgBody( - richText = RichText( - elements = arrayListOf( - Elem( - generalFlags = GeneralFlags( - longTextFlag = 1u, - longTextResid = resId - ) - ) - ) - ) - ), - msgSeq = msgSeq.incrementAndGet().toUInt(), - msgRand = Random.nextUInt(), - msgVia = 0u - ) - BaseSvc.sendBufferAW("MessageSvc.PbSendMsg", true, req.toByteArray()) - return ok("ok", echo) - } - - override val requiredParams: Array = arrayOf("res_id", "peer_id", "message_type") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt deleted file mode 100644 index fade95b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt +++ /dev/null @@ -1,30 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers; - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_private_forward_msg", ["send_private_forward_message"]) -internal object SendPrivateForwardMessage : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val userId = session.getLong("user_id") - val groupId = session.getLongOrNull("group_id") - val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 - return if (session.isArray("messages")) { - val messages = session.getArray("messages") - SendForwardMessage( - MsgConstant.KCHATTYPEC2C, - userId.toString(), - groupId?.toString() ?: userId.toString(), - messages, - retryCnt, - echo = session.echo - ) - } else { - logic("未知格式合并转发消息", session.echo) - } - } - - override val requiredParams: Array = arrayOf("messages", "user_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateMessage.kt deleted file mode 100644 index fcd355b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateMessage.kt +++ /dev/null @@ -1,47 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.jsonArray -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("send_private_msg", ["send_private_message", "send_friend_msg"]) -internal object SendPrivateMessage : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val userId = session.getString("user_id").let { - if (it == "self") TicketSvc.getUin() else it - } - val groupId = session.getLongOrNull("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") - SendMessage( - chatType = chatType, - peerId = userId, - message = message, - autoEscape = autoEscape, - echo = session.echo, - fromId = groupId?.toString() ?: userId, - retryCnt = retryCnt ?: 5, - recallDuration = recallDuration - ) - } else { - SendMessage( - chatType = chatType, - peerId = userId, - message = if (session.isArray("message")) session.getArray("message") else listOf(session.getObject("message")).jsonArray, - echo = session.echo, - fromId = groupId?.toString() ?: userId, - retryCnt = retryCnt ?: 5, - recallDuration = recallDuration - ) - } - } - - override val requiredParams: Array = arrayOf("message", "user_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetEssenceMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetEssenceMessage.kt deleted file mode 100644 index c1a30c5..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetEssenceMessage.kt +++ /dev/null @@ -1,34 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_essence_msg", ["set_essence_message"]) -internal object SetEssenceMessage: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val messageId = session.getInt("message_id") - return invoke(messageId, session.echo) - } - - suspend operator fun invoke(messageId: Int, echo: JsonElement = EmptyJsonString): String { - val msg = MsgSvc.getMsg(messageId).onFailure { - return logic("Obtain msg failed, please check your msg_id.", echo) - }.getOrThrow() - val (success, tip) = GroupSvc.setEssenceMessage( - groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0, - seq = msg.msgSeq, - rand = msg.msgRandom - ) - return if (success) { - ok("成功", echo) - } else { - logic(tip, echo) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetFriendAddRequest.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetFriendAddRequest.kt deleted file mode 100644 index 6e8b6a2..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetFriendAddRequest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.FriendSvc -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_friend_add_request") -internal object SetFriendAddRequest: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val flag = session.getString("flag") - val approve = session.getBooleanOrDefault("approve", true) - val remark = session.getStringOrNull("remark") - val notSeen = session.getBooleanOrDefault("notSeen", false) - return invoke(flag, approve, remark, notSeen, session.echo) - } - - suspend operator fun invoke(flag: String, approve: Boolean? = true, remark: String? = "", notSeen: Boolean? = false, echo: JsonElement = EmptyJsonString): String { - val flags = flag.split(";") - var ts = flags[0].toLong() -// val src = flags[1].toInt() -// val subSrc = flags[2].toInt() - val applier = flags[3].toLong() - if (ts.toString().length < 13) { - // time but not seq, query seq again - val reqs = FriendSvc.requestFriendSystemMsgNew(20, 0, 0, 1) - val req = reqs?.firstOrNull { - it.msg_time.get() == ts - } - // 好友请求seq貌似就是time*1000,查不到直接*1000 - ts = req?.msg_seq?.get() ?: (ts * 1000) - } - return try { - FriendSvc.requestFriendRequest(ts, applier, remark ?: "", approve, notSeen) - ok("成功", echo) - } catch (err: Throwable) { - err.printStackTrace() - error("失败:${err.message}", echo) - } - } - - override val requiredParams: Array = arrayOf("flag") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupAddRequest.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupAddRequest.kt deleted file mode 100644 index 1553526..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupAddRequest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_add_request") -internal object SetGroupAddRequest: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val flag = session.getString("flag") - val approve = session.getBooleanOrDefault("approve", true) - val remark = session.getStringOrNull("reason") - val notSeen = session.getBooleanOrDefault("not_seen", false) - val subType = session.getString("sub_type") - return invoke(flag, approve, subType, remark, notSeen, session.echo) - } - - suspend operator fun invoke(flag: String, approve: Boolean? = true, subType: String, remark: String? = "", notSeen: Boolean? = false, echo: JsonElement = EmptyJsonString): String { - val flags = flag.split(";") - var ts = flags[0].toLong() - try { - if (ts.toString().length < 13) { - // time but not seq, query seq again - var reqs = GroupSvc.requestGroupSystemMsgNew(20, 1) - val riskReqs = GroupSvc.requestGroupSystemMsgNew(20, 2) - reqs = reqs + riskReqs - val req = reqs.firstOrNull { - it.msg_time.get() == ts - } - ts = req?.msg_seq?.get() ?: return error("失败:未找到该请求", echo) - } - } catch (err: Throwable) { - LogCenter.log(err.stackTraceToString(), Level.WARN) - return error("查找请求失败:${err.message}", echo) - } - val groupCode = flags[1].toLong() - val uin = flags[2].toLong() - return try { - val result = GroupSvc.requestGroupRequest(ts, uin, groupCode, remark ?: "", approve, notSeen, subType) - if (result.isSuccess) { - ok(result.getOrNull(), echo) - } else { - logic(result.exceptionOrNull()?.message ?: "", echo) - } - } catch (err: Throwable) { - err.printStackTrace() - error("失败:${err.message}", echo) - } - } - - override val requiredParams: Array = arrayOf("flag", "sub_type") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupAdmin.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupAdmin.kt deleted file mode 100644 index d6f205d..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupAdmin.kt +++ /dev/null @@ -1,26 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_admin") -internal object SetGroupAdmin: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val userId = session.getLong("user_id") - val enable = session.getBoolean("enable") - return invoke(groupId, userId, enable, session.echo) - } - - operator fun invoke(groupId: Long, userId: Long, enable: Boolean, echo: JsonElement = EmptyJsonString): String { - if (!GroupSvc.isOwner(groupId)) { - return logic("you are not owner", echo) - } - GroupSvc.setGroupAdmin(groupId, userId, enable) - return ok("成功", echo) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupCommentFace.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupCommentFace.kt deleted file mode 100644 index da9b32c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupCommentFace.kt +++ /dev/null @@ -1,30 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.ChatSvc -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_comment_face") -internal object SetGroupCommentFace: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val msgId = session.getIntOrNull("msg_id") ?: session.getInt("message_id") - val faceId = session.getInt("face_id") - val isSet = session.getBooleanOrDefault("is_set", true) - return invoke(groupId, msgId, faceId, isSet, session.echo) - } - - operator fun invoke(groupId: Long, msgHash: Int, faceIndex: Int, isSet: Boolean, echo: JsonElement = EmptyJsonString): String { - val mapping = MessageHelper.getMsgMappingByHash(msgHash) - ?: return error("failed to locate message", echo = echo) - ChatSvc.setGroupMessageCommentFace(groupId, mapping.msgSeq.toULong(), faceIndex.toString(), isSet) - return ok("success", echo = echo) - } - - override val requiredParams: Array = arrayOf("group_id", "face_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupUnique.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupUnique.kt deleted file mode 100644 index b533eb2..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupUnique.kt +++ /dev/null @@ -1,28 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_special_title") -internal object SetGroupUnique: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val userId = session.getLong("user_id") - val unique = session.getString("special_title") - return invoke(groupId, userId, unique, session.echo) - } - - suspend operator fun invoke(groupId: Long, userId: Long, unique: String, echo: JsonElement = EmptyJsonString): String { - if (!GroupSvc.isOwner(groupId)) { - return error("you are not owner", echo) - } - GroupSvc.setGroupUniqueTitle(groupId, userId, unique) - return ok("成功", echo) - } - - override val requiredParams: Array = arrayOf("group_id", "user_id", "special_title") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupWholeBan.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupWholeBan.kt deleted file mode 100644 index ff18764..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGroupWholeBan.kt +++ /dev/null @@ -1,24 +0,0 @@ -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 -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_group_whole_ban") -internal object SetGroupWholeBan: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val enable = session.getBoolean("enable") - return invoke(groupId, enable, session.echo) - } - - operator fun invoke(groupId: Long, enable: Boolean, echo: JsonElement = EmptyJsonString): String { - GroupSvc.setGroupWholeBan(groupId, enable) - return ok("成功", echo) - } - - override val requiredParams: Array = arrayOf() -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGuildMemberRole.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGuildMemberRole.kt deleted file mode 100644 index 178c5f9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetGuildMemberRole.kt +++ /dev/null @@ -1,42 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -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.asString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("set_guild_member_role") -internal object SetGuildMemberRole: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id").toULong() - val role = session.getString("role_id").toULong() - val set = session.getBooleanOrDefault("set", false) - return if (session.has("user_id")) { - val userId = session.getLong("user_id").toULong() - invoke(guildId, userId, role, set, echo = session.echo) - } else if (session.isArray("users")) { - invoke(guildId, session.getArray("users").map { - it.asString.toULong() - }, role, set, echo = session.echo) - } else { - logic("missing user_id or users", echo = session.echo) - } - } - - operator fun invoke(guildId: ULong, users: List, roleId: ULong, set: Boolean, echo: JsonElement = EmptyJsonString): String { - users.forEach { - GProSvc.setMemberRole(guildId, it, roleId, set) - } - return ok("success", echo = echo) - } - - operator fun invoke(guildId: ULong, user: ULong, roleId: ULong, set: Boolean, echo: JsonElement = EmptyJsonString): String { - GProSvc.setMemberRole(guildId, user, roleId, set) - return ok("success", echo = echo) - } - - override val requiredParams: Array = arrayOf("guild_id", "role_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetModelShow.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetModelShow.kt deleted file mode 100644 index beded26..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetModelShow.kt +++ /dev/null @@ -1,35 +0,0 @@ -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.qqinterface.servlet.CardSvc -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("_set_model_show") -internal object SetModelShow : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val model = session.getString("model") - val manu = session.getStringOrNull("manu") ?: session.getString("model_show") - val modelShow = session.getStringOrNull("modelshow") ?: "Android" - val imei = session.getStringOrNull("imei") ?: PlatformUtils.getAndroidID() - val show = session.getBooleanOrDefault("show", true) - return invoke(model, manu, modelShow, imei, show, session.echo) - } - - suspend operator fun invoke( - model: String, - manu: String, - modelShow: String, - imei: String, - show: Boolean, - echo: JsonElement = EmptyJsonString - ): String { - CardSvc.setModelShow(model, manu, modelShow, imei, show) - return ok("成功", echo = echo) - } - - override val requiredParams: Array = arrayOf("model") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetProfileCard.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetProfileCard.kt deleted file mode 100644 index 162a1be..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SetProfileCard.kt +++ /dev/null @@ -1,45 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import android.os.Bundle -import moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileProtocolConst -import com.tencent.mobileqq.profilecard.api.IProfileProtocolService -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.symbols.OneBotHandler -import mqq.app.MobileQQ - -@OneBotHandler("set_qq_profile") -internal object SetProfileCard: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val nickName = session.getString("nickname") - val company = session.getString("company") - val email = session.getString("email") - val college = session.getString("college") - val personalNote = session.getString("personal_note") - - val birthday = session.getIntOrNull("birthday") - val age = session.getIntOrNull("age") - - val bundle = Bundle() - val service = AppRuntimeFetcher.appRuntime - .getRuntimeService(IProfileProtocolService::class.java, "all") - bundle.putString(ProfileProtocolConst.KEY_NICK, nickName) - bundle.putString(ProfileProtocolConst.KEY_COMPANY, company) - bundle.putString(ProfileProtocolConst.KEY_EMAIL, email) - bundle.putString(ProfileProtocolConst.KEY_COLLEGE, college) - bundle.putString(ProfileProtocolConst.KEY_PERSONAL_NOTE, personalNote) - - if (birthday != null) { - bundle.putInt(ProfileProtocolConst.KEY_BIRTHDAY, birthday.toInt()) - } - if (age != null) { - bundle.putInt(ProfileProtocolConst.KEY_AGE, age.toInt()) - } - - service.setProfileDetail(bundle) - return ok("设置成功", session.echo) - } - - override val requiredParams: Array = arrayOf("nickname", "company", "email", "college", "personal_note") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SwitchAccount.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SwitchAccount.kt deleted file mode 100644 index 2a98cbc..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SwitchAccount.kt +++ /dev/null @@ -1,35 +0,0 @@ -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.EmptyJsonString -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.symbols.OneBotHandler -import mqq.app.MobileQQ - -@OneBotHandler("switch_account") -internal object SwitchAccount: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val userId = session.getLong("user_id") - return invoke(userId, session.echo) - } - - operator fun invoke( - userId: Long, - echo: JsonElement = EmptyJsonString - ): String { - val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == userId.toString() } - ?: return error("账号不存在", echo) - val runtime = AppRuntimeFetcher.appRuntime - val result = kotlin.runCatching { - runtime.switchAccount(account, null) - } - if (result.isFailure) { - return error(result.exceptionOrNull()?.message ?: "切换账号失败", echo) - } - return ok("切换成功", echo) - } - - override val requiredParams: Array = arrayOf("user_id") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/TestHandler.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/TestHandler.kt deleted file mode 100644 index 50b2a6b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/TestHandler.kt +++ /dev/null @@ -1,31 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.Serializable -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import de.robv.android.xposed.XposedBridge.log -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("test") -internal object TestHandler: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - kotlin.runCatching { - val msg = StringBuffer() - return resultToString( - isOk = true, - code = Status.Ok, - data = Test(System.currentTimeMillis()), - msg = msg.toString(), - echo = session.echo - ) - }.onFailure { - log(it) - } - return "error" - } - - @Serializable - data class Test(val time: Long) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UpdateGuildRole.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UpdateGuildRole.kt deleted file mode 100644 index 85940d3..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UpdateGuildRole.kt +++ /dev/null @@ -1,28 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.GProSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("update_guild_role") -internal object UpdateGuildRole: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val guildId = session.getString("guild_id").toULong() - val roleId = session.getString("role_id").toULong() - val name = session.getString("name") - val color = session.getLong("color") - return invoke(guildId, roleId, name, color, session.echo) - } - - suspend operator fun invoke(guildId: ULong, roleId: ULong, name: String, color: Long, echo: JsonElement = EmptyJsonString): String { - val result = GProSvc.updateGuildRole(guildId, roleId, name, color).onFailure { - return error(it.message ?: "Unknown error", echo) - } - return ok("success", echo = echo) - } - - override val requiredParams: Array = arrayOf("role_id", "guild_id", "name", "color") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadFileToShamrock.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadFileToShamrock.kt deleted file mode 100644 index a89b073..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadFileToShamrock.kt +++ /dev/null @@ -1,59 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import android.util.Base64 -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -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.hex2ByteArray -import moe.fuqiuluo.shamrock.tools.toHexString -import moe.fuqiuluo.shamrock.utils.FileUtils -import moe.fuqiuluo.symbols.OneBotHandler -import java.io.RandomAccessFile - -@OneBotHandler("upload_file_to_shamrock") -internal object UploadFileToShamrock: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val md5 = session.getString("md5").hex2ByteArray() - val offset = session.getStringOrNull("offset")?.toULong() ?: 0uL - val chunk = Base64.decode(session.getString("chunk"), Base64.DEFAULT) - val fileSize = session.getStringOrNull("file_size")?.toULong() ?: chunk.size.toULong() - return invoke(md5, fileSize, offset, chunk, session.echo) - } - - operator fun invoke( - md5: ByteArray, - fileSize: ULong, - offset: ULong, - chunk: ByteArray, - echo: JsonElement = EmptyJsonString - ): String { - val file = FileUtils.getFileByMd5(md5.toHexString()) - runCatching { - if (!file.exists()) { - file.createNewFile() - } - val rd = RandomAccessFile(file, "rw") - rd.setLength(fileSize.toLong()) - rd.seek(offset.toLong()) - rd.write(chunk, 0, chunk.size) - rd.close() - }.onFailure { - return error(it.message ?: it.toString(), echo) - } - return ok(UploadFileResult( - fileSize = fileSize, - isFinish = fileSize <= offset + chunk.size.toULong(), - filePath = file.absolutePath - ), echo = echo) - } - - @Serializable - data class UploadFileResult( - @SerialName("file_size") val fileSize: ULong, - @SerialName("finish") val isFinish: Boolean, - @SerialName("path") val filePath: String - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadGroupFile.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadGroupFile.kt deleted file mode 100644 index 5ee6010..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadGroupFile.kt +++ /dev/null @@ -1,163 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.media.MediaMetadataRetriever -import com.tencent.mobileqq.qroute.QRoute -import com.tencent.qqnt.kernel.nativeinterface.FileElement -import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgElement -import com.tencent.qqnt.msg.api.IMsgService -import com.tencent.qqnt.msg.api.IMsgUtilApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.transfile.RichMediaUploadHandler -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.helper.TransfileHelper -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.utils.FileUtils -import moe.fuqiuluo.shamrock.utils.MD5 -import moe.fuqiuluo.symbols.OneBotHandler -import java.io.File -import java.io.FileOutputStream -import kotlin.coroutines.resume - -@OneBotHandler("upload_group_file") -internal object UploadGroupFile : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val file = session.getString("file") - val name = session.getString("name") - .replace("/", "_") - .replace("\\", "_") - .replace("\n", "_") - .replace("\t", "_") - .replace("\r", "_") - return invoke(groupId, file, name, session.echo) - } - - suspend operator fun invoke( - groupId: Long, - file: String, - name: String, - echo: JsonElement = EmptyJsonString - ): String { - var srcFile = File(file) - if (!srcFile.exists()) { - srcFile = FileUtils.getFile(file) - } - if (!srcFile.exists()) { - srcFile = file.let { - val md5 = it.replace( - regex = "[{}\\-]".toRegex(), - replacement = "" - ).split(".")[0].lowercase() - if (md5.length == 32) { - FileUtils.getFileByMd5(it) - } else { - FileUtils.parseAndSave(it) - } - } - } - - if (!srcFile.exists()) { - return badParam("文件不存在", echo) - } - - val fileElement = FileElement() - fileElement.fileMd5 = "" - fileElement.fileName = name - fileElement.filePath = srcFile.absolutePath - fileElement.fileSize = srcFile.length() - fileElement.folderId = srcFile.parent ?: "" - fileElement.picWidth = 0 - fileElement.picHeight = 0 - fileElement.videoDuration = 0 - fileElement.picThumbPath = HashMap() - fileElement.expireTime = 0L - fileElement.fileSha = "" - fileElement.fileSha3 = "" - fileElement.file10MMd5 = "" - when (TransfileHelper.getExtensionId(name)) { - 0 -> { - val wh = QRoute.api(IMsgUtilApi::class.java) - .getPicSizeByPath(srcFile.absolutePath) - fileElement.picWidth = wh.first - fileElement.picHeight = wh.second - fileElement.picThumbPath[750] = srcFile.absolutePath - } - 2 -> { - val thumbPic = FileUtils.getFileByMd5(MD5.genFileMd5Hex(srcFile.absolutePath)) - withContext(Dispatchers.IO) { - val fileOutputStream = FileOutputStream(thumbPic) - val retriever = MediaMetadataRetriever() - retriever.setDataSource(fileElement.filePath) - retriever.frameAtTime?.compress(Bitmap.CompressFormat.JPEG, 60, fileOutputStream) - fileOutputStream.flush() - fileOutputStream.close() - } - val options = BitmapFactory.Options() - BitmapFactory.decodeFile(thumbPic.absolutePath, options) - fileElement.picHeight = options.outHeight - fileElement.picWidth = options.outWidth - fileElement.picThumbPath = hashMapOf(750 to thumbPic.absolutePath) - } - } - val msgElement = MsgElement() - msgElement.elementType = MsgConstant.KELEMTYPEFILE - msgElement.fileElement = fileElement - - // 根据文件大小调整超时时间 - val msgIdPair = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEGROUP) - val info = (withTimeoutOrNull((srcFile.length() / (125 * 1024)) * 1000 + 5000) { - val msgService = QRoute.api(IMsgService::class.java) - val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId.toString()) - suspendCancellableCoroutine { - msgService.sendMsgWithMsgId( - contact, msgIdPair.qqMsgId, arrayListOf(msgElement) - ) { code, reason -> - LogCenter.log("群文件消息发送异常(code = $code, reason = $reason)") - it.resume(null) - } - RichMediaUploadHandler.registerListener(msgIdPair.qqMsgId) { - it.resume(this) - return@registerListener true - } - } - } ?: return error("上传文件失败", echo)).also { - if (it.commonFileInfo == null) { - return error(it.fileErrMsg ?: "上传文件失败", echo) - } - }.commonFileInfo - - return ok(data = FileUploadResult( - msgHash = msgIdPair.msgHashId, - bizid = info.bizType ?: 0, - md5 = info.md5, - sha = info.sha, - sha3 = info.sha3, - fileId = info.uuid - ), echo = echo) - } - - override val requiredParams: Array = arrayOf("group_id", "file", "name") - - @Serializable - data class FileUploadResult( - @SerialName("msg_id") val msgHash: Int, - @SerialName("bizid") val bizid: Int, - @SerialName("md5") val md5: String, - @SerialName("sha") val sha: String, - @SerialName("sha3") val sha3: String, - @SerialName("file_id") val fileId: String - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt deleted file mode 100644 index 2a12dc4..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt +++ /dev/null @@ -1,82 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import kotlinx.serialization.json.* -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.helper.ParamsException -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.UploadForwardMessageResult -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.symbols.OneBotHandler - -@OneBotHandler("upload_multi_message") -internal object UploadMultiMessage : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type") - try { - val chatType = detailType?.let { - MessageHelper.obtainMessageTypeByDetailType(it) - } ?: run { - if (session.has("user_id")) { - 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, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id") - ?: return noParam("user_id", session.echo) - else -> error("unknown chat type: $chatType") - } - val fromId = session.getStringOrNull("group_id") - val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 - return if (session.isArray("messages")) { - val messages = session.getArray("messages") - invoke(chatType, peerId, fromId ?: peerId, messages, retryCnt, echo = session.echo) - } else { - logic("未知格式合并转发消息", session.echo) - } - } catch (e: ParamsException) { - return noParam(e.message!!, session.echo) - } catch (e: Throwable) { - return logic(e.message ?: e.toString(), session.echo) - } - } - - suspend operator fun invoke( - chatType: Int, - peerId: String, - fromId: String = peerId, - messages: JsonArray, - retryCnt: Int, - echo: JsonElement = EmptyJsonString - ): String { - kotlin.runCatching { - MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt).getOrThrow() - }.onFailure { - return error("合并转发消息失败: ${it.stackTraceToString()}", echo) - }.onSuccess { message -> - return ok( - UploadForwardMessageResult( - resId = message.data["id"] as String, - filename = message.data["filename"] as String, - summary = message.data["summary"] as String, - desc = message.data["desc"] as String - ), echo = echo - ) - } - return logic("合并转发消息失败(unknown error)", echo) - } - - override val requiredParams: Array = arrayOf("messages") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadNtResource.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadNtResource.kt deleted file mode 100644 index 88febbb..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadNtResource.kt +++ /dev/null @@ -1,82 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.structures.CommFileInfo -import moe.fuqiuluo.qqinterface.servlet.structures.UploadResult -import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.utils.FileUtils -import moe.fuqiuluo.symbols.OneBotHandler -import kotlin.time.Duration.Companion.seconds - -@OneBotHandler("upload_nt_resource", ["upload_nt_res"]) -internal object UploadNtResource: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val pic = session.getString("file") - val chatType = when(session.getStringOrNull("message_type")) { - "group" -> MsgConstant.KCHATTYPEGROUP - "guild" -> MsgConstant.KCHATTYPEGUILD - "private" -> MsgConstant.KCHATTYPEC2C - else -> MsgConstant.KCHATTYPEGROUP - } - val fileType = when(session.getStringOrNull("file_type")) { - "file" -> MsgConstant.KELEMTYPEFILE - "image", "pic" -> MsgConstant.KELEMTYPEPIC - "video" -> MsgConstant.KELEMTYPEVIDEO - "audio", "voice", "record" -> MsgConstant.KELEMTYPEPTT - else -> MsgConstant.KELEMTYPEFILE - } - return invoke(chatType, fileType, pic, session.echo) - } - - suspend operator fun invoke( - chatType: Int, - fileType: Int, - picture: String, - echo: JsonElement = EmptyJsonString - ): String { - if (ShamrockConfig.isDev()) { - val file = picture.let { - val md5 = it.replace( - regex = "[{}\\-]".toRegex(), - replacement = "" - ).split(".")[0].lowercase() - if (md5.length == 32) { - FileUtils.getFileByMd5(it) - } else { - FileUtils.parseAndSave(it) - } - } - if (!file.exists()) { - return logic("picture file is not exists", echo) - } - NtV2RichMediaSvc.tryUploadResourceByNt( - chatType = chatType, - elementType = fileType, - resources = arrayListOf(file), - timeout = 30.seconds - ).onSuccess { - return ok(UploadResult(it.map { - CommFileInfo( - modeId = it.fileModelId, - fileName = it.fileName, - fileSize = it.fileSize, - md5 = it.md5, - uuid = it.uuid, - subId = it.subId, - sha = it.sha ?: "" - ) - }), echo) - }.onFailure { - return logic("upload failed: ${it.message ?: it.toString()}", echo) - } - } - return logic("upload failed", echo) - } - - override val requiredParams: Array = arrayOf("file") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadPrivateFile.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadPrivateFile.kt deleted file mode 100644 index ffe5b84..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadPrivateFile.kt +++ /dev/null @@ -1,154 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.action.handlers - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.media.MediaMetadataRetriever -import com.tencent.mobileqq.qroute.QRoute -import com.tencent.qqnt.kernel.nativeinterface.FileElement -import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgElement -import com.tencent.qqnt.msg.api.IMsgService -import com.tencent.qqnt.msg.api.IMsgUtilApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.helper.TransfileHelper -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.qqinterface.servlet.transfile.RichMediaUploadHandler -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.utils.FileUtils -import moe.fuqiuluo.shamrock.utils.MD5 -import moe.fuqiuluo.symbols.OneBotHandler -import java.io.File -import java.io.FileOutputStream -import kotlin.coroutines.resume - -@OneBotHandler("upload_private_file") -internal object UploadPrivateFile : IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val userId = session.getLong("user_id") - val file = session.getString("file") - val name = session.getString("name") - .replace("/", "_") - .replace("\\", "_") - .replace("\n", "_") - .replace("\t", "_") - .replace("\r", "_") - return invoke(userId, file, name, session.echo) - } - - suspend operator fun invoke( - userId: Long, - file: String, - name: String, - echo: JsonElement = EmptyJsonString - ): String { - var srcFile = File(file) - if (!srcFile.exists()) { - srcFile = FileUtils.getFile(file) - } - - if (!srcFile.exists()) { - srcFile = file.let { - val md5 = it.replace( - regex = "[{}\\-]".toRegex(), - replacement = "" - ).split(".")[0].lowercase() - if (md5.length == 32) { - FileUtils.getFileByMd5(it) - } else { - FileUtils.parseAndSave(it) - } - } - } - - if (!srcFile.exists()) { - return badParam("文件不存在", echo) - } - - val fileElement = FileElement() - fileElement.fileMd5 = "" - fileElement.fileName = name - fileElement.filePath = srcFile.absolutePath - fileElement.fileSize = srcFile.length() - fileElement.folderId = srcFile.parent ?: "" - fileElement.picWidth = 0 - fileElement.picHeight = 0 - fileElement.videoDuration = 0 - fileElement.picThumbPath = HashMap() - fileElement.expireTime = 0L - fileElement.fileSha = "" - fileElement.fileSha3 = "" - fileElement.file10MMd5 = "" - when (TransfileHelper.getExtensionId(name)) { - 0 -> { - val wh = QRoute.api(IMsgUtilApi::class.java) - .getPicSizeByPath(srcFile.absolutePath) - fileElement.picWidth = wh.first - fileElement.picHeight = wh.second - fileElement.picThumbPath[750] = srcFile.absolutePath - } - 2 -> { - val thumbPic = FileUtils.getFileByMd5(MD5.genFileMd5Hex(srcFile.absolutePath)) - withContext(Dispatchers.IO) { - val fileOutputStream = FileOutputStream(thumbPic) - val retriever = MediaMetadataRetriever() - retriever.setDataSource(fileElement.filePath) - retriever.frameAtTime?.compress(Bitmap.CompressFormat.JPEG, 60, fileOutputStream) - fileOutputStream.flush() - fileOutputStream.close() - } - val options = BitmapFactory.Options() - BitmapFactory.decodeFile(thumbPic.absolutePath, options) - fileElement.picHeight = options.outHeight - fileElement.picWidth = options.outWidth - fileElement.picThumbPath = hashMapOf(750 to thumbPic.absolutePath) - } - } - val msgElement = MsgElement() - msgElement.elementType = MsgConstant.KELEMTYPEFILE - msgElement.fileElement = fileElement - - // 根据文件大小调整超时时间 - val msgIdPair = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEC2C) - val info = (withTimeoutOrNull((srcFile.length() / (300 * 1024)) * 1000 + 5000) { - val msgService = QRoute.api(IMsgService::class.java) - val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, userId.toString()) - suspendCancellableCoroutine { - msgService.sendMsgWithMsgId( - contact, msgIdPair.qqMsgId, arrayListOf(msgElement) - ) { code, reason -> - if (code != 0) { - LogCenter.log("私聊文件消息发送异常(code = $code, reason = $reason)") - it.resume(null) - } - } - RichMediaUploadHandler.registerListener(msgIdPair.qqMsgId) { - it.resume(this) - return@registerListener true - } - } - } ?: return error("上传文件失败", echo)).also { - if (it.commonFileInfo == null) { - return error(it.fileErrMsg ?: "上传文件失败", echo) - } - }.commonFileInfo - - return ok(data = UploadGroupFile.FileUploadResult( - msgHash = msgIdPair.msgHashId, - bizid = info.bizType ?: 0, - md5 = info.md5, - sha = info.sha, - sha3 = info.sha3, - fileId = info.uuid - ), echo = echo) - } - - override val requiredParams: Array = arrayOf("user_id", "file", "name") -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/BDHWorker.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/BDHWorker.kt deleted file mode 100644 index 84d4513..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/BDHWorker.kt +++ /dev/null @@ -1,50 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import android.util.Base64 -import com.tencent.mobileqq.transfile.TransferRequest -import com.tencent.mobileqq.transfile.api.ITransFileController -import io.ktor.server.routing.Routing -import io.ktor.server.routing.post -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.tools.fetchPost -import moe.fuqiuluo.shamrock.tools.respond -import moe.fuqiuluo.shamrock.utils.MD5 -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import mqq.app.MobileQQ -import kotlin.random.Random -import kotlin.random.nextLong - -fun Routing.registerBDH() { - if(ShamrockConfig.isDev()) post("/upload_group_image") { - val troop = fetchPost("troop") - val picBytes = Base64.decode(fetchPost("pic"), Base64.DEFAULT) - val md5Str = MD5.getMd5Hex(picBytes) - val file = MobileQQ.getContext().cacheDir.resolve("vas_ad").also { - if (!it.exists()) it.mkdir() - }.resolve("$md5Str.jpg") - file.writeBytes(picBytes) - val sender = fetchPost("sender") - - val runtime = AppRuntimeFetcher.appRuntime - val transferRequest = TransferRequest() - transferRequest.needSendMsg = false - transferRequest.mSelfUin = runtime.account - transferRequest.mPeerUin = troop - transferRequest.mSecondId = sender - transferRequest.mUinType = 1 - transferRequest.mFileType = 1 - transferRequest.mUniseq = Random.nextLong(10000L .. 1000000) - transferRequest.mIsUp = true - transferRequest.mBusiType = 1030 - transferRequest.mMd5 = md5Str - transferRequest.mLocalPath = file.absolutePath - val picUpExtraInfo = TransferRequest.PicUpExtraInfo() - picUpExtraInfo.mIsRaw = true - transferRequest.mPicSendSource = 8 - transferRequest.mExtraObj = picUpExtraInfo - (runtime.getRuntimeService(ITransFileController::class.java, "all") as ITransFileController) - .transferAsync(transferRequest) - respond(isOk = true, Status.Ok, "$md5Str.jpg") - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/FavouriteWeiyun.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/FavouriteWeiyun.kt deleted file mode 100644 index e4cb185..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/FavouriteWeiyun.kt +++ /dev/null @@ -1,48 +0,0 @@ -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) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/FriendAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/FriendAction.kt deleted file mode 100644 index bb540e2..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/FriendAction.kt +++ /dev/null @@ -1,40 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi -import com.tencent.mobileqq.qroute.QRoute -import io.ktor.http.ContentType -import io.ktor.server.application.call -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 -import moe.fuqiuluo.shamrock.tools.fetchOrNull -import moe.fuqiuluo.shamrock.tools.fetchOrThrow -import moe.fuqiuluo.shamrock.tools.getOrPost - -fun Routing.friendAction() { - getOrPost("/get_stranger_info") { - val userId = fetchOrThrow("user_id").toLong() - call.respondText(GetStrangerInfo(userId), ContentType.Application.Json) - } - - getOrPost("/get_friend_list") { - val refresh = fetchOrNull("no_cache")?.toBooleanStrict() - ?: fetchOrNull("refresh")?.toBooleanStrict() ?: false - call.respondText(GetFriendList(refresh), ContentType.Application.Json) - } - - getOrPost("/is_blacklist_uin") { - val userId = fetchOrThrow("user_id").toLong() - call.respondText(IsBlackListUin(userId), ContentType.Application.Json) - } - - getOrPost("/get_friend_system_msg") { - call.respondText(GetFriendSystemMsg(), ContentType.Application.Json) - } - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GenerateQSign.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GenerateQSign.kt deleted file mode 100644 index ff7af36..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GenerateQSign.kt +++ /dev/null @@ -1,437 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.remote.api - -import android.app.ActivityManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.os.Process -import com.tencent.mobileqq.qsec.qsecdandelionsdk.Dandelion -import com.tencent.qphone.base.util.BaseApplication -import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call -import io.ktor.server.response.respond -import io.ktor.server.response.respondText -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.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.io.core.BytePacketBuilder -import kotlinx.io.core.readBytes -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY -import moe.fuqiuluo.shamrock.tools.EmptyJsonObject -import moe.fuqiuluo.shamrock.tools.fetchGetOrThrow -import moe.fuqiuluo.shamrock.tools.fetchOrNull -import moe.fuqiuluo.shamrock.tools.fetchOrThrow -import moe.fuqiuluo.shamrock.tools.fetchPostOrThrow -import moe.fuqiuluo.shamrock.tools.getOrPost -import moe.fuqiuluo.shamrock.tools.hex2ByteArray -import moe.fuqiuluo.shamrock.tools.json -import moe.fuqiuluo.shamrock.tools.respond -import moe.fuqiuluo.shamrock.tools.toHexString -import moe.fuqiuluo.shamrock.xposed.ipc.ShamrockIpc -import moe.fuqiuluo.shamrock.xposed.ipc.bytedata.IByteData -import moe.fuqiuluo.shamrock.xposed.ipc.qsign.IQSigner -import mqq.app.MobileQQ -import java.nio.ByteBuffer - -private var signer: IQSigner? = null -private var byteData: IByteData? = null - -private fun getMsfServiceInfo(): ActivityManager.RunningServiceInfo? { - val context = MobileQQ.getContext() - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val runningServices = activityManager.getRunningServices(Int.MAX_VALUE) - for (serviceInfo in runningServices) { - val serviceName = serviceInfo.service.className - if (serviceName == "com.tencent.mobileqq.msf.service.MsfService") { - return serviceInfo - } - } - return null -} - -private fun isMsfServiceAlive(): Boolean { - return getMsfServiceInfo() != null -} - -fun Routing.qsign() { - getOrPost("/reset_qsign") { - val context = MobileQQ.getContext() - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val runningServices = activityManager.getRunningServices(Int.MAX_VALUE) - - for (serviceInfo in runningServices) { - val serviceName = serviceInfo.service.className - if (serviceName == "com.tencent.mobileqq.msf.service.MsfService") { - Process.killProcess(serviceInfo.pid) - } - } - - GlobalScope.launch(Dispatchers.Main) { - val componentName = ComponentName(BaseApplication.getContext().packageName, "com.tencent.mobileqq.msf.service.MsfService") - val intent = Intent() - intent.component = componentName - intent.putExtra("to_SenderProcessName", "com.tencent.mobileqq") - BaseApplication.getContext().startService(intent) - } - - call.respond(OldApiResult(0, "重新启动MSF", data = EmptyJsonObject)) - } - - getOrPost("/get_running_service") { - val context = MobileQQ.getContext() - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val runningServices = activityManager.getRunningServices(Int.MAX_VALUE) - val output = mutableListOf() - for (serviceInfo in runningServices) { - val serviceName = serviceInfo.service.className - val pid = serviceInfo.pid - val uid = serviceInfo.uid - output.add(mapOf( - "service" to serviceName, - "pid" to pid, - "uid" to uid - ).json) - } - call.respondText(output.json.toString()) - } - - get("/get_cmd_whitelist") { - if (!isMsfServiceAlive()) { - call.respond(OldApiResult(-2, "MSF服务未启动", null)) - return@get - } - if (signer == null || signer?.asBinder()?.isBinderAlive == false) { - if (!initSigner()) { - respond(false, Status.InternalHandlerError) - return@get - } - } - val list = signer!!.cmdWhiteList - call.respond(OldApiResult(0, "success", list)) - } - - getOrPost("/get_xw_debug_id") { - if (!isMsfServiceAlive()) { - call.respond(OldApiResult(-2, "MSF服务未启动", null)) - return@getOrPost - } - if (signer == null || signer?.asBinder()?.isBinderAlive == false) { - if (!initSigner()) { - respond(false, Status.InternalHandlerError) - return@getOrPost - } - } - - val uin = fetchOrThrow("uin") - val data = fetchOrThrow("data") - - lateinit var start: String - lateinit var end: String - - data.split("_").let { - start = it[0] - end = it[1] - } - val xwDebugId = signer!!.xwDebugId(uin, start, end) - - call.respond(OldApiResult(0, "success", xwDebugId.toHexString())) - } - - route("/sign") { - get { - val uin = fetchGetOrThrow("uin") - val cmd = fetchGetOrThrow("cmd") - val seq = fetchGetOrThrow("seq").toInt() - val buffer = fetchGetOrThrow("buffer").hex2ByteArray() - - requestSign(cmd, uin, seq, buffer) - } - post { - val uin = fetchPostOrThrow("uin") - val cmd = fetchPostOrThrow("cmd") - val seq = fetchPostOrThrow("seq").toInt() - val buffer = fetchPostOrThrow("buffer").hex2ByteArray() - - requestSign(cmd, uin, seq, buffer) - } - } - - get("/custom_energy") { - val data = fetchGetOrThrow("data") - val salt = fetchGetOrThrow("salt").hex2ByteArray() - - val sign = Dandelion.getInstance().fly(data, salt) - call.respond(OldApiResult(0, "success", sign.toHexString())) - } - - route("/energy") { - get { - val data = fetchGetOrThrow("data") - if(!(data.startsWith("810_") || data.startsWith("812_"))) { - call.respond(OldApiResult(-2, "data参数不合法", null)) - return@get - } - - val uin = fetchOrThrow("uin") - val salt = fetchSalt(data, uin) - if (salt.isEmpty()) { - call.respond(OldApiResult(-2, "无法自动决断mode,请主动提供", null)) - return@get - } - - val sign = Dandelion.getInstance().fly(data, salt) - call.respond(OldApiResult(0, "success", sign.toHexString())) - } - post { - val data = fetchPostOrThrow("data") - if(!(data.startsWith("810_") || data.startsWith("812_"))) { - call.respond(OldApiResult(-2, "data参数不合法", null)) - return@post - } - val uin = fetchOrThrow("uin") - val salt = fetchSalt(data, uin) - if (salt.isEmpty()) { - call.respond(OldApiResult(-2, "无法自动决断mode,请主动提供", null)) - return@post - } - - val sign = Dandelion.getInstance().fly(data, salt) - call.respond(OldApiResult(0, "success", sign.toHexString())) - } - } - - get("/get_byte") { - if (!isMsfServiceAlive()) { - call.respond(OldApiResult(-2, "MSF服务未启动", null)) - return@get - } - if (byteData == null || byteData?.asBinder()?.isBinderAlive == false) { - val binder = ShamrockIpc.get(ShamrockIpc.IPC_BYTEDATA) - if (binder == null) { - call.respond(OldApiResult(-2, "获取失败", null)) - return@get - } else { - byteData = IByteData.Stub.asInterface(binder) - binder.linkToDeath({ - byteData = null - }, 0) - } - } - - val data = fetchGetOrThrow("data") - if(!(data.startsWith("810_") || data.startsWith("812_"))) { - call.respond(OldApiResult(-2, "data参数不合法", null)) - return@get - } - - val uin = fetchOrThrow("uin") - val salt = fetchSalt(data, uin) - if (salt.isEmpty()) { - call.respond(OldApiResult(-2, "无法自动决断mode,请主动提供", null)) - return@get - } - - val sign = byteData!!.sign(uin, data, salt).sign - - if (sign == null) { - call.respond(OldApiResult(-2, "获取失败", null)) - } else { - call.respond(OldApiResult(0, "success", sign.toHexString())) - } - } - - get("/friend_sign") { - if (!isMsfServiceAlive()) { - call.respond(OldApiResult(-2, "MSF服务未启动", null)) - return@get - } - if (signer == null || signer?.asBinder()?.isBinderAlive == false) { - if (!initSigner()) { - respond(false, Status.InternalHandlerError) - return@get - } - } - - val addUin = fetchOrThrow("add_uin") - val source = fetchOrThrow("source") - val uin = fetchOrThrow("uin").toLong() - - val sign = signer!!.energy("add_friend", BytePacketBuilder().also { - it.writeLong(uin) - it.writeLong(addUin.toLong()) - it.writeInt(source.toInt()) - }.build().readBytes()) - if (sign == null) { - call.respond(OldApiResult(-1, "failed", null)) - } else { - call.respond(OldApiResult(0, "success", sign.toHexString())) - } - } - - get("/group_sign") { - if (!isMsfServiceAlive()) { - call.respond(OldApiResult(-2, "MSF服务未启动", null)) - return@get - } - if (signer == null || signer?.asBinder()?.isBinderAlive == false) { - if (!initSigner()) { - respond(false, Status.InternalHandlerError) - return@get - } - } - - val addUin = fetchOrThrow("group_uin") - val source = fetchOrThrow("source") - val subsource = fetchOrThrow("sub_source") - val uin = fetchOrThrow("uin").toLong() - - val sign = signer!!.energy("add_group", BytePacketBuilder().also { - it.writeLong(uin) - it.writeLong(addUin.toLong()) - it.writeInt(source.toInt()) - it.writeInt(subsource.toInt()) - }.build().readBytes()) - if (sign == null) { - call.respond(OldApiResult(-1, "failed", null)) - } else { - call.respond(OldApiResult(0, "success", sign.toHexString())) - } - } -} - -private suspend inline fun PipelineContext.fetchSalt( - data: String, - uin: String -): ByteArray { - var mode = fetchOrNull("mode") - if (mode == null) { - mode = when(data) { - "810_d", "810_a", "810_f", "810_9" -> "v2" - "810_2", "810_25", "810_7", "810_24" -> "v1" - "812_b", "812_a" -> "v3" - "812_5" -> "v4" - else -> null - } - } - if (mode == null) { - return EMPTY_BYTE_ARRAY - } - - val version = fetchOrThrow("version") - if (!version.startsWith("6.0.0")) { - throw RuntimeException("version参数应该是6.0.0开头") - } - - return when (mode) { - "v1" -> { - val guid = fetchOrThrow("guid").hex2ByteArray() - val salt = ByteBuffer.allocate(8 + 2 + guid.size + 2 + 10 + 4) - val sub = data.substring(4).toInt(16) - salt.putLong(uin.toLong()) - salt.putShort(guid.size.toShort()) - salt.put(guid) - salt.putShort(version.length.toShort()) - salt.put(version.toByteArray()) - salt.putInt(sub) - salt.array() - } - "v2" -> { - val guid = fetchOrThrow("guid").hex2ByteArray() - val sub = data.substring(4).toInt(16) - val salt = ByteBuffer.allocate(4 + 2 + guid.size + 2 + 10 + 4 + 4) - salt.putInt(0) - salt.putShort(guid.size.toShort()) - salt.put(guid) - salt.putShort(version.length.toShort()) - salt.put(version.toByteArray()) - salt.putInt(sub) - salt.putInt(0) - salt.array() - } - "v3" -> { // 812_a - val phone = fetchOrThrow("phone").toByteArray() // 86-xxx - val salt = ByteBuffer.allocate(phone.size + 2 + 2 + version.length + 2) - salt.put(phone) - //println(String(phone)) - salt.putShort(0) - salt.putShort(version.length.toShort()) - salt.put(version.toByteArray()) - salt.putShort(0) - salt.array() - } - "v4" -> { // 812_5 - error("Not support [v4] mode.") - } - else -> EMPTY_BYTE_ARRAY - } -} - -@Serializable -private data class Sign( - val token: String, - val extra: String, - val sign: String, - val o3did: String, - val requestCallback: List -) - -private suspend fun initSigner(): Boolean { - if (!isMsfServiceAlive()) { - return false - } - val binder = ShamrockIpc.get(ShamrockIpc.IPC_QSIGN) - if (binder == null) { - //respond(false, Status.InternalHandlerError) - return false - } else { - signer = IQSigner.Stub.asInterface(binder) - binder.linkToDeath({ - signer = null - }, 0) - return true - } -} - -private suspend fun PipelineContext.requestSign( - cmd: String, - uin: String, - seq: Int, - buffer: ByteArray, -) { - if (!isMsfServiceAlive()) { - call.respond(OldApiResult(-2, "MSF服务未启动", null)) - return - } - if (signer == null || signer?.asBinder()?.isBinderAlive == false) { - if (!initSigner()) { - respond(false, Status.InternalHandlerError) - return - } - } - - val sign = withTimeoutOrNull(5000) { - signer!!.sign(cmd, seq, uin, buffer) - } ?: run { - respond(false, Status.IAmTired) - return - } - - call.respond(OldApiResult(0, "success", Sign( - sign.token.toHexString(), - sign.extra.toHexString(), - sign.sign.toHexString(), "", listOf() - ))) -} - diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GetFrameworkInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GetFrameworkInfo.kt deleted file mode 100644 index 1e6fa68..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GetFrameworkInfo.kt +++ /dev/null @@ -1,27 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import io.ktor.server.routing.Routing -import io.ktor.server.routing.get -import kotlinx.coroutines.delay -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.tools.getOrPost -import moe.fuqiuluo.shamrock.tools.respond -import moe.fuqiuluo.shamrock.helper.LogCenter -import kotlin.system.exitProcess - -fun Routing.obtainFrameworkInfo() { - getOrPost("/get_start_time") { - respond( - isOk = true, - code = Status.Ok, - moe.fuqiuluo.shamrock.remote.HTTPServer.startTime - ) - } - - get("/shut") { - moe.fuqiuluo.shamrock.remote.HTTPServer.stop() - LogCenter.log("正在关闭Shamrock。", toast = true) - delay(3000) - exitProcess(0) - } -} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GroupAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GroupAction.kt deleted file mode 100644 index 5526c89..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GroupAction.kt +++ /dev/null @@ -1,174 +0,0 @@ -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.* -import moe.fuqiuluo.shamrock.tools.fetchGetOrNull -import moe.fuqiuluo.shamrock.tools.fetchOrNull -import moe.fuqiuluo.shamrock.tools.fetchOrThrow -import moe.fuqiuluo.shamrock.tools.getOrPost - -fun Routing.troopAction() { - getOrPost("/set_group_comment_face") { - val groupId = fetchOrThrow("group_id").toLong() - val msgId = fetchOrNull("msg_id")?.toIntOrNull() ?: fetchOrThrow("message_id").toInt() - val faceId = fetchOrThrow("face_id").toInt() - val isSet = fetchGetOrNull("is_set") ?: "true" - call.respondText(SetGroupCommentFace(groupId, msgId, faceId, when(isSet) { - "true" -> true - "false" -> false - "1" -> true - "0" -> false - else -> true - }), ContentType.Application.Json) - } - - getOrPost("/get_not_joined_group_info") { - val groupId = fetchOrThrow("group_id").toLong() - call.respondText(GetNotJoinedGroupInfo(groupId), ContentType.Application.Json) - } - - getOrPost("/get_prohibited_member_list") { - val groupId = fetchOrThrow("group_id").toLong() - call.respondText(GetProhibitedMemberList(groupId), ContentType.Application.Json) - } - - getOrPost("/group_touch") { - val groupId = fetchOrThrow("group_id").toLong() - val userId = fetchOrThrow("user_id").toLong() - call.respondText(GroupPoke(groupId, userId), ContentType.Application.Json) - } - - getOrPost("/get_group_honor_info") { - val groupId = fetchOrThrow("group_id").toLong() - val refresh = fetchOrNull("no_cache")?.toBooleanStrict() - ?: fetchOrNull("refresh")?.toBooleanStrict() ?: false - call.respondText(GetTroopHonor(groupId, refresh), ContentType.Application.Json) - } - - getOrPost("/get_group_member_list") { - val groupId = fetchOrThrow("group_id").toLong() - val refresh = fetchOrNull("no_cache")?.toBooleanStrict() - ?: fetchOrNull("refresh")?.toBooleanStrict() ?: false - call.respondText(GetTroopMemberList(groupId, refresh), ContentType.Application.Json) - } - - getOrPost("/get_group_member_info") { - val groupId = fetchOrThrow("group_id").toLong() - val userId = fetchOrThrow("user_id").toLong() - val refresh = fetchOrNull("no_cache")?.toBooleanStrict() - ?: fetchOrNull("refresh")?.toBooleanStrict() ?: false - call.respondText(GetTroopMemberInfo(groupId, userId, refresh), ContentType.Application.Json) - } - - getOrPost("/get_group_list") { - val refresh = fetchOrNull("no_cache")?.toBooleanStrict() - ?: fetchOrNull("refresh")?.toBooleanStrict() ?: true - call.respondText(GetTroopList(refresh), ContentType.Application.Json) - } - - getOrPost("/get_group_info") { - val groupId = fetchOrThrow("group_id").toLong() - val refresh = fetchOrNull("no_cache")?.toBooleanStrict() - ?: fetchOrNull("refresh")?.toBooleanStrict() ?: false - call.respondText(GetTroopInfo(groupId, refresh), ContentType.Application.Json) - } - - getOrPost("/set_group_special_title") { - val groupId = fetchOrThrow("group_id").toLong() - val userId = fetchOrThrow("user_id").toLong() - val title = fetchOrThrow("special_title") - call.respondText(SetGroupUnique(groupId, userId, title), ContentType.Application.Json) - } - - getOrPost("/set_group_name") { - val groupId = fetchOrThrow("group_id").toLong() - val card = fetchOrThrow("group_name") - call.respondText(ModifyTroopName(groupId, card), ContentType.Application.Json) - } - - getOrPost("/set_group_card") { - val groupId = fetchOrThrow("group_id").toLong() - val userId = fetchOrThrow("user_id").toLong() - val card = fetchOrNull("card") ?: "" - call.respondText(ModifyTroopMemberName(groupId, userId, card), ContentType.Application.Json) - } - - getOrPost("/set_group_admin") { - val groupId = fetchOrThrow("group_id").toLong() - val userId = fetchOrThrow("user_id").toLong() - val enable = fetchOrThrow("enable").toBooleanStrict() - call.respondText(SetGroupAdmin(groupId, userId, enable), ContentType.Application.Json) - } - - getOrPost("/set_group_whole_ban") { - val groupId = fetchOrThrow("group_id").toLong() - val enable = fetchOrThrow("enable").toBooleanStrict() - call.respondText(SetGroupWholeBan(groupId, enable), ContentType.Application.Json) - } - - getOrPost("/set_group_ban") { - val groupId = fetchOrThrow("group_id").toLong() - val userId = fetchOrThrow("user_id").toLong() - val duration = fetchOrNull("duration")?.toInt() ?: (30 * 60) - - call.respondText(BanTroopMember(groupId, userId, duration), ContentType.Application.Json) - } - - getOrPost("/set_group_kick") { - val userId = fetchOrThrow("user_id").toLong() - val groupId = fetchOrThrow("group_id").toLong() - val kickMsg = fetchOrNull("kick_msg") ?: "" - val rejectAddRequest = when(fetchGetOrNull("reject_add_request")) { - "1", "true" -> true - else -> false - } - call.respondText(KickTroopMember(groupId, userId, rejectAddRequest, kickMsg), ContentType.Application.Json) - } - - getOrPost("/set_essence_msg") { - val messageId = fetchOrThrow("message_id").toInt() - call.respondText(SetEssenceMessage(messageId), ContentType.Application.Json) - } - - getOrPost("/delete_essence_msg") { - val messageId = fetchOrThrow("message_id").toInt() - call.respondText(DeleteEssenceMessage(messageId), ContentType.Application.Json) - } - - getOrPost("/get_essence_msg_list") { - val groupId = fetchOrThrow("group_id").toLong() - val page = fetchOrNull("page")?.toIntOrNull() ?: 0 - val pageSize = fetchOrNull("page_size")?.toIntOrNull() ?: 20 - call.respondText(GetEssenceMessageList(groupId, page, pageSize), ContentType.Application.Json) - } - - getOrPost("/get_group_system_msg") { - call.respondText(GetGroupSystemMsg(), ContentType.Application.Json) - } - - getOrPost("/_get_group_notice") { - val groupId = fetchOrThrow("group_id").toLong() - call.respondText(GetGroupNotice(groupId), ContentType.Application.Json) - } - - getOrPost("/_send_group_notice") { - val groupId = fetchOrThrow("group_id").toLong() - val text = fetchOrThrow("content") - val image = fetchOrNull("image") - call.respondText(SendGroupNotice(groupId, text, image), ContentType.Application.Json) - } - - 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) - } - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GuildAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GuildAction.kt deleted file mode 100644 index 7140b65..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/GuildAction.kt +++ /dev/null @@ -1,171 +0,0 @@ -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 io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.route -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.remote.action.handlers.CreateGuildRole -import moe.fuqiuluo.shamrock.remote.action.handlers.DeleteGuildRole -import moe.fuqiuluo.shamrock.remote.action.handlers.GetGProChannelList -import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildFeeds -import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildList -import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildMemberList -import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildMemberProfile -import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildMetaByGuest -import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildRoles -import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildServiceProfile -import moe.fuqiuluo.shamrock.remote.action.handlers.SendGuildMessage -import moe.fuqiuluo.shamrock.remote.action.handlers.SendMessage -import moe.fuqiuluo.shamrock.remote.action.handlers.SetGuildMemberRole -import moe.fuqiuluo.shamrock.remote.action.handlers.UpdateGuildRole -import moe.fuqiuluo.shamrock.tools.fetchGetOrNull -import moe.fuqiuluo.shamrock.tools.fetchGetOrThrow -import moe.fuqiuluo.shamrock.tools.fetchOrNull -import moe.fuqiuluo.shamrock.tools.fetchOrThrow -import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray -import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject -import moe.fuqiuluo.shamrock.tools.fetchPostJsonString -import moe.fuqiuluo.shamrock.tools.fetchPostOrNull -import moe.fuqiuluo.shamrock.tools.fetchPostOrThrow -import moe.fuqiuluo.shamrock.tools.getOrPost -import moe.fuqiuluo.shamrock.tools.isJsonData -import moe.fuqiuluo.shamrock.tools.isJsonObject -import moe.fuqiuluo.shamrock.tools.isJsonString -import moe.fuqiuluo.shamrock.tools.jsonArray - -fun Routing.guildAction() { - getOrPost("/get_guild_service_profile") { - call.respondText(GetGuildServiceProfile(), ContentType.Application.Json) - } - - getOrPost("/get_guild_list") { - val refresh = fetchGetOrNull("refresh") ?: fetchOrNull("no_cache") - call.respondText(GetGuildList(refresh?.toBoolean() ?: false, false), ContentType.Application.Json) - } - - getOrPost("/get_guild_member_list") { - val guildId = fetchOrThrow("guild_id") - val all = fetchGetOrNull("all")?.toBoolean() ?: false - call.respondText(GetGuildMemberList(guildId.toULong(), all, fetchOrNull("next_token") ?: ""), ContentType.Application.Json) - } - - getOrPost("/get_guild_meta_by_guest") { - val guildId = fetchOrThrow("guild_id") - call.respondText(GetGuildMetaByGuest(guildId.toULong()), ContentType.Application.Json) - } - - getOrPost("/get_guild_channel_list") { - val guildId = fetchOrThrow("guild_id") - val refresh = fetchGetOrNull("refresh") ?: fetchOrNull("no_cache") - call.respondText(GetGProChannelList(guildId.toULong(), refresh?.toBoolean() ?: false), ContentType.Application.Json) - } - - getOrPost("/get_guild_member_profile") { - val guildId = fetchOrThrow("guild_id") - val userId = fetchOrThrow("user_id") - call.respondText(GetGuildMemberProfile(guildId.toULong(), userId.toULong()), ContentType.Application.Json) - } - - route("/(send_guild_channel_msg|send_guild_message|send_guild_msg)".toRegex()) { - get { - val guildId = fetchGetOrThrow("guild_id").toULong() - val channelId = fetchGetOrThrow("channel_id").toULong() - val message = fetchGetOrThrow("message") - val autoEscape = fetchGetOrNull("auto_escape")?.toBoolean() ?: false - val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 3 - val recallDuration = fetchGetOrNull("recall_duration")?.toLong() - call.respondText(SendGuildMessage(guildId, channelId, message, autoEscape, retryCnt, recallDuration), ContentType.Application.Json) - } - post { - val retryCnt = fetchOrNull("retry_cnt")?.toInt() ?: 3 - val recallDuration = fetchOrNull("recall_duration")?.toLong() - val guildId = fetchOrThrow("guild_id").toULong() - val channelId = fetchOrThrow("channel_id").toULong() - call.respondText(if (isJsonData() && !isJsonString("message")) { - if (isJsonObject("message")) { - SendGuildMessage( - guildId = guildId, - channelId = channelId, - message = listOf(fetchPostJsonObject("message")).jsonArray, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } else { - SendGuildMessage( - guildId = guildId, - channelId = channelId, - message = fetchPostJsonArray("message"), - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } - } else { - val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false - SendGuildMessage( - guildId = guildId, - channelId = channelId, - message = fetchOrThrow("message"), - autoEscape = autoEscape, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - }, ContentType.Application.Json) - } - } - - getOrPost("/get_guild_feeds") { - val guildId = fetchOrThrow("guild_id").toULong() - val channelId = fetchOrNull("channel_id")?.toULong() ?: 0uL - val from = fetchOrNull("from")?.toInt() ?: 0 - call.respondText(GetGuildFeeds(guildId, channelId, from), ContentType.Application.Json) - } - - getOrPost("/get_guild_roles") { - val guildId = fetchOrThrow("guild_id").toULong() - call.respondText(GetGuildRoles(guildId), ContentType.Application.Json) - } - - getOrPost("/delete_guild_role") { - val guildId = fetchOrThrow("guild_id").toULong() - val roleId = fetchOrThrow("role_id").toULong() - call.respondText(DeleteGuildRole(guildId, roleId), ContentType.Application.Json) - } - - getOrPost("/set_guild_member_role") { - val guildId = fetchOrThrow("guild_id").toULong() - val roleId = fetchOrThrow("role_id").toULong() - val set = fetchOrNull("set")?.toBoolean() ?: false - val userId = fetchOrNull("user_id")?.toULong() - val users = fetchOrNull("users")?.split(",")?.map { it.toULong() } - call.respondText( - if (userId != null) { - SetGuildMemberRole(guildId, userId, roleId, set) - } else if (users != null) { - SetGuildMemberRole(guildId, users, roleId, set) - } else { - throw IllegalArgumentException("missing user_id or users") - }, - ContentType.Application.Json - ) - } - - getOrPost("/update_guild_role") { - val guildId = fetchOrThrow("guild_id").toULong() - val roleId = fetchOrThrow("role_id").toULong() - val name = fetchOrThrow("name") - val color = fetchOrThrow("color").toLong() - call.respondText(UpdateGuildRole(guildId, roleId, name, color), ContentType.Application.Json) - } - - getOrPost("/create_guild_role") { - val guildId = fetchOrThrow("guild_id").toULong() - val name = fetchOrThrow("name") - val color = fetchOrThrow("color").toLong() - val initialUsers = fetchOrThrow("initial_users").split(",").map { it.toULong() } - call.respondText(CreateGuildRole(guildId, color, name, initialUsers), ContentType.Application.Json) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/LogExplorer.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/LogExplorer.kt deleted file mode 100644 index a74c730..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/LogExplorer.kt +++ /dev/null @@ -1,17 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import io.ktor.server.application.call -import io.ktor.server.response.respondText -import io.ktor.server.routing.Routing -import moe.fuqiuluo.shamrock.tools.fetchOrNull -import moe.fuqiuluo.shamrock.tools.getOrPost -import moe.fuqiuluo.shamrock.helper.LogCenter - -fun Routing.showLog() { - getOrPost("/log") { - val start = fetchOrNull("start")?.toIntOrNull() ?: 0 - val recent =fetchOrNull("recent")?.toBooleanStrictOrNull() ?: false - val log = LogCenter.getLogLines(start, recent) - call.respondText(log.joinToString("\n")) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MainRoute.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MainRoute.kt deleted file mode 100644 index 59c4645..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MainRoute.kt +++ /dev/null @@ -1,111 +0,0 @@ -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 -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 -import moe.fuqiuluo.shamrock.remote.config.ECHO_KEY -import moe.fuqiuluo.shamrock.remote.structures.EmptyObject -import moe.fuqiuluo.shamrock.remote.structures.IndexData -import moe.fuqiuluo.shamrock.remote.structures.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.fetchPostJsonObjectOrNull -import moe.fuqiuluo.shamrock.tools.isJsonArray -import moe.fuqiuluo.shamrock.tools.isJsonObject -import moe.fuqiuluo.shamrock.tools.json -import moe.fuqiuluo.shamrock.tools.respond -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import mqq.app.MobileQQ - -@Serializable -data class OldApiResult( - val code: Int, - val msg: String = "", - @Contextual - val data: T? = null -) - -suspend fun PipelineContext.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.handleAsJsonArray(data: JsonArray) { - data.forEach { - when (it) { - is JsonArray -> handleAsJsonArray(it) - is JsonObject -> handleAsJsonObject(it) - else -> handleAsJsonObject(it.jsonObject) - } - } -} - -fun Routing.echoVersion() { - route("/") { - get { - respond( - isOk = true, - code = Status.Ok, - data = IndexData(PlatformUtils.getClientVersion(MobileQQ.getContext()), HTTPServer.startTime, call.request.httpVersion) - ) - } - 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") - } else { - (fetchOrNull("echo") ?: "").json - } - call.attributes.put(ECHO_KEY, echo) - - val params = fetchPostJsonObjectOrNull("params") ?: 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) - } - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt deleted file mode 100644 index 983687b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt +++ /dev/null @@ -1,385 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import moe.fuqiuluo.shamrock.helper.MessageHelper -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import io.ktor.http.ContentType -import io.ktor.server.application.call -import io.ktor.server.response.respondText -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 moe.fuqiuluo.shamrock.helper.db.MessageDB -import moe.fuqiuluo.shamrock.remote.action.handlers.* -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.tools.fetchGetOrNull -import moe.fuqiuluo.shamrock.tools.fetchGetOrThrow -import moe.fuqiuluo.shamrock.tools.fetchOrNull -import moe.fuqiuluo.shamrock.tools.fetchOrThrow -import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray -import moe.fuqiuluo.shamrock.tools.fetchPostJsonElement -import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject -import moe.fuqiuluo.shamrock.tools.fetchPostJsonString -import moe.fuqiuluo.shamrock.tools.fetchPostOrNull -import moe.fuqiuluo.shamrock.tools.fetchPostOrThrow -import moe.fuqiuluo.shamrock.tools.getOrPost -import moe.fuqiuluo.shamrock.tools.isJsonArray -import moe.fuqiuluo.shamrock.tools.isJsonData -import moe.fuqiuluo.shamrock.tools.isJsonObject -import moe.fuqiuluo.shamrock.tools.isJsonString -import moe.fuqiuluo.shamrock.tools.jsonArray -import moe.fuqiuluo.shamrock.tools.respond - -fun Routing.messageAction() { - route("/adapt_share_json") { - get { - val cover = fetchGetOrThrow("cover") - val desc = fetchGetOrThrow("desc") - val url = fetchGetOrNull("url") ?: "" - call.respondText(AdaptShareJson(cover, desc, url), ContentType.Application.Json) - } - post { - //val json = if (isJsonData() && (isJsonObject("json") || isJsonArray("json"))) - // fetchPostJsonElement("json").toString() - //else fetchPostOrThrow("json") - val cover = fetchPostOrThrow("cover") - val desc = fetchPostOrThrow("desc") - val url = fetchPostOrNull("url") ?: "" - call.respondText(AdaptShareJson(cover, desc, url), ContentType.Application.Json) - } - } - - getOrPost("/get_forward_msg") { - val id = fetchOrThrow("id") - call.respondText(GetForwardMsg(id), ContentType.Application.Json) - } - - getOrPost("/get_group_msg_history") { - val peerId = fetchOrThrow("group_id") - val cnt = fetchOrNull("count")?.toInt() ?: 20 - val startId = fetchOrNull("message_id")?.let { - val messageId = it.toInt() - if (messageId == 0) return@let 0L - MessageDB.getInstance() - .messageMappingDao() - .queryByMsgHashId(messageId)?.qqMsgId - } ?: fetchOrNull("message_seq")?.let { - val messageSeq = it.toInt() - if (messageSeq == 0) return@let 0L - MessageDB.getInstance() - .messageMappingDao() - .queryByMsgSeq(MsgConstant.KCHATTYPEGROUP, peerId, messageSeq)?.qqMsgId - } ?: 0L - call.respondText(GetHistoryMsg("group", peerId, cnt, startId), ContentType.Application.Json) - } - - getOrPost("/get_history_msg") { - val msgType = fetchOrThrow("message_type") - val peerId = fetchOrThrow(if (msgType == "group") "group_id" else "user_id") - val cnt = fetchOrNull("count")?.toInt() ?: 20 - val startId = fetchOrNull("message_seq")?.toInt()?.let { - if (it == 0) return@let 0L - MessageDB.getInstance() - .messageMappingDao() - .queryByMsgHashId(it)?.qqMsgId - } ?: 0L - call.respondText(GetHistoryMsg(msgType, peerId, cnt, startId), ContentType.Application.Json) - } - - getOrPost("/clear_msgs") { - val msgType = fetchOrThrow("message_type") - val peerId = fetchOrThrow(if (msgType == "group") "group_id" else "user_id") - call.respondText(ClearMsgs(msgType, peerId), ContentType.Application.Json) - } - - getOrPost("/delete_msg") { - val msgHash = fetchOrThrow("message_id").toInt() - call.respondText(DeleteMessage(msgHash), ContentType.Application.Json) - } - - getOrPost("/get_msg") { - val msgHash = fetchOrThrow("message_id").toInt() - call.respondText(GetMsg(msgHash), ContentType.Application.Json) - } - - getOrPost("/send_msg_by_resid") { - val resId = fetchOrThrow("res_id") - val peerId = fetchOrThrow("peer_Id") - val messageType = fetchOrThrow("message_type") - call.respondText(SendMsgByResid(peerId, resId, messageType)) - } - - route("/(send_msg|send_message)".toRegex()) { - get { - val msgType = fetchGetOrThrow("message_type") - val message = fetchGetOrThrow("message") - val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 5 - 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 ?: "", - 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")) { - SendMessage( - chatType = chatType, - peerId = peerId, - message = if (isJsonObject("message")) listOf(fetchPostJsonObject("message")).jsonArray else fetchPostJsonArray( - "message" - ), - fromId = groupId ?: userId ?: "", - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } else { - val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false - //SendMessage(chatType, peerId, fetchPostOrThrow("message"), autoEscape) - SendMessage( - chatType = chatType, - peerId = peerId, - message = fetchPostOrThrow("message"), - autoEscape = autoEscape, - fromId = groupId ?: userId ?: "", - retryCnt = retryCnt, - recallDuration = recallDuration - ) - }, ContentType.Application.Json - ) - } - } - - route("/send_group_(msg|message)".toRegex()) { - get { - val groupId = fetchGetOrThrow("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( - 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, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } else { - SendMessage( - chatType = MsgConstant.KCHATTYPEGROUP, - peerId = groupId, - message = if (isJsonObject("message")) listOf(fetchPostJsonObject("message")).jsonArray else fetchPostJsonArray( - "message" - ), - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } - } else { - SendMessage( - MsgConstant.KCHATTYPEGROUP, - groupId, - fetchPostOrThrow("message"), - autoEscape, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } - - call.respondText(result, ContentType.Application.Json) - } - } - - route("/send_private_(msg|message)".toRegex()) { - get { - 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, - retryCnt = retryCnt, recallDuration = recallDuration - ), ContentType.Application.Json - ) - } - post { - val userId = fetchPostOrThrow("user_id") - val groupId = fetchPostOrNull("group_id") - - val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP - - 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( - chatType = chatType, - peerId = userId, - message = fetchPostJsonString("message"), - autoEscape = autoEscape, - fromId = groupId ?: userId, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } else { - SendMessage( - chatType = chatType, - peerId = userId, - message = if (isJsonObject("message")) listOf(fetchPostJsonObject("message")).jsonArray else fetchPostJsonArray( - "message" - ), - fromId = groupId ?: userId, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } - } else { - SendMessage( - chatType = chatType, - peerId = userId, - message = fetchPostOrThrow("message"), - autoEscape = autoEscape, - fromId = groupId ?: userId, - retryCnt = retryCnt, - recallDuration = recallDuration - ) - } - - call.respondText(result, ContentType.Application.Json) - } - } - - route("/upload_multi_(msg|message)".toRegex()) { - post { - val msgType = fetchPostOrThrow("message_type") - val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType) - val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5 - - val userId = fetchPostOrNull("user_id") - val groupId = fetchPostOrNull("group_id") - val messages = fetchPostJsonArray("messages") - call.respondText(UploadMultiMessage( - chatType = chatType, - peerId = if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!, - fromId = groupId ?: userId ?: "", - messages = messages, - retryCnt = retryCnt - ), ContentType.Application.Json) - } - get { - respond(false, Status.InternalHandlerError, "Not support GET method") - } - } - - route("/send_forward_(msg|message)".toRegex()) { - post { - val msgType = fetchPostOrThrow("message_type") - val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType) - val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5 - - val userId = fetchPostOrNull("user_id") - val groupId = fetchPostOrNull("group_id") - val messages = fetchPostJsonArray("messages") - call.respondText( - SendForwardMessage( - chatType, - if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!, - groupId ?: userId ?: "", - messages, - retryCnt - ), - ContentType.Application.Json - ) - } - get { - respond(false, Status.InternalHandlerError, "Not support GET method") - } - } - - route("/send_private_forward_(msg|message)".toRegex()) { - post { - val userId = fetchPostOrThrow("user_id") - val groupId = fetchPostOrNull("group_id") - - val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5 - val messages = fetchPostJsonArray("messages") - call.respondText( - SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId, groupId ?: userId, messages, retryCnt), - ContentType.Application.Json - ) - } - get { - respond(false, Status.InternalHandlerError, "Not support GET method") - } - } - - route("/send_group_forward_(msg|message)".toRegex()) { - post { - val groupId = fetchPostOrThrow("group_id") - - val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5 - val messages = fetchPostJsonArray("messages") - call.respondText( - SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId, messages = messages, retryCnt = retryCnt), - ContentType.Application.Json - ) - } - get { - respond(false, Status.InternalHandlerError, "Not support GET method") - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/OtherAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/OtherAction.kt deleted file mode 100644 index 930426e..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/OtherAction.kt +++ /dev/null @@ -1,147 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import io.ktor.http.ContentType -import io.ktor.http.content.PartData -import io.ktor.http.content.forEachPart -import io.ktor.http.content.streamProvider -import io.ktor.server.application.call -import io.ktor.server.request.receiveMultipart -import io.ktor.server.response.respondText -import io.ktor.server.routing.Routing -import io.ktor.server.routing.post -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.json.JsonObject -import moe.fuqiuluo.shamrock.remote.action.handlers.CleanCache -import moe.fuqiuluo.shamrock.remote.action.handlers.DownloadFile -import moe.fuqiuluo.shamrock.remote.action.handlers.GetDeviceBattery -import moe.fuqiuluo.shamrock.remote.action.handlers.GetVersionInfo -import moe.fuqiuluo.shamrock.remote.action.handlers.RestartMe -import moe.fuqiuluo.shamrock.remote.action.handlers.UploadFileToShamrock -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.tools.asString -import moe.fuqiuluo.shamrock.tools.fetchOrNull -import moe.fuqiuluo.shamrock.tools.fetchOrThrow -import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray -import moe.fuqiuluo.shamrock.tools.getOrPost -import moe.fuqiuluo.shamrock.tools.hex2ByteArray -import moe.fuqiuluo.shamrock.tools.isJsonArray -import moe.fuqiuluo.shamrock.tools.json -import moe.fuqiuluo.shamrock.tools.respond -import moe.fuqiuluo.shamrock.utils.FileUtils -import moe.fuqiuluo.shamrock.utils.MD5 -import java.io.File - -fun Routing.otherAction() { - - if (ShamrockConfig.allowShell()) { - post("/shell") { - val runtime = Runtime.getRuntime() - val dir = fetchOrThrow("dir") - val out = hashMapOf() - withTimeoutOrNull(5000L) { - if (isJsonArray("cmd")) { - val cmd = fetchPostJsonArray("cmd").map { - if (it is JsonObject) it.toString() else it.asString - }.toTypedArray() - withContext(Dispatchers.IO) { - runtime.exec(cmd, null, File(dir)).apply { waitFor() } - } - } else { - val cmd = fetchOrThrow("cmd") - withContext(Dispatchers.IO) { - runtime.exec(cmd, null, File(dir)).apply { waitFor() } - } - } - }.also { - if (it == null) { - respond(false, Status.IAmTired, "执行超时") - } else { - it.inputStream.use { - out["out"] = it.readBytes().toString(Charsets.UTF_8) - } - it.errorStream.use { - out["err"] = it.readBytes().toString(Charsets.UTF_8) - } - } - } - - call.respondText(out.json.toString(), ContentType.Application.Json) - } - } - - getOrPost("/get_version_info") { - call.respondText(GetVersionInfo(), ContentType.Application.Json) - } - - getOrPost("/get_device_battery") { - call.respondText(GetDeviceBattery(), ContentType.Application.Json) - } - - getOrPost("/clean_cache") { - call.respondText(CleanCache(), ContentType.Application.Json) - } - - getOrPost("/set_restart") { - call.respondText(RestartMe(2000), ContentType.Application.Json) - } - - getOrPost("/download_file") { - val url = fetchOrNull("url") - val b64 = fetchOrNull("base64") - val name = fetchOrNull("name") - val root = fetchOrNull("root") - val threadCnt = fetchOrNull("thread_cnt")?.toInt() ?: 0 - val headers = fetchOrNull("headers") ?: "" - call.respondText(DownloadFile(url, b64, threadCnt, headers.split("\r\n"), name, root), ContentType.Application.Json) - } - - post("/upload_file") { - val partData = call.receiveMultipart() - partData.forEachPart { part -> - if (part.name == "file") { - val bytes = (part as PartData.FileItem).streamProvider().readBytes() - val tmp = FileUtils.renameByMd5(FileUtils.getTmpFile("cache").also { - it.writeBytes(bytes) - }) - respond(true, Status.Ok, DownloadFile.DownloadResult( - file = tmp.absolutePath, - md5 = MD5.genFileMd5Hex(tmp.absolutePath) - ), "成功") - return@forEachPart - } - } - respond(false, Status.BadRequest, "没有上传文件信息") - } - - getOrPost("/upload_file_to_shamrock") { - val md5 = fetchOrThrow("md5").hex2ByteArray() - val offset = fetchOrNull("offset")?.toULong() ?: 0uL - val chunk = fetchOrThrow("chunk").toByteArray() - val fileSize = fetchOrNull("file_size")?.toULong() ?: chunk.size.toULong() - call.respondText(UploadFileToShamrock(md5, fileSize, offset, chunk), ContentType.Application.Json) - } - - getOrPost("/config/set_boolean") { - val key = fetchOrThrow("key") - val value = fetchOrThrow("value").toBooleanStrict() - ShamrockConfig[key] = value - respond(true, Status.Ok, "success") - } - - getOrPost("/config/set_int") { - val key = fetchOrThrow("key") - val value = fetchOrThrow("value").toInt() - ShamrockConfig[key] = value - respond(true, Status.Ok, "success") - } - - getOrPost("/config/set_string") { - val key = fetchOrThrow("key") - val value = fetchOrThrow("value") - ShamrockConfig[key] = value - respond(true, Status.Ok, "success") - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ProfileAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ProfileAction.kt deleted file mode 100644 index 0d148e7..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ProfileAction.kt +++ /dev/null @@ -1,86 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import com.tencent.mobileqq.app.QQAppInterface -import io.ktor.http.ContentType -import io.ktor.server.application.call -import io.ktor.server.response.respond -import io.ktor.server.response.respondText -import io.ktor.server.routing.Routing -import moe.fuqiuluo.shamrock.remote.action.ActionManager -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.handlers.GetLoginInfo -import moe.fuqiuluo.shamrock.remote.structures.CommonResult -import moe.fuqiuluo.shamrock.remote.structures.CurrentAccount -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import mqq.app.MobileQQ - -fun Routing.profileRouter() { - getOrPost("/set_qq_profile") { - val nickName = fetchOrThrow("nickname") - val company = fetchOrThrow("company") - val email = fetchOrThrow("email") - val college = fetchOrThrow("college") - val personalNote = fetchOrThrow("personal_note") - - val age = fetchOrNull("age") - val birthday = fetchOrNull("birthday") - - val handler = ActionManager["set_qq_profile"]!! - - call.respondText( - handler.handle( - ActionSession( - mapOf( - "nickname" to nickName, - "company" to company, - "email" to email, - "college" to college, - "personal_note" to personalNote, - "age" to age, - "birthday" to birthday - ) - ) - ), ContentType.Application.Json - ) - } - - getOrPost("/get_account_info") { - val accounts = MobileQQ.getMobileQQ().allAccounts - val runtime = AppRuntimeFetcher.appRuntime - val curUin = runtime.currentAccountUin - val account = accounts?.firstOrNull { it.uin == curUin } - if (!runtime.isLogin || account == null || !account.isLogined) { - this.call.respond( - CommonResult( - "ok", Status.InternalHandlerError.code, CurrentAccount( - 1094950020L, false, "未登录" - ) - ) - ) - } else { - this.call.respond( - CommonResult( - "ok", 0, CurrentAccount( - curUin.toLong(), - runtime.isLogin, - if (runtime is QQAppInterface) runtime.currentNickname else "unknown" - ) - ) - ) - } - } - - getOrPost("/get_history_account_info") { - val accounts = MobileQQ.getMobileQQ().allAccounts - val accountList = accounts.map { - CurrentAccount(it.uin.toLong(), it.isLogined) - } - respond(true, Status.Ok, accountList) - } - - getOrPost("/get_login_info") { - call.respondText(GetLoginInfo(), ContentType.Application.Json) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ProtocolAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ProtocolAction.kt deleted file mode 100644 index 60e29a9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ProtocolAction.kt +++ /dev/null @@ -1,79 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import com.tencent.mobileqq.dt.model.FEBound -import io.ktor.server.routing.Routing -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import moe.fuqiuluo.shamrock.remote.structures.Protocol -import moe.fuqiuluo.shamrock.remote.structures.QSignDtConfig -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.utils.MMKVFetcher -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import mqq.app.MobileQQ -import oicq.wlogin_sdk.tlv_type.tlv_t100 -import oicq.wlogin_sdk.tlv_type.tlv_t106 -import oicq.wlogin_sdk.tlv_type.tlv_t18 -import oicq.wlogin_sdk.tools.util - -fun Routing.obtainProtocolData() { - getOrPost("/send_packet") { - val cmd = fetchOrThrow("cmd") - val isPb = fetchOrThrow("proto").toBooleanStrict() - val buffer = fetchOrThrow("buffer").hex2ByteArray() - val resp = BaseSvc.sendBufferAW(cmd, isPb, buffer) - respond(true, Status.Ok, data = resp?.toHexString() ?: "null", msg = "成功") - } - - getOrPost("/set_guid") { - val guid = fetchOrThrow("guid").hex2ByteArray() - val ctx = MobileQQ.getContext() - util.save_cur_guid(ctx, guid) - util.saveGuidToFile(ctx, guid) - val guildLock = MMKVFetcher.mmkvWithId("guid") - guildLock.putString("guid", guid.toHexString()) - respond(true, Status.Ok, msg = "成功") - } - - getOrPost("/get_msf_info") { - val mqq = MobileQQ.getMobileQQ() - val ctx = MobileQQ.getContext() - - val t18 = tlv_t18() - val t100 = tlv_t100() - val t106 = tlv_t106() - - val qimei = kotlin.runCatching { - util.get_qimei(ctx).toHexString() - }.getOrDefault("") - - val encodeTable = FEBound::class.java.getDeclaredField("mConfigEnCode").also { - it.isAccessible = true - }.get(null) as Array - val decodeTable = FEBound::class.java.getDeclaredField("mConfigDeCode").also { - it.isAccessible = true - }.get(null) as Array - - respond( - isOk = true, - code = Status.Ok, - data = Protocol( - mqq.qqProcessName, - mqq.appId.toLong(), mqq.qua, kotlin.runCatching { mqq.ntCoreVersion }.getOrDefault(0), - mqq.msfConnectedNetType, - qimei, - util.getSvnVersion(), - PlatformUtils.getAndroidID(), - util.getGuidFromFile(ctx).toHexString(), - util.get_ksid(ctx).toHexString(), - util.get_network_type(ctx), - t18._ping_version.toByte(), t18._sso_version, - t100._sso_ver, t100._db_buf_ver, - t106._SSoVer, t106._TGTGTVer, - - util.get_android_dev_info(ctx).toHexString(), - - qSignDtConfig = QSignDtConfig(encodeTable, decodeTable) - ) - ) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/RequestAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/RequestAction.kt deleted file mode 100644 index 5a663bb..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/RequestAction.kt +++ /dev/null @@ -1,39 +0,0 @@ -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.SetFriendAddRequest -import moe.fuqiuluo.shamrock.remote.action.handlers.SetGroupAddRequest -import moe.fuqiuluo.shamrock.tools.fetchOrNull -import moe.fuqiuluo.shamrock.tools.fetchOrThrow -import moe.fuqiuluo.shamrock.tools.getOrPost - -fun Routing.requestRouter() { - getOrPost("/set_friend_add_request") { - val flag = fetchOrThrow("flag") - val approve = fetchOrNull("approve")?.toBooleanStrict() ?: true - val remark = fetchOrNull("remark") - val notSeen = fetchOrNull("not_seen")?.toBooleanStrict() ?: false - - call.respondText( - SetFriendAddRequest(flag, approve, remark, notSeen), - ContentType.Application.Json - ) - } - - getOrPost("/set_group_add_request") { - val flag = fetchOrThrow("flag") - val approve = fetchOrNull("approve")?.toBooleanStrict() ?: true - val remark = fetchOrNull("reason") - val subType = fetchOrThrow("sub_type") - val notSeen = fetchOrNull("not_seen")?.toBooleanStrict() ?: false - - call.respondText( - SetGroupAddRequest(flag, approve, subType, remark, notSeen), - ContentType.Application.Json - ) - } - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ResourceAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ResourceAction.kt deleted file mode 100644 index 5e6629f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/ResourceAction.kt +++ /dev/null @@ -1,101 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import io.ktor.http.ContentType -import moe.fuqiuluo.shamrock.utils.FileUtils -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.call -import io.ktor.server.request.document -import io.ktor.server.response.respond -import io.ktor.server.response.respondFile -import io.ktor.server.response.respondText -import io.ktor.server.routing.Routing -import io.ktor.server.routing.get -import io.ktor.server.routing.route -import moe.fuqiuluo.shamrock.remote.action.handlers.* -import moe.fuqiuluo.shamrock.tools.* - -private fun formatFileName(file: String): String = file - .replace(regex = "[{}\\-]".toRegex(), replacement = "") - .replace(" ", "") - .split(".")[0].lowercase() - -fun Routing.fetchRes() { - getOrPost("/get_record") { - val file = formatFileName( fetchGetOrThrow("file") ) - val format = fetchOrThrow("out_format") - call.respondText(GetRecord(file, format), ContentType.Application.Json) - } - - getOrPost("/get_image") { - val file = formatFileName( fetchGetOrThrow("file") ) - call.respondText(GetImage(file), ContentType.Application.Json) - } - - getOrPost("/upload_group_file") { - val groupId = fetchOrThrow("group_id").toLong() - val file = fetchOrThrow("file") - val name = fetchOrThrow("name") - call.respondText(UploadGroupFile(groupId, file, name), ContentType.Application.Json) - } - - getOrPost("/upload_private_file") { - val userId = fetchOrThrow("user_id").toLong() - val file = fetchOrThrow("file") - val name = fetchOrThrow("name") - call.respondText(UploadPrivateFile(userId, file, name), ContentType.Application.Json) - } - - getOrPost("/create_group_file_folder") { - val groupId = fetchOrThrow("group_id").toLong() - val name = fetchOrThrow("name") - call.respondText(CreateGroupFileFolder(groupId, name), ContentType.Application.Json) - } - - getOrPost("/delete_group_folder") { - val groupId = fetchOrThrow("group_id").toLong() - val id = fetchOrThrow("folder_id") - call.respondText(DeleteGroupFolder(groupId, id), ContentType.Application.Json) - } - - getOrPost("/delete_group_file") { - val groupId = fetchOrThrow("group_id").toLong() - val id = fetchOrThrow("file_id") - val busid = fetchOrThrow("busid").toInt() - call.respondText(DeleteGroupFile(groupId, id, busid), ContentType.Application.Json) - } - - getOrPost("/get_group_file_system_info") { - val groupId = fetchOrThrow("group_id").toLong() - call.respondText(GetGroupFileSystemInfo(groupId), ContentType.Application.Json) - } - - getOrPost("/get_group_root_files") { - val groupId = fetchOrThrow("group_id").toLong() - call.respondText(GetGroupRootFiles(groupId), ContentType.Application.Json) - } - - getOrPost("/get_group_files_by_folder") { - val groupId = fetchOrThrow("group_id").toLong() - val folderId = fetchOrThrow("folder_id") - call.respondText(GetGroupSubFiles(groupId, folderId), ContentType.Application.Json) - } - - getOrPost("/get_group_file_url") { - val groupId = fetchOrThrow("group_id").toLong() - val id = fetchOrThrow("file_id") - val busid = fetchOrThrow("busid").toInt() - call.respondText(GetGroupFileUrl(groupId, id, busid), ContentType.Application.Json) - } - - route("/res/[a-fA-F0-9]{32}".toRegex()) { - get { - val md5 = call.request.document() - val file = FileUtils.getFile(md5) - if (!file.exists()) { - call.respond(HttpStatusCode.NotFound) - } else { - call.respondFile(file) - } - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TestAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TestAction.kt deleted file mode 100644 index 68334d1..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TestAction.kt +++ /dev/null @@ -1,54 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgElement -import com.tencent.qqnt.kernel.nativeinterface.TextElement -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -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.handlers.SendMsgByResid -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.tools.fetchOrNull -import moe.fuqiuluo.shamrock.tools.fetchOrThrow -import moe.fuqiuluo.shamrock.tools.getOrPost -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher - -fun Routing.testAction() { - if (ShamrockConfig.isDev()) { - LogCenter.log("testAction is enabled.", Level.WARN) - } else { - return - } - - getOrPost("/createUidFromTinyId") { - val selfId = fetchOrThrow("selfId").toLong() - val peerId = fetchOrThrow("peerId") - call.respondText( - NTServiceFetcher.kernelService.wrapperSession.msgService.createUidFromTinyId( - selfId, - peerId.toLong() - ) - ) - } - - getOrPost("/addSendMsg") { - val msgService = NTServiceFetcher.kernelService.wrapperSession.msgService - val msgId = msgService.getMsgUniqueId(System.currentTimeMillis()) - msgService.addSendMsg(msgId, - MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()), - arrayListOf( - MsgElement().apply { - elementType = MsgConstant.KELEMTYPETEXT - textElement = TextElement().apply { - content = "测试消息" - } - } - ), - hashMapOf()) - call.respondText("ok") - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TicketAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TicketAction.kt deleted file mode 100644 index f082d0f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TicketAction.kt +++ /dev/null @@ -1,81 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import io.ktor.http.ContentType -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import io.ktor.server.application.call -import io.ktor.server.response.respondText -import io.ktor.server.routing.Routing -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.remote.action.handlers.* -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.tools.* - -fun Routing.ticketActions() { - getOrPost("/get_http_cookies") { - val appid =fetchOrThrow("appid") - val daid = fetchOrThrow("daid") - val jumpurl = fetchOrThrow("jumpurl") - call.respondText(GetHttpCookies(appid, daid, jumpurl), ContentType.Application.Json) - } - - getOrPost("/get_credentials") { - val domain = fetchOrNull("domain") - if (domain != null) { - call.respondText(GetCredentials(domain), ContentType.Application.Json) - } else { - call.respondText(GetCredentials(), ContentType.Application.Json) - } - } - - getOrPost("/get_cookies") { - val domain = fetchOrNull("domain") - if (domain != null) { - call.respondText(GetCookies(domain = domain), ContentType.Application.Json) - } else { - call.respondText(GetCookies(), ContentType.Application.Json) - } - } - - getOrPost("/get_csrf_token") { - val domain = fetchOrNull("domain") - if (domain != null) { - call.respondText(GetCSRF(domain), ContentType.Application.Json) - } else { - call.respondText(GetCSRF(), ContentType.Application.Json) - } - } - - fun getTicket(uin: String, id: Int, debug: Boolean = false) = TicketSvc.getTicket(uin, id)?.let { - mutableMapOf( - "sig" to (it._sig?.toHexString() ?: "null"), - "key" to (it._sig_key?.toHexString() ?: "null") - ).also { map -> - if (debug) - map["content"] = ((it._sig?.decodeToString() ?: "") + ":" + (it._sig_key?.decodeToString() ?: "null")) - }.json.asJsonObject - } ?: EmptyJsonObject - - getOrPost("/get_ticket") { - val uin = fetchOrThrow("uin") - val ticket = when(val id = fetchOrThrow("id").toInt()) { - 32 -> TicketSvc.getStWeb(uin) - else -> { - respond(true, Status.Ok, data = getTicket(uin, id)) - return@getOrPost - } - } - respond(true, Status.Ok, data = ticket) - } - - if (ShamrockConfig.isDev()) getOrPost("/get_all_ticket") { - val uin = fetchOrThrow("uin") - - val ticketMap = mutableMapOf() - TicketSvc.SigType.ALL_TICKET.forEach { - ticketMap[it] = getTicket(uin, it, true) - } - - respond(true, Status.Ok, data = ticketMap) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/UserAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/UserAction.kt deleted file mode 100644 index bcd1847..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/UserAction.kt +++ /dev/null @@ -1,56 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.api - -import io.ktor.http.ContentType -import moe.fuqiuluo.shamrock.helper.LogicException -import io.ktor.server.application.call -import io.ktor.server.response.respondText -import io.ktor.server.routing.Routing -import moe.fuqiuluo.shamrock.remote.action.ActionManager -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.handlers.* -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.utils.PlatformUtils - -fun Routing.userAction() { - getOrPost("/switch_account") { - val userId = fetchOrThrow("user_id").toLong() - call.respondText(SwitchAccount(userId), ContentType.Application.Json) - } - - getOrPost("/set_group_leave") { - val groupId = fetchOrThrow("group_id").toLong() - call.respondText(LeaveTroop(groupId), ContentType.Application.Json) - } - - getOrPost("/_get_online_clients") { - call.respondText(GetOnlineClients(), ContentType.Application.Json) - } - - getOrPost("/_get_model_show") { - val model = fetchOrThrow("model") - call.respondText(GetModelShowList(model), ContentType.Application.Json) - } - - getOrPost("/_set_model_show") { - val model = fetchOrThrow("model") - val manu = fetchOrNull("manu") ?: fetchOrThrow("model_show") - val modelshow = fetchOrNull("modelshow") ?: "Android" - val imei = fetchOrNull("imei") ?: PlatformUtils.getAndroidID() - val show = fetchOrNull("show")?.toBooleanStrictOrNull() ?: true - call.respondText(SetModelShow(model, manu, modelshow, imei, show), ContentType.Application.Json) - } - - getOrPost("/get_model_show") { - val uin = fetchOrNull("user_id") - call.respondText(GetModelShow(uin?.toLong() ?: 0), ContentType.Application.Json) - } - - getOrPost("/send_like") { - val uin = fetchOrThrow("user_id") - val cnt = fetchOrThrow("times") - call.respondText(SendLike( - uin.toLong(), - cnt.toInt() - ), ContentType.Application.Json) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/WeatherAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/WeatherAction.kt deleted file mode 100644 index d52a296..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/WeatherAction.kt +++ /dev/null @@ -1,26 +0,0 @@ -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.GetWeather -import moe.fuqiuluo.shamrock.remote.action.handlers.GetWeatherCityCode -import moe.fuqiuluo.shamrock.tools.* - -fun Routing.weatherAction() { - getOrPost("/get_weather") { - val cityCode = fetchOrNull("code") - if (cityCode == null) { - val city = fetchOrThrow("city") - call.respondText(GetWeather(city)) - return@getOrPost - } - call.respondText(GetWeather(cityCode.toInt()), ContentType.Application.Json) - } - - getOrPost("/get_weather_city_code") { - val city = fetchOrThrow("city") - call.respondText(GetWeatherCityCode(city), ContentType.Application.Json) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/config/ContentNegotiation.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/config/ContentNegotiation.kt deleted file mode 100644 index 4636c34..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/config/ContentNegotiation.kt +++ /dev/null @@ -1,17 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.config - -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.Application -import io.ktor.server.application.install -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import kotlinx.serialization.json.Json - -fun Application.contentNegotiation() { - install(ContentNegotiation) { - json(Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/config/StatusPages.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/config/StatusPages.kt deleted file mode 100644 index 18b0312..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/config/StatusPages.kt +++ /dev/null @@ -1,77 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.config - -import moe.fuqiuluo.shamrock.helper.ErrorTokenException -import moe.fuqiuluo.shamrock.helper.LogicException -import moe.fuqiuluo.shamrock.helper.ParamsException -import io.ktor.server.application.Application -import io.ktor.server.application.install -import io.ktor.server.plugins.statuspages.StatusPages -import io.ktor.server.request.uri -import io.ktor.server.response.respond -import io.ktor.util.AttributeKey -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.remote.structures.CommonResult -import moe.fuqiuluo.shamrock.remote.structures.ErrorCatch -import moe.fuqiuluo.shamrock.remote.structures.Status - -val ECHO_KEY = AttributeKey("echo") - -fun Application.statusPages() { - install(StatusPages) { - exception { call, cause -> - val echo = if (call.attributes.contains(ECHO_KEY)) { - call.attributes[ECHO_KEY] - } else null - call.respond( - CommonResult( - status = "failed", - retcode = Status.BadParam.code, - data = ErrorCatch(call.request.uri, cause.message ?: ""), - echo = echo - ) - ) - } - exception { call, cause -> - val echo = if (call.attributes.contains(ECHO_KEY)) { - call.attributes[ECHO_KEY] - } else null - call.respond( - CommonResult( - status = "failed", - retcode = Status.LogicError.code, - data = ErrorCatch(call.request.uri, cause.message ?: ""), - echo = echo - ) - ) - } - exception { call, cause -> - val echo = if (call.attributes.contains(ECHO_KEY)) { - call.attributes[ECHO_KEY] - } else null - call.respond( - CommonResult( - status = "failed", - retcode = Status.ErrorToken.code, - data = ErrorCatch(call.request.uri, cause.message ?: ""), - echo = echo - ) - ) - } - exception { call, cause -> - val echo = if (call.attributes.contains(ECHO_KEY)) { - call.attributes[ECHO_KEY] - } else null - LogCenter.log(cause.stackTraceToString(), Level.ERROR) - call.respond( - CommonResult( - status = "failed", - retcode = Status.InternalHandlerError.code, - data = ErrorCatch(call.request.uri, cause.message ?: ""), - echo = echo - ) - ) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/plugin/Auth.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/plugin/Auth.kt deleted file mode 100644 index 8f1149a..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/plugin/Auth.kt +++ /dev/null @@ -1,53 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.plugin - -import io.ktor.server.application.ApplicationCall -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() - if (token.isBlank()) { - return - } - var accessToken = request.headers["Authorization"] - ?: 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 ", ignoreCase = true)) { - accessToken = accessToken.substring(7) - } - if (token != accessToken) { - throw ErrorTokenException - } -} - -internal val Auth = createApplicationPlugin("Auth") { - // 获取get请求的token参数并校验 - this.onCall { call -> - call.checkToken() - } - /* - this.onCallReceive { call, _ -> - var accessToken = call.fetchOrNull("access_token") - ?: call.fetchOrNull("ticket") - ?: call.request.headers["Authorization"] - ?: throw ErrorTokenException - if (accessToken.startsWith("Bearer ")) { - accessToken = accessToken.substring(7) - } - if (token != accessToken) { - throw ErrorTokenException - } - }*/ -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/HttpService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/HttpService.kt deleted file mode 100644 index ae6407c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/HttpService.kt +++ /dev/null @@ -1,144 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) -package moe.fuqiuluo.shamrock.remote.service - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import moe.fuqiuluo.shamrock.helper.MessageHelper -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 kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -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.shamrock.remote.service.api.HttpTransmitServlet -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.service.api.GlobalEventTransmitter - -internal object HttpService: HttpTransmitServlet() { - private val subscribes = arrayListOf() - - override fun subscribe(job: Job) { - subscribes.add(job) - } - - override fun unsubscribe() { - subscribes.removeIf { - it.cancel() - return@removeIf true - } - } - - override fun init() { - if (subscribes.isNotEmpty()) return - subscribe(GlobalScope.launch { - GlobalEventTransmitter.onMessageEvent { (record, event) -> - val respond = pushTo(event) ?: return@onMessageEvent - handleQuicklyReply(record, event.messageId, respond.bodyAsText()) - } - }) - subscribe(GlobalScope.launch { - GlobalEventTransmitter.onNoticeEvent { event -> - pushTo(event) - } - }) - subscribe(GlobalScope.launch { - GlobalEventTransmitter.onRequestEvent { - pushTo(it) - } - }) - LogCenter.log("HttpService: 初始化服务", Level.WARN) - } - - private suspend fun handleQuicklyReply(record: MsgRecord, msgHash: Int, jsonText: String) { - try { - 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() - 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 && 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 handleQuicklyActions(data: JsonObject) { - val action = data["action"].asString - val echo = data["echo"] ?: EmptyJsonString - - 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)) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/PacketReceiver.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/PacketReceiver.kt deleted file mode 100644 index 6d2eedd..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/PacketReceiver.kt +++ /dev/null @@ -1,69 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.remote.service - -import com.tencent.qphone.base.remote.FromServiceMsg -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import moe.fuqiuluo.shamrock.tools.broadcast -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver -import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest -import mqq.app.MobileQQ - -internal object PacketReceiver { - private val allowCommandList: MutableSet by lazy { mutableSetOf( - "trpc.msg.olpush.OlPushService.MsgPush", - - ) } // 非动态注册,永久常驻的包 - private val HandlerByIpcSet = hashSetOf() - - fun init() { - DynamicReceiver.register("register_handler_cmd", IPCRequest { - val cmd = it.getStringExtra("handler_cmd")!! - LogCenter.log({ "RegisterHandler(cmd = $cmd)" }, Level.DEBUG) - HandlerByIpcSet.add(cmd) - }) - DynamicReceiver.register("unregister_handler_cmd", IPCRequest { - val cmd = it.getStringExtra("handler_cmd")!! - LogCenter.log({ "UnRegisterHandler(cmd = $cmd)" }, Level.DEBUG) - HandlerByIpcSet.remove(cmd) - }) - MobileQQ.getContext().broadcast("xqbot") { - putExtra("__cmd", "msf_waiter") - LogCenter.log("MSF Packet Receiver running!") - } - } - - private fun onReceive(from: FromServiceMsg) { - if (HandlerByIpcSet.contains(from.serviceCmd) - || allowCommandList.contains(from.serviceCmd) - ) { - LogCenter.log({ "ReceivePacket(cmd = ${from.serviceCmd})" }, Level.DEBUG) - MobileQQ.getContext().broadcast("xqbot") { - putExtra("__cmd", from.serviceCmd) - putExtra("buffer", from.wupBuffer) - putExtra("seq", from.requestSsoSeq) - } - } else { - val seq = if (from.appSeq == -1) from.requestSsoSeq else from.appSeq - val hash = (from.serviceCmd + seq).hashCode() - LogCenter.log({ "ReceivePacket[$hash](cmd = ${from.serviceCmd}, seq = $seq)" }, Level.DEBUG) - MobileQQ.getContext().broadcast("xqbot") { - putExtra("__hash", hash) - putExtra("buffer", from.wupBuffer) - putExtra("seq", seq) - } - } - } - - fun internalOnReceive(from: FromServiceMsg?) { - if (from == null) return - GlobalScope.launch(Dispatchers.Default) { - onReceive(from) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketClientService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketClientService.kt deleted file mode 100644 index 26f1f7c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketClientService.kt +++ /dev/null @@ -1,54 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service - -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import moe.fuqiuluo.shamrock.remote.service.api.WebSocketClientServlet -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onMessageEvent -import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onNoticeEvent -import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onRequestEvent - -internal class WebSocketClientService( - override val address: String, - heartbeatInterval: Long, - wsHeaders: Map -) : WebSocketClientServlet(address, heartbeatInterval, wsHeaders) { - private val subscribes = mutableSetOf() - - init { - startHeartbeatTimer() - } - - override fun subscribe(job: Job) { - subscribes.add(job) - } - - override fun init() { - subscribe(launch { - onMessageEvent { (_, event) -> - pushTo(event) - } - }) - subscribe(launch { - onNoticeEvent { event -> - pushTo(event) - } - }) - subscribe(launch { - onRequestEvent { event -> - pushTo(event) - } - }) - LogCenter.log("WebSocketClientService: 初始化服务", Level.WARN) - } - - override fun unsubscribe() { - subscribes.removeIf { job -> - job.cancel() - return@removeIf true - } - LogCenter.log("WebSocketClientService: 释放服务", Level.WARN) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketService.kt deleted file mode 100644 index b6cd734..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketService.kt +++ /dev/null @@ -1,99 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.remote.service - -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import moe.fuqiuluo.shamrock.helper.ErrorTokenException -import moe.fuqiuluo.shamrock.remote.service.api.WebSocketTransmitServlet -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.remote.service.data.BotStatus -import moe.fuqiuluo.shamrock.remote.service.data.Self -import moe.fuqiuluo.shamrock.remote.service.data.push.* -import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onMessageEvent -import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onNoticeEvent -import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter.onRequestEvent -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import org.java_websocket.WebSocket -import org.java_websocket.handshake.ClientHandshake -import java.net.URI - -internal class WebSocketService( - host: String, - port: Int, - heartbeatInterval: Long, -): WebSocketTransmitServlet(host, port, heartbeatInterval) { - private val subscribes = mutableSetOf() - - override fun subscribe(job: Job) { - subscribes.add(job) - } - - override fun init() { - subscribe(launch { - onMessageEvent { (_, event) -> pushTo(event) } - }) - subscribe(launch { - onNoticeEvent { event -> pushTo(event) } - }) - subscribe(launch { - onRequestEvent { event -> pushTo(event) } - }) - LogCenter.log("WebSocketService: 初始化服务", Level.WARN) - } - - override fun unsubscribe() { - subscribes.removeIf { job -> - job.cancel() - return@removeIf true - } - LogCenter.log("WebSocketService: 释放服务", Level.WARN) - } - - override fun onOpen(conn: WebSocket, handshake: ClientHandshake) { - val token = ShamrockConfig.getActiveWebSocketConfig()?.tokens - ?: ShamrockConfig.getActiveWebSocketConfig()?.token?.split(",", "|", ",") - ?: listOf(ShamrockConfig.getToken()) - if (token.isNotEmpty()) { - var accessToken = handshake.getFieldValue("access_token") - .ifNullOrEmpty(handshake.getFieldValue("ticket")) - .ifNullOrEmpty(handshake.getFieldValue("Authorization")) - ?: throw ErrorTokenException - if (accessToken.startsWith("Bearer ", ignoreCase = true)) { - accessToken = accessToken.substring(7) - } - if (!token.contains(accessToken)) { - conn.close() - LogCenter.log({ "WSServer连接错误(${conn.remoteSocketAddress.address.hostAddress}:${conn.remoteSocketAddress.port}) 没有提供正确的token, $accessToken。" }, Level.ERROR) - return - } - } - val path = URI.create(handshake.resourceDescriptor).path - if (path != "/api") { - eventReceivers.add(conn) - pushMetaLifecycle() - } - LogCenter.log({ "WSServer连接(${conn.remoteSocketAddress.address.hostAddress}:${conn.remoteSocketAddress.port}$path)" }, Level.WARN) - } - - private fun pushMetaLifecycle() { - launch { - val runtime = AppRuntimeFetcher.appRuntime - pushTo(PushMetaEvent( - time = System.currentTimeMillis() / 1000, - selfId = app.longAccountUin, - postType = PostType.Meta, - type = MetaEventType.LifeCycle, - subType = MetaSubType.Connect, - status = BotStatus( - Self("qq", runtime.longAccountUin), runtime.isLogin, status = "正常", good = true - ), - interval = heartbeatInterval - )) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/BaseTransmitServlet.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/BaseTransmitServlet.kt deleted file mode 100644 index a25e4eb..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/BaseTransmitServlet.kt +++ /dev/null @@ -1,24 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.api - -import com.tencent.mobileqq.app.QQAppInterface -import kotlinx.coroutines.Job -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import oicq.wlogin_sdk.tools.MD5 - -internal interface BaseTransmitServlet { - val address: String - - fun transmitAccess(): Boolean - - fun subscribe(job: Job) - - fun unsubscribe() - - fun init() - - val app: QQAppInterface - get() = AppRuntimeFetcher.appRuntime as QQAppInterface - - val id: String - get() = MD5.getMD5String(address.toByteArray()) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt deleted file mode 100644 index 29bbb4b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt +++ /dev/null @@ -1,592 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.remote.service.api - -import com.tencent.qqnt.kernel.nativeinterface.MsgElement -import com.tencent.qqnt.kernel.nativeinterface.MsgRecord -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import moe.fuqiuluo.qqinterface.servlet.CardSvc -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.qqinterface.servlet.msg.toSegments -import moe.fuqiuluo.qqinterface.servlet.msg.toJson -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.remote.service.data.push.GroupFileMsg -import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole -import moe.fuqiuluo.shamrock.remote.service.data.push.MsgSubType -import moe.fuqiuluo.shamrock.remote.service.data.push.MsgType -import moe.fuqiuluo.shamrock.remote.service.data.push.PostType -import moe.fuqiuluo.shamrock.remote.service.data.push.MessageEvent -import moe.fuqiuluo.shamrock.remote.service.data.push.MessageTempSource -import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeEvent -import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeSubType -import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeType -import moe.fuqiuluo.shamrock.remote.service.data.push.PokeDetail -import moe.fuqiuluo.shamrock.remote.service.data.push.PrivateFileMsg -import moe.fuqiuluo.shamrock.remote.service.data.push.RequestEvent -import moe.fuqiuluo.shamrock.remote.service.data.push.RequestSubType -import moe.fuqiuluo.shamrock.remote.service.data.push.RequestType -import moe.fuqiuluo.shamrock.remote.service.data.push.Sender -import moe.fuqiuluo.shamrock.remote.service.data.push.SignDetail -import moe.fuqiuluo.shamrock.tools.ShamrockDsl -import moe.fuqiuluo.shamrock.tools.json -import java.util.ArrayList - -internal object GlobalEventTransmitter: BaseSvc() { - private val messageEventFlow by lazy { - MutableSharedFlow>() - } - private val noticeEventFlow by lazy { - MutableSharedFlow() - } - private val requestEventFlow by lazy { - MutableSharedFlow() - } - - private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent) - - private suspend fun pushRequest(requestEvent: RequestEvent) = requestEventFlow.emit(requestEvent) - - private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message) - - /** - * 消息 - */ - object MessageTransmitter { - /** - * 推送群聊消息 - */ - suspend fun transGroupMessage( - record: MsgRecord, - elements: ArrayList, - rawMsg: String, - msgHash: Int, - postType: PostType - ): Boolean { - val uin = app.longAccountUin - transMessageEvent(record, - MessageEvent( - time = record.msgTime, - selfId = uin, - postType = postType, - messageType = MsgType.Group, - subType = MsgSubType.NORMAL, - messageId = msgHash, - groupId = record.peerUin, - peerId = uin, - userId = record.senderUin, - message = if(ShamrockConfig.useCQ()) rawMsg.json - else elements.toSegments(record.chatType, record.peerUin.toString(), "0").toJson(), - rawMessage = rawMsg, - font = 0, - sender = Sender( - userId = record.senderUin, - nickname = record.sendNickName - .ifEmpty { record.sendRemarkName } - .ifEmpty { record.sendMemberName } - .ifEmpty { record.peerName }, - card = record.sendMemberName, - role = GroupSvc.getMemberRole(record.peerUin, record.senderUin)/*when (record.senderUin) { - GroupSvc.getOwner(record.peerUin.toString()) -> MemberRole.Owner - in GroupSvc.getAdminList(record.peerUin.toString()) -> MemberRole.Admin - else -> MemberRole.Member - }*/, - title = "", - level = "", - ) - ) - ) - return true - } - - /** - * 推送私聊消息 - */ - suspend fun transPrivateMessage( - record: MsgRecord, - elements: ArrayList, - rawMsg: String, - msgHash: Int, - postType: PostType, - tempSource: MessageTempSource = MessageTempSource.Unknown, - groupId: Long = Long.MIN_VALUE, - fromNick: String? = null - ): Boolean { - val botUin = app.longAccountUin - var nickName = record.sendNickName - if (nickName.isNullOrEmpty()) { - CardSvc.getProfileCard(record.senderUin).onSuccess { - nickName = it.strNick ?: record.peerName - } - } - transMessageEvent(record, - MessageEvent( - time = record.msgTime, - selfId = botUin, - postType = postType, - messageType = MsgType.Private, - subType = MsgSubType.Friend, - messageId = msgHash, - targetId = record.peerUin, - peerId = botUin, - userId = record.senderUin, - message = if(ShamrockConfig.useCQ()) rawMsg.json - else elements.toSegments(record.chatType, record.peerUin.toString(), "0").toJson(), - rawMessage = rawMsg, - font = 0, - sender = Sender( - userId = record.senderUin, - nickname = nickName, - card = record.sendMemberName, - role = MemberRole.Member, - title = "", - level = "", - ), - tmpSource = tempSource.id, - groupId = groupId, - fromNickName = fromNick - ) - ) - return true - } - - /** - * 推送私聊消息 - */ - suspend fun transGuildMessage( - record: MsgRecord, - elements: ArrayList, - rawMsg: String, - msgHash: Int, - postType: PostType, - ): Boolean { - val botUin = app.longAccountUin - var nickName = record.sendNickName - if (nickName.isNullOrEmpty()) { - CardSvc.getProfileCard(record.senderUin).onSuccess { - nickName = it.strNick ?: record.peerName - } - } - transMessageEvent(record, - MessageEvent( - time = record.msgTime, - selfId = botUin, - postType = postType, - messageType = MsgType.Guild, - subType = MsgSubType.Channel, - guildId = record.guildId, - channelId = record.channelId, - messageId = msgHash, - targetId = record.peerUin, - peerId = botUin, - userId = record.senderUin, - message = if(ShamrockConfig.useCQ()) rawMsg.json - else elements.toSegments(record.chatType, record.guildId, record.channelId).toJson(), - rawMessage = rawMsg, - font = 0, - sender = Sender( - userId = record.senderUin, - nickname = nickName, - card = record.sendMemberName, - role = MemberRole.Member, // TODO(GUILD ROLE) - title = record.sendNickName, - level = record.roleId.toString(), - tinyId = record.senderUid - ), - ) - ) - return true - } - } - - /** - * 文件通知 通知器 - */ - object FileNoticeTransmitter { - /** - * 推送私聊文件事件 - */ - suspend fun transPrivateFileEvent( - msgTime: Long, - userId: Long, - fileId: String, - fileSubId: String, - fileName: String, - fileSize: Long, - expireTime: Long, - url: String - ): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.PrivateUpload, - operatorId = userId, - userId = userId, - senderId = userId, - privateFile = PrivateFileMsg( - id = fileId, - name = fileName, - size = fileSize, - url = url, - subId = fileSubId, - expire = expireTime - ) - )) - return true - } - - /** - * 推送私聊文件事件 - */ - suspend fun transGroupFileEvent( - msgTime: Long, - userId: Long, - groupId: Long, - uuid: String, - fileName: String, - fileSize: Long, - bizId: Int, - url: String - ): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupUpload, - operatorId = userId, - userId = userId, - groupId = groupId, - file = GroupFileMsg( - id = uuid, - name = fileName, - size = fileSize, - busid = bizId.toLong(), - url = url - ) - )) - return true - } - } - - /** - * 群聊通知 通知器 - */ - 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, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.Notify, - subType = NoticeSubType.Poke, - operatorId = operation, - userId = operation, - groupId = groupCode, - target = target, - pokeDetail = PokeDetail( - action = action, - suffix = suffix, - actionImg = actionImg - ) - )) - return true - } - - suspend fun transGroupMemberNumChanged( - time: Long, - target: Long, - targetUid: String, - groupCode: Long, - operator: Long, - operatorUid: String, - noticeType: NoticeType, - noticeSubType: NoticeSubType - ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = noticeType, - subType = noticeSubType, - operatorId = operator, - userId = target, - senderId = operator, - target = target, - groupId = groupCode, - targetUid = targetUid, - operatorUid = operatorUid, - userUid = targetUid - )) - return true - } - - suspend fun transGroupAdminChanged( - msgTime: Long, - target: Long, - targetUid: String, - groupCode: Long, - setAdmin: Boolean - ): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupAdminChange, - subType = if (setAdmin) NoticeSubType.Set else NoticeSubType.UnSet, - operatorId = 0, - userId = target, - userUid = targetUid, - target = target, - targetUid = targetUid, - groupId = groupCode - )) - return true - } - - suspend fun transGroupBan( - msgTime: Long, - subType: NoticeSubType, - operator: Long, - operatorUid: String, - target: Long, - targetUid: String, - groupCode: Long, - duration: Int - ): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupBan, - subType = subType, - operatorId = operator, - userId = target, - senderId = operator, - target = target, - groupId = groupCode, - duration = duration, - operatorUid = operatorUid, - targetUid = targetUid - )) - return true - } - - suspend fun transGroupMsgRecall( - time: Long, - operator: Long, - target: Long, - groupCode: Long, - msgHash: Int, - tipText: String - ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupRecall, - operatorId = operator, - userId = target, - msgId = msgHash, - tip = tipText, - groupId = groupCode - )) - return true - } - - suspend fun transCardChange( - time: Long, - targetId: Long, - oldCard: String, - newCard: String, - groupId: Long - ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupCard, - userId = targetId, - cardNew = newCard, - cardOld = oldCard, - groupId = groupId - )) - return true - } - - suspend fun transTitleChange( - time: Long, - targetId: Long, - title: String, - groupId: Long - ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.Notify, - userId = targetId, - groupId = groupId, - title = title, - subType = NoticeSubType.Title - )) - return true - } - - suspend fun transEssenceChange( - time: Long, - senderUin: Long, - operatorUin: Long, - msgId: Int, - groupId: Long, - subType: NoticeSubType - ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.Essence, - senderId = senderUin, - groupId = groupId, - operatorId = operatorUin, - msgId = msgId, - subType = subType - )) - return true - } - } - - /** - * 私聊通知 通知器 - */ - object PrivateNoticeTransmitter { - suspend fun transPrivatePoke(msgTime: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.Notify, - subType = NoticeSubType.Poke, - operatorId = operation, - userId = operation, - senderId = operation, - target = target, - pokeDetail = PokeDetail( - actionImg = actionImg, - action = action, - suffix = suffix - ) - )) - return true - } - - suspend fun transPrivateRecall(time: Long, operation: Long, msgHashId: Int, tipText: String): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.FriendRecall, - operatorId = operation, - userId = operation, - msgId = msgHashId, - tip = tipText - )) - return true - } - - } - - /** - * 请求 通知器 - */ - object RequestTransmitter { - suspend fun transFriendApp(time: Long, operation: Long, tipText: String, flag: String): Boolean { - pushRequest(RequestEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Request, - type = RequestType.Friend, - userId = operation, - comment = tipText, - flag = flag - )) - return true - } - - suspend fun transGroupApply( - time: Long, - applier: Long, - applierUid: String, - reason: String, - groupCode: Long, - flag: String, - subType: RequestSubType - ): Boolean { - pushRequest(RequestEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Request, - type = RequestType.Group, - userId = applier, - userUid = applierUid, - comment = reason, - groupId = groupCode, - subType = subType, - flag = flag - )) - return true - } - } - - @ShamrockDsl - suspend inline fun onMessageEvent(collector: FlowCollector>) { - messageEventFlow.collect { - GlobalScope.launch { - collector.emit(it) - } - } - } - - @ShamrockDsl - suspend inline fun onNoticeEvent(collector: FlowCollector) { - noticeEventFlow.collect { - GlobalScope.launch { - collector.emit(it) - } - } - } - - @ShamrockDsl - suspend inline fun onRequestEvent(collector: FlowCollector) { - requestEventFlow.collect { - GlobalScope.launch { - collector.emit(it) - } - } - } -} - - - diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/HttpTransmitServlet.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/HttpTransmitServlet.kt deleted file mode 100644 index a666742..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/HttpTransmitServlet.kt +++ /dev/null @@ -1,65 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.api - -import io.ktor.client.network.sockets.ConnectTimeoutException -import io.ktor.client.plugins.HttpRequestTimeoutException -import io.ktor.client.request.header -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.client.statement.HttpResponse -import io.ktor.http.ContentType -import io.ktor.http.contentType -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.tools.GlobalClient -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.tools.ShamrockVersion -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import mqq.app.MobileQQ -import java.net.SocketException - -internal abstract class HttpTransmitServlet : BaseTransmitServlet { - override val address: String by lazy { ShamrockConfig.getWebHookAddress() } - - override fun transmitAccess(): Boolean { - return ShamrockConfig.allowWebHook() - } - - protected suspend inline fun pushTo(body: T): HttpResponse? { - if (!transmitAccess()) return null - try { - if (address.startsWith("http://") || address.startsWith("https://")) { - val response = GlobalClient.post(address) { - contentType(ContentType.Application.Json) - setBody(body) - - header("User-Agent", "Shamrock/$ShamrockVersion") - header("X-QQ-Version", PlatformUtils.getClientVersion(MobileQQ.getContext())) - val runtime = AppRuntimeFetcher.appRuntime - val curUin = runtime.currentAccountUin - header("X-Self-ID", curUin) - header("X-OneBot-Version", "11") - header("X-Impl", "Shamrock") - 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) - } - } catch (e: ConnectTimeoutException) { - LogCenter.log("HTTP推送失败: 请检查你的推送服务器。", Level.ERROR) - } catch (e: SocketException) { - LogCenter.log("HTTP推送失败: 网络波动。", Level.ERROR) - } catch (e: HttpRequestTimeoutException) { - LogCenter.log("HTTP推送失败: 推送服务器无法连接。", Level.ERROR) - } catch (e: Throwable) { - LogCenter.log("HTTP推送失败: ${e.stackTraceToString()}", Level.ERROR) - } - return null - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/WebSocketClientServlet.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/WebSocketClientServlet.kt deleted file mode 100644 index c7d90d5..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/WebSocketClientServlet.kt +++ /dev/null @@ -1,192 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.remote.service.api - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import moe.fuqiuluo.shamrock.remote.action.ActionManager -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.structures.EmptyObject -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.remote.service.data.BotStatus -import moe.fuqiuluo.shamrock.remote.service.data.Self -import moe.fuqiuluo.shamrock.remote.service.data.push.MetaEventType -import moe.fuqiuluo.shamrock.remote.service.data.push.MetaSubType -import moe.fuqiuluo.shamrock.remote.service.data.push.PostType -import moe.fuqiuluo.shamrock.remote.service.data.push.PushMetaEvent -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import org.java_websocket.client.WebSocketClient -import org.java_websocket.handshake.ServerHandshake -import java.lang.Exception -import java.net.URI -import kotlin.concurrent.timer -import kotlin.coroutines.CoroutineContext - -internal abstract class WebSocketClientServlet( - private val url: String, - private val heartbeatInterval: Long, - private val wsHeaders: Map -) : BaseTransmitServlet, WebSocketClient(URI(url), wsHeaders), CoroutineScope { - init { - if (connectedClients.containsKey(url)) { - throw RuntimeException("WebSocketClient已存在: $url") - } - } - - private var firstOpen = true - - override fun transmitAccess(): Boolean { - return ShamrockConfig.openWebSocketClient() - } - - override fun onMessage(message: String) { - launch { - handleMessage(message) - } - } - - private suspend fun handleMessage(message: String) { - val respond = kotlin.runCatching { - val actionObject = Json.parseToJsonElement(message).asJsonObject - if (actionObject["post_type"].asStringOrNull == "meta_event") { - // 防止二比把元事件push回来 - return - } - - val action = actionObject["action"].asString - val echo = actionObject["echo"] ?: EmptyJsonString - val params = actionObject["params"].asJsonObjectOrNull ?: EmptyJsonObject - - val handler = ActionManager[action] - handler?.handle(ActionSession(params, echo)) - ?: resultToString( - false, - Status.UnsupportedAction, - EmptyObject, - "不支持的Action", - echo = echo - ) - }.getOrNull() - respond?.let { send(it) } - } - - override fun onOpen(handshakedata: ServerHandshake?) { - LogCenter.log("WebSocketClient onOpen: ${handshakedata?.httpStatus}, ${handshakedata?.httpStatusMessage}") - - connectedClients[url] = this - - pushMetaLifecycle() - if (firstOpen) { - firstOpen = false - } else { - unsubscribe() - } - init() - } - - 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") - unsubscribe() - coroutineContext.cancel() - connectedClients.remove(url) - } - - override fun onError(ex: Exception?) { - LogCenter.log("WebSocketClient onError: ${ex?.message}") - unsubscribe() - coroutineContext.cancel() - connectedClients.remove(url) - } - - protected suspend inline fun pushTo(body: T) { - if (!transmitAccess() || isClosed || isClosing) return - try { - send(GlobalJson.encodeToString(body)) - } catch (e: Throwable) { - LogCenter.log("被动WS推送失败: ${e.stackTraceToString()}", Level.ERROR) - } - } - - fun startHeartbeatTimer() { - if (heartbeatInterval <= 0) { - LogCenter.log("被动WebSocket心跳间隔为0,不启动心跳", Level.WARN) - return - } - timer( - name = "heartbeat", - initialDelay = heartbeatInterval, - period = heartbeatInterval, - ) { - if (isClosed || isClosing || !isOpen) { - cancel() - return@timer - } - val runtime = AppRuntimeFetcher.appRuntime - LogCenter.log("WebSocketClient心跳: ${app.longAccountUin}", Level.DEBUG) - send(GlobalJson.encodeToString( - PushMetaEvent( - time = System.currentTimeMillis() / 1000, - selfId = app.longAccountUin, - postType = PostType.Meta, - type = MetaEventType.Heartbeat, - subType = MetaSubType.Connect, - status = BotStatus( - Self("qq", runtime.longAccountUin), - runtime.isLogin, - status = "正常", - good = true - ), - interval = heartbeatInterval - ) - ) - ) - } - } - - private fun pushMetaLifecycle() { - launch { - val runtime = AppRuntimeFetcher.appRuntime - val curUin = runtime.currentAccountUin - pushTo( - PushMetaEvent( - time = System.currentTimeMillis() / 1000, - selfId = app.longAccountUin, - postType = PostType.Meta, - type = MetaEventType.LifeCycle, - subType = MetaSubType.Connect, - status = BotStatus( - Self("qq", curUin.toLong()), runtime.isLogin, status = "正常", good = true - ), - interval = heartbeatInterval - ) - ) - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - override val coroutineContext: CoroutineContext = - Dispatchers.IO.limitedParallelism(20) - - companion object { - private val connectedClients = mutableMapOf() - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/WebSocketTransmitServlet.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/WebSocketTransmitServlet.kt deleted file mode 100644 index 1fb98d0..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/WebSocketTransmitServlet.kt +++ /dev/null @@ -1,157 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.remote.service.api - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import moe.fuqiuluo.shamrock.remote.action.ActionManager -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.structures.EmptyObject -import moe.fuqiuluo.shamrock.remote.structures.Status -import moe.fuqiuluo.shamrock.remote.structures.resultToString -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.remote.service.data.BotStatus -import moe.fuqiuluo.shamrock.remote.service.data.Self -import moe.fuqiuluo.shamrock.remote.service.data.push.MetaEventType -import moe.fuqiuluo.shamrock.remote.service.data.push.MetaSubType -import moe.fuqiuluo.shamrock.remote.service.data.push.PostType -import moe.fuqiuluo.shamrock.remote.service.data.push.PushMetaEvent -import moe.fuqiuluo.shamrock.tools.GlobalJson -import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import org.java_websocket.WebSocket -import org.java_websocket.server.WebSocketServer -import java.net.InetSocketAddress -import java.net.URI -import java.util.Collections -import java.util.Timer -import kotlin.concurrent.timer -import kotlin.coroutines.CoroutineContext - -internal abstract class WebSocketTransmitServlet( - host:String, - port: Int, - protected val heartbeatInterval: Long, -) : BaseTransmitServlet, WebSocketServer(InetSocketAddress(host, port)), CoroutineScope { - private lateinit var heartbeatTask: Timer - protected val eventReceivers: MutableList = Collections.synchronizedList(mutableListOf()) - - init { - connectionLostTimeout = 0 - } - - override val address: String = "-" - - override fun transmitAccess(): Boolean { - return ShamrockConfig.openWebSocket() - } - - inline fun broadcastAnyEvent(any: T) { - broadcastTextEvent(GlobalJson.encodeToString(any)) - } - - fun broadcastTextEvent(text: String) { - broadcast(text, eventReceivers) - } - - init { - if (heartbeatInterval > 0) { - heartbeatTask = 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, - selfId = app.longAccountUin, - postType = PostType.Meta, - type = MetaEventType.Heartbeat, - subType = MetaSubType.Connect, - status = BotStatus( - Self("qq", curUin.toLong()), - runtime.isLogin, - status = "正常", - good = true - ), - interval = heartbeatInterval - ) - ) - } - } else { - LogCenter.log("主动WebSocket心跳间隔为0,不启动心跳", Level.WARN) - } - } - - override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) { - val path = URI.create(conn.resourceDescriptor).path - if (path != "/api") { - eventReceivers.remove(conn) - } - 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) { - val path = URI.create(conn.resourceDescriptor).path - launch { - onHandleAction(conn, message, path) - } - } - - private suspend fun onHandleAction(conn: WebSocket, message: String, path: String) { - val respond = kotlin.runCatching { - val actionObject = Json.parseToJsonElement(message).asJsonObject - if (actionObject["post_type"].asStringOrNull == "meta_event") { - // 防止二比把元事件push回来 - return - } - val action = actionObject["action"].asString - val echo = actionObject["echo"] ?: EmptyJsonString - val params = actionObject["params"].asJsonObjectOrNull ?: EmptyJsonObject - - val handler = ActionManager[action] - handler?.handle(ActionSession(params, echo)) - ?: resultToString(false, Status.UnsupportedAction, EmptyObject, "不支持的Action", echo = echo) - }.getOrNull() - respond?.let { conn.send(it) } - } - - override fun onError(conn: WebSocket, ex: Exception?) { - LogCenter.log("WSServer Error: " + ex?.stackTraceToString(), Level.ERROR) - unsubscribe() - coroutineContext.cancel() - if (::heartbeatTask.isInitialized) { - heartbeatTask.cancel() - } - } - - override fun onStart() { - LogCenter.log("WSServer start running on ws://${getAddress()}!") - init() - } - - protected inline fun pushTo(body: T) { - if(!transmitAccess()) return - try { - broadcastTextEvent(GlobalJson.encodeToString(body)) - } catch (e: Throwable) { - LogCenter.log("WS推送失败: ${e.stackTraceToString()}", Level.ERROR) - } - } - - override val coroutineContext: CoroutineContext = - Dispatchers.IO.limitedParallelism(40) - } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/Config.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/Config.kt deleted file mode 100644 index 72f2b74..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/Config.kt +++ /dev/null @@ -1,41 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.config - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ServiceConfig( - @SerialName("rules") val rules: Rules? = null, - @SerialName("default_token") var defaultToken: String? = null, - @SerialName("active_websocket") var activeWebSocket: ConnectionConfig? = null, - @SerialName("passive_websocket") var passiveWebSocket: MutableList? = null, - @SerialName("allow-temp-session") var allowTempSession: Boolean = false, - @SerialName("anti_qq_trace") var antiTrace: Boolean = true -) - -@Serializable -data class ConnectionConfig( - @SerialName("address") val address: String? = null, - @SerialName("port") var port: Int? = null, - @SerialName("token") val token: String? = null, - @SerialName("tokens") val tokens: List? = null, - @SerialName("heartbeat_interval") var heartbeatInterval: Long? = null, -) - -@Serializable -data class Rules( - @SerialName("group_rule") val groupRule: GroupRule? = null, - @SerialName("private_rule") val privateRule: PrivateRule? = null -) - -@Serializable -data class GroupRule( - @SerialName("black_list") val black: List? = null, - @SerialName("white_list") val white: List? = null, -) - -@Serializable -data class PrivateRule( - @SerialName("black_list") val black: List? = null, - @SerialName("white_list") val white: List? = null, -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt deleted file mode 100644 index 1225e7c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt +++ /dev/null @@ -1,284 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.config - -import android.content.Intent -import com.tencent.mmkv.MMKV -import de.robv.android.xposed.XposedBridge -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.tools.GlobalJson5 -import moe.fuqiuluo.shamrock.utils.MMKVFetcher -import mqq.app.MobileQQ -import java.io.File - -internal object ShamrockConfig { - private val ConfigDir = MobileQQ.getContext().getExternalFilesDir(null)!! - .parentFile!!.resolve("Tencent/Shamrock").also { - if (it.exists()) it.delete() - it.mkdirs() - } - private val config = kotlin.runCatching { - GlobalJson5.decodeFromString(ConfigDir.resolve("config.json").also { - if (!it.exists()) it.writeText("{}") - }.readText()) - }.onFailure { - LogCenter.log("您的配置文件出现错误: ${it.stackTraceToString()}", Level.ERROR) - }.getOrElse { - ServiceConfig() - } - - fun isInit(): Boolean { - val mmkv = MMKVFetcher.mmkvWithId("shamrock_config") - return mmkv.getBoolean("isInit", false) - } - - private fun updateConfig(config: ServiceConfig = this.config) { - ConfigDir.resolve("config.json").writeText(GlobalJson5.encodeToString(config)) - } - - fun updateConfig(intent: Intent) { - mmkv.apply { - if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) { - putBoolean("tablet", intent.getBooleanExtra("tablet", false)) // 强制平板模式 - putInt("port", intent.getIntExtra("port", 5700)) // 主动HTTP端口 - putBoolean("ws", intent.getBooleanExtra("ws", false)) // 主动WS开关 - putBoolean("http", intent.getBooleanExtra("http", false)) // HTTP回调开关 - putString("http_addr", intent.getStringExtra("http_addr")) // WebHook回调地址 - putBoolean("ws_client", intent.getBooleanExtra("ws_client", false)) // 被动WS开关 - putBoolean("use_cqcode", intent.getBooleanExtra("use_cqcode", false)) // 使用CQ码 - putBoolean("inject_packet", intent.getBooleanExtra("inject_packet", false)) // 拦截无用包 - putBoolean("debug", intent.getBooleanExtra("debug", false)) // 调试模式 - putString( "key_store", intent.getStringExtra("key_store")) // 证书路径 - putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码 - putString( "ssl_private_pwd", intent.getStringExtra("ssl_private_pwd")) // 证书私钥密码 - putString( "ssl_alias", intent.getStringExtra("ssl_alias")) // 证书别名 - putInt( "ssl_port", intent.getIntExtra("ssl_port", 5701)) // 主动HTTP端口 - putBoolean("alive_reply", intent.getBooleanExtra("alive_reply", false)) // 自回复测试 - putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息 - putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口 - putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息 - putBoolean("forbid_useless_process", intent.getBooleanExtra("forbid_useless_process", false)) // 禁用QQ生成无用进程 - putBoolean("enable_old_bdh", intent.getBooleanExtra("enable_old_bdh", false)) // 启用旧版BDH - intent.getStringExtra("up_res_group")?.let { putString("up_res_group", it) } - } else { - XposedBridge.log("[Shamrock] 已禁用自动同步配置") - } - config.defaultToken = intent.getStringExtra("token") - config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true) - val wsPort = intent.getIntExtra("ws_port", 5800) - config.activeWebSocket = if (config.activeWebSocket == null) ConnectionConfig( - address = "0.0.0.0", - port = wsPort, - ) else config.activeWebSocket?.also { - it.port = wsPort - } - config.passiveWebSocket = intent.getStringExtra("ws_addr")?.split(",", "|", ",")?.filter { address -> - address.isNotBlank() && (address.startsWith("ws://") || address.startsWith("wss://")) - }?.map { - ConnectionConfig(address = it) - }?.toMutableList() - putBoolean("isInit", true) - } - if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) { - updateConfig() - } - } - - fun putDefaultSettings() { - val mmkv = MMKVFetcher.mmkvWithId("shamrock_config") - if ((!isInit()) && (!mmkv.getBoolean("isEmergencyInitBefore", false))){ - mmkv.apply { - putBoolean("tablet", false) // 强制平板模式 - putInt("port", 5700) // 主动HTTP端口 - putBoolean("ws", false) // 主动WS开关 - putBoolean("http", false) // HTTP回调开关 - putString("http_addr", null) // WebHook回调地址 - putBoolean("ws_client", false) // 被动WS开关 - putBoolean("use_cqcode", false) // 使用CQ码 - putBoolean("inject_packet", false) // 拦截无用包 - putBoolean("debug", false) // 调试模式 - putString("key_store", null) // 证书路径 - putString("ssl_pwd", null) // 证书密码 - putString("ssl_private_pwd", null) // 证书私钥密码 - putString("ssl_alias", null) // 证书别名 - putInt("ssl_port", 5701) // 主动HTTP端口 - putBoolean("alive_reply", true) // 自回复测试 - putBoolean("enable_self_msg", false) // 推送自己发的消息 - putBoolean("shell", false) // 开启Shell接口 - putBoolean("enable_sync_msg_as_sent_msg", true) // 推送同步消息 - putBoolean("forbid_useless_process", false) // 禁用QQ生成无用进程 - putBoolean("enable_old_bdh", false) // 启用旧版BDH - putBoolean("antiTrace", false) - putBoolean("super_anti", true) - putString("up_res_group", "") - - config.defaultToken = null - // config.antiTrace = true - val wsPort = 5800 - config.activeWebSocket = - if (config.activeWebSocket == null) ConnectionConfig( - address = "0.0.0.0", - port = wsPort, - ) else config.activeWebSocket?.also { - it.port = wsPort - } - config.passiveWebSocket = null - putBoolean("isInit", true) - putBoolean("isEmergencyInitBefore", true) - } - XposedBridge.log("[Shamrock] 强制初始化配置完成,请重新打开QQ") - } else { - XposedBridge.log("[Shamrock] 程序逻辑错误或错误地多次强制初始化") - mmkv.putBoolean("isEmergencyInitBefore", false) - XposedBridge.log("[Shamrock] 如果执意要再次强制初始化,请重新执行程序") - } - } - - private val mmkv: MMKV - get() = MMKVFetcher.mmkvWithId("shamrock_config") - - fun getUpResGroup(): String { - return mmkv.getString("up_res_group", "") ?: "" - } - - fun aliveReply(): Boolean { - return mmkv.getBoolean("alive_reply", false) - } - - fun allowTempSession(): Boolean { - return config.allowTempSession - } - - fun getGroupMsgRule(): GroupRule? { - return config.rules?.groupRule - } - - fun getPrivateRule(): PrivateRule? { - 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) - } - - fun forbidUselessProcess(): Boolean { - return mmkv.getBoolean("forbid_useless_process", false) - } - - fun openWebSocketClient(): Boolean { - return mmkv.getBoolean("ws_client", false) - } - - fun getWebSocketClientAddress(): List { - return config.passiveWebSocket ?: emptyList() - } - - fun openWebSocket(): Boolean { - return mmkv.getBoolean("ws", false) - } - - fun getActiveWebSocketConfig(): ConnectionConfig? { - return config.activeWebSocket - } - - fun getToken(): String { - return config.defaultToken ?: "" - } - - fun useCQ(): Boolean { - return mmkv.getBoolean("use_cqcode", false) - } - - fun allowWebHook(): Boolean { - return mmkv.getBoolean("http", false) - } - - fun getWebHookAddress(): String { - return mmkv.getString("http_addr", "") ?: "" - } - - fun forceTablet(): Boolean { - return mmkv.getBoolean("tablet", true) - } - - fun getPort(): Int { - return mmkv.getInt("port", 5700) - } - - fun isInjectPacket(): Boolean { - return mmkv.getBoolean("inject_packet", false) - } - - fun enableOldBDH(): Boolean { - return mmkv.getBoolean("enable_old_bdh", false) - } - - fun isDebug(): Boolean { - return mmkv.getBoolean("debug", false) - } - - fun ssl(): Boolean { - return getKeyStorePath()?.exists() == true - } - - fun getKeyStorePath(): File? { - mmkv.getString("key_store", null)?.let { - return File(it) - } - return null - } - - fun sslPwd(): CharArray? { - return mmkv.getString("ssl_pwd", null)?.toCharArray() - } - - fun sslPrivatePwd(): String? { - return mmkv.getString("ssl_private_pwd", null) - } - - fun sslAlias(): String? { - return mmkv.getString("ssl_alias", null) - } - - fun getSslPort(): Int { - return mmkv.getInt("ssl_port", getPort()) - } - - fun isDev(): Boolean { - return mmkv.getBoolean("dev", false) - } - - operator fun set(key: String, value: String) { - mmkv.putString(key, value) - } - - operator fun set(key: String, value: Boolean) { - mmkv.putBoolean(key, value) - } - - operator fun set(key: String, value: Int) { - mmkv.putInt(key, value) - } - - operator fun set(key: String, value: Long) { - mmkv.putLong(key, value) - } - - operator fun set(key: String, value: Float) { - mmkv.putFloat(key, value) - } - - fun isAntiTrace(): Boolean { - return config.antiTrace - } - - fun allowShell(): Boolean { - return mmkv.getBoolean("shell", false) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/BotStatus.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/BotStatus.kt deleted file mode 100644 index 6443de6..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/BotStatus.kt +++ /dev/null @@ -1,30 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class BotStatus( - val self: Self, - val online: Boolean, - val good: Boolean, - @SerialName("qq.status") - val status: String -) - -@Serializable -data class Self( - val platform: String, - @SerialName("user_id") - val userId: Long -) - -@Serializable -data class UserDetail( - @SerialName("user_id") - val userId: Long, - @SerialName("user_name") - val userName: String, - @SerialName("user_displayname") - val userDisplayName: String -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/FriendData.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/FriendData.kt deleted file mode 100644 index 51fbc1e..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/FriendData.kt +++ /dev/null @@ -1,37 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class FriendEntry( - @SerialName("user_id") val id: Long = 0, - @SerialName("user_name") val name: String?, - @SerialName("user_displayname") val displayName: String?, - @SerialName("user_remark") val remark: String?, - val age: Int, - val gender: Byte, - @SerialName("group_id") val groupId: Int, - @SerialName("platform") val platformType: PlatformType, - @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?, - -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/GroupAnnouncement.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/GroupAnnouncement.kt deleted file mode 100644 index 402ef96..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/GroupAnnouncement.kt +++ /dev/null @@ -1,25 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class GroupAnnouncement ( - @SerialName("sender_id") val senderId: Long = 0, - @SerialName("publish_time") val publishTime: Long, - @SerialName("message") val message: GroupAnnouncementMessage, -) - -@Serializable -internal data class GroupAnnouncementMessage ( - @SerialName("text") val text: String, - @SerialName("images") val images: List, -) - -@Serializable -internal data class GroupAnnouncementMessageImage ( - @SerialName("height") val height: String, - @SerialName("width") val width: String, - @SerialName("id") val id: String, -) - diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/GroupHonor.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/GroupHonor.kt deleted file mode 100644 index 7cd58c9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/GroupHonor.kt +++ /dev/null @@ -1,39 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -internal const val HONOR_TALKATIVE = 1 -internal const val HONOR_GROUP_FIRE = 2 -internal const val HONOR_GROUP_FLAME = 4 -internal const val HONOR_NEWBIE = 5 -internal const val HONOR_HAPPY = 6 -internal const val HONOR_ACADEMIC_STAR = 7 -internal const val HONOR_TOP_STUDENT = 8 -internal const val HONOR_TOP_GOD = 9 -internal const val HONOR_LEADING = 10 -internal const val HONOR_NEWBIE_2 = 11 -internal const val HONOR_ATMOSPHERE = 12 -internal const val HONOR_GIFT = 13 - -@Serializable -data class GroupMemberHonor( - @SerialName("user_id") val userId: Long, - @SerialName("nickname") var nick: String, - @SerialName("avatar") val avatar: String, - @SerialName("day_count") val dayCount: Int, - @SerialName("id") val id: Int, - @SerialName("description") val desc: String, -) - -@Serializable -internal data class GroupAllHonor( - @SerialName("group_id") val groupId: Long, - @SerialName("current_talkative") val currentTalkActive: GroupMemberHonor?, - @SerialName("talkative_list") val talkativeList: List?, - @SerialName("performer_list") val performerList: List?, - @SerialName("legend_list") val legendList: List?, - @SerialName("strong_newbie_list") val strongNewbieList: List?, - @SerialName("emotion_list") val emotionList: List?, - @SerialName("all") val all: List?, -) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt deleted file mode 100644 index 9193729..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt +++ /dev/null @@ -1,70 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement - -@Serializable -internal data class MessageResult( - @SerialName("message_id") val msgId: Int, - @SerialName("time") val time: Long -) - -@Serializable -internal data class UploadForwardMessageResult( - @SerialName("res_id") val resId: String, - @SerialName("filename") val filename: String, - @SerialName("summary") val summary: String, - @SerialName("desc") val desc: String, -) - -@Serializable -internal data class SendForwardMessageResult( - @SerialName("message_id") val msgId: Int, - @SerialName("res_id") val resId: String, - @SerialName("forward_id") val forwardId: String = resId -) - -@Serializable -internal data class MessageDetail( - @SerialName("time") val time: Int, - @SerialName("message_type") val msgType: String, - @SerialName("message_id") val msgId: Int, - @SerialName("message_id_qq") val qqMsgId: Long, - @SerialName("message_seq") val msgSeq: Long, - @SerialName("real_id") val realId: Long, - @SerialName("sender") val sender: MessageSender, - @SerialName("message") val message: List>, - @SerialName("group_id") val groupId: Long = 0, - @SerialName("peer_id") val peerId: Long, - @SerialName("target_id") val targetId: Long = 0, -) - -@Serializable -internal data class GetForwardMsgResult( - @SerialName("messages") val msgs: List -) - -@Serializable -internal data class MessageSender( - @SerialName("user_id") val userId: Long, - @SerialName("nickname") val nickName: String, - @SerialName("sex") val sex: String, - @SerialName("age") val age: Int, - @SerialName("uid") val uid: String, - @SerialName("tiny_id") val tinyId: String, -) - -@Serializable -internal data class EssenceMessage( - @SerialName("sender_id") val senderId: Long, - @SerialName("sender_nick") val senderNick: String, - @SerialName("sender_time") val senderTime: Long, - @SerialName("operator_id") val operatorId: Long, - @SerialName("operator_nick") val operatorNick: String, - @SerialName("operator_time") val operatorTime: Long, - @SerialName("message_id") var messageId: Int, - @SerialName("message_seq") val messageSeq: Int, - @SerialName("real_id") val realId: Int, - @SerialName("message_content") val messageContent: JsonElement, -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Platform.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Platform.kt deleted file mode 100644 index da7e226..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Platform.kt +++ /dev/null @@ -1,68 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -internal enum class PlatformType { - ANDROID_PAD, - AOL_CHAOJIHUIYUAN, - AOL_HUIYUAN, - AOL_SQQ, - CAR, - HRTX_IPHONE, - HRTX_PC, - MC_3G, - MISRO_MSG, - MOBILE_ANDROID, - MOBILE_ANDROID_NEW, - MOBILE_HD, - MOBILE_HD_NEW, - MOBILE_IPAD, - MOBILE_IPAD_NEW, - MOBILE_IPHONE, - MOBILE_OTHER, - MOBILE_PC, - MOBILE_WINPHONE_NEW, - QQ_FORELDER, - QQ_SERVICE, - TV_QQ, - WIN8, - MAC, - WINDOWS, - PC_OR_MAC_OR_LINUX, - WATCH, - WINPHONE; - - companion object { - fun valueOf(termType: Int): PlatformType { - return when(termType) { - 68104 -> ANDROID_PAD - 73730 -> AOL_CHAOJIHUIYUAN - 73474 -> AOL_HUIYUAN - 69378 -> AOL_SQQ - 65806 -> CAR - 66566 -> HRTX_IPHONE - 66561 -> HRTX_PC - 65795 -> MC_3G - 69634 -> MISRO_MSG - 65799 -> MOBILE_ANDROID - 72450 -> MOBILE_ANDROID_NEW - 65805 -> MOBILE_HD - 71426 -> MOBILE_HD_NEW - 68361 -> MOBILE_IPAD - 72194 -> MOBILE_IPAD_NEW - 67586 -> MOBILE_IPHONE - 65794 -> MOBILE_OTHER - 65793 -> MOBILE_PC - 81154, 66818, 66831 -> MAC - 77313 -> WINDOWS - 83714 -> PC_OR_MAC_OR_LINUX - 72706 -> MOBILE_WINPHONE_NEW - 70922 -> QQ_FORELDER - 71170 -> QQ_SERVICE - 69130 -> TV_QQ - 69899 -> WIN8 - 65804 -> WINPHONE - 78082, 78096, 75023 -> WATCH - else -> MOBILE_OTHER - } - } - } -} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/ResourceData.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/ResourceData.kt deleted file mode 100644 index 211a1cb..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/ResourceData.kt +++ /dev/null @@ -1,9 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.Serializable - -@Serializable -internal data class OutResource( - val file: String, - val url: String -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/TicketData.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/TicketData.kt deleted file mode 100644 index b0d69c7..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/TicketData.kt +++ /dev/null @@ -1,17 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class Credentials( - @SerialName("token") val bkn: String = "", - @SerialName("cookies") val cookie: String = "", - @SerialName("bigdata_ticket") val bigDataTicket: BigDataTicket? = null -) - -@Serializable -data class BigDataTicket( - var key: String? = null, - var sig: String? = null -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/TroopData.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/TroopData.kt deleted file mode 100644 index b78f21f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/TroopData.kt +++ /dev/null @@ -1,70 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole - -@Serializable -internal data class SimpleTroopInfo( - @SerialName("group_id") val groupId: Long, - @SerialName("group_name") val groupName: String?, - @SerialName("group_remark") val groupRemark: String?, - @SerialName("group_uin") val groupUin: Long, - @SerialName("admins") val adminList: List, - @SerialName("class_text") val classText: String?, - @SerialName("is_frozen") val isFrozen: Boolean, - //@SerialName("troop_level") val troopLevel: String?, - @SerialName("max_member") val maxMember: Int, - @SerialName("member_num") val memNum: Int, - @SerialName("member_count") val memCount: Int, - @SerialName("max_member_count") val maxNum: Int, -) - -@Serializable -internal data class SimpleTroopMemberInfo( - @SerialName("user_id") val uin: Long, - @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, - @SerialName("user_displayname") val showName: String?, - @SerialName("card") val cardName: String?, - @SerialName("distance") val distance: Int, - @SerialName("honor") val honor: List, - @SerialName("join_time") val joinTime: Long, - @SerialName("last_active_time") val lastActiveTime: Long, - @SerialName("last_sent_time") val lastSentTime: Long, - @SerialName("unique_name") val uniqueName: String?, - @SerialName("area") val area: String, - @SerialName("level") val level: Int, - @SerialName("role") val role: MemberRole, - @SerialName("unfriendly") val unfriendly: Boolean, - @SerialName("card_changeable") val cardChangeable: Boolean, - @SerialName("shut_up_timestamp") val shutUpTimestamp: Long?, -) - -@Serializable -internal data class GroupRequest( - // InvitedRequest - @SerialName("request_id") val msgSeq: Long, - @SerialName("invitor_uin") val invitorUin: Long?, - @SerialName("invitor_nick") val invitorNick: String?, - @SerialName("group_id") val groupId: Long, - @SerialName("group_name") val groupName: String, - @SerialName("checked") val checked: Boolean, - @SerialName("actor") val actor: Long, - // JoinRequest - @SerialName("requester_uin") val requesterUin: Long, - @SerialName("requester_nick") val requesterNick: String, - @SerialName("message") val message: String, - @SerialName("flag") val flag: String, -) - -@Serializable -internal data class GroupSystemMessage( - @SerialName("invited_requests") var invited: List, - @SerialName("join_requests") var join: List, -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/VersionInfo.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/VersionInfo.kt deleted file mode 100644 index 480686f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/VersionInfo.kt +++ /dev/null @@ -1,18 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class VersionInfo( - @SerialName("app_full_name") - val appFullName: String, - @SerialName("app_name") - val appName: String, - @SerialName("app_version") - val appVersion: String, - val impl: String, - val version: String, - @SerialName("onebot_version") - val onebotVersion: String -) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/VipData.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/VipData.kt deleted file mode 100644 index 965268c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/VipData.kt +++ /dev/null @@ -1,22 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class VipInfo( - val type: VipType, - val level: Int, - @SerialName("vip_type") val vipType: Int, - @SerialName("template_id") val templateId: Long -) - -enum class VipType { - QQ_VIP, - SUPER_QQ, - SUPER_VIP, - QQ_VIDEO, - QQ_READING, - BIG_VIP, - YELLOW_VIP -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/profile/ProfileCard.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/profile/ProfileCard.kt deleted file mode 100644 index a58ebb1..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/profile/ProfileCard.kt +++ /dev/null @@ -1,37 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data.profile - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import moe.fuqiuluo.shamrock.remote.service.data.VipInfo - -@Serializable -data class ProfileCard( - @SerialName("user_id") val uin: Long, - @SerialName("user_name") val name: String, - @SerialName("user_displayname") val displayName: String?, - @SerialName("user_remark") val remark: String?, - @SerialName("mail") val mail: String?, - @SerialName("find_method") val findMethod: String?, - @SerialName("max_vote_cnt") val maxVoteCnt: Short, - @SerialName("have_vote_cnt") val haveVoteCnt: Short, - @SerialName("vip_list") val vipList: List, - @SerialName("hobby_entry") val hobbyEntry: String?, - @SerialName("level") val level: Int, - @SerialName("birthday") val birthday: Long, - @SerialName("login_day") val loginDay: Long, - @SerialName("vote_cnt") val voteCnt: Long, - @SerialName("qid") val qid: String, - @SerialName("is_school_verified") val schoolVerified: Boolean, - @SerialName("location") val location: Location, - @SerialName("cookie") val cookie: ByteArray, -) - -@Serializable -data class Location( - val city: String?, - val company: String?, - val country: String?, - val province: String?, - val hometown: String?, - val school: String? -) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/profile/ProfileProtocolConst.java b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/profile/ProfileProtocolConst.java deleted file mode 100644 index 202c4d0..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/profile/ProfileProtocolConst.java +++ /dev/null @@ -1,41 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data.profile; - -public interface ProfileProtocolConst { - public static final String CMD_GET_PROFILE_DETAIL = "OidbSvc.0x480_9_IMCore"; - public static final String CMD_SET_PROFILE_DETAIL = "OidbSvc.0x4ff_9_IMCore"; - public static final String KET_INTERESTS = "interests"; - public static final String KEY_AGE = "age"; - public static final String KEY_BIRTHDAY = "birthday"; - public static final String KEY_COLLEGE = "college"; - public static final String KEY_COMPANY = "company"; - public static final String KEY_CONSTELLATION = "key_constellation"; - public static final String KEY_EMAIL = "email"; - public static final String KEY_HOMETOWN = "hometown"; - public static final String KEY_HOMETOWN_DESC = "hometown_desc"; - public static final String KEY_LOCATION = "location"; - public static final String KEY_LOCATION_DESC = "location_desc"; - public static final String KEY_LOCATION_NAME = "location_name"; - public static final String KEY_NICK = "nick"; - public static final String KEY_PARSE_PROFILE_LOCATION = "parse_profile_location"; - public static final String KEY_PERSONAL_NOTE = "personalNote"; - public static final String KEY_PROFESSION = "profession"; - public static final String KEY_SEX = "sex"; - public static final String PARAM_ADD_FRIEND_SOURCE = "addFriendSource"; - public static final String PARAM_COME_FROM_TYPE = "comeFromType"; - public static final String PARAM_GET_CONTROL = "getControl"; - public static final String PARAM_IS_FRIEND = "isFriend"; - public static final String PARAM_LOGIN_SIG = "loginSig"; - public static final String PARAM_QZONE_FEED_TIMESTAMP = "qZoneFeedTimeStamp"; - public static final String PARAM_QZONE_SEED = "qZoneSeed"; - public static final String PARAM_REQ_0X5EB = "req0x5ebList"; - public static final String PARAM_REQ_EXTEND = "reqExtendFriend"; - public static final String PARAM_REQ_MEDAL = "reqMedalWall"; - public static final String PARAM_REQ_SERVICES = "reqServiceList"; - public static final String PARAM_REQ_TEMPLATE = "reqTemplate"; - public static final String PARAM_SEARCH_NAME = "searchName"; - public static final String PARAM_SECURE_SIG = "secureSig"; - public static final String PARAM_SELF_UIN = "selfUin"; - public static final String PARAM_TARGET_UIN = "targetUin"; - public static final String PARAM_TROOP_CODE = "troopCode"; - public static final String PARAM_TROOP_UIN = "troopUin"; -} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/MessageEvent.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/MessageEvent.kt deleted file mode 100644 index bc62ad8..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/MessageEvent.kt +++ /dev/null @@ -1,106 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data.push - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement - - -@Serializable -internal enum class MsgSubType { - /** - * 群聊子类型 - */ - @SerialName("normal") NORMAL, - @SerialName("anonymous") ANONYMOUS, - @SerialName("notice") NOTICE, - @SerialName("group_self") GroupSelf, - - /** - * 私聊子类型 - */ - @SerialName("group") GroupLess, - @SerialName("friend") Friend, - @SerialName("other") Other, - - @SerialName("channel") Channel -} - -@Serializable -internal enum class MsgType { - @SerialName("group") Group, - @SerialName("private") Private, - @SerialName("guild") Guild -} - -@Serializable -internal enum class PostType { - @SerialName("meta_event") Meta, - @SerialName("notice") Notice, - @SerialName("request") Request, - @SerialName("message") Msg, - @SerialName("message_sent") MsgSent, -} - -/** - * 不要使用继承的方式实现通用字段,那样会很难维护! - */ -@Serializable -internal data class MessageEvent ( - @SerialName("time") val time: Long, - @SerialName("self_id") val selfId: Long, - @SerialName("post_type") val postType: PostType, - @SerialName("message_type") val messageType: MsgType, - @SerialName("sub_type") val subType: MsgSubType, - @SerialName("message_id") val messageId: Int, - @SerialName("group_id") val groupId: Long = Long.MIN_VALUE, - @SerialName("guild_id") val guildId: String? = null, - @SerialName("channel_id") val channelId: String? = null, - @SerialName("target_id") val targetId: Long = Long.MIN_VALUE, - @SerialName("peer_id") val peerId: Long, - @SerialName("user_id") val userId: Long, - @SerialName("anonymous") val anonymous: Anonymous? = null, - @SerialName("message") val message: JsonElement, - @SerialName("raw_message") val rawMessage: String, - @SerialName("font") val font: Int, - @SerialName("sender") val sender: Sender, - @SerialName("temp_source") val tmpSource: Int = Int.MIN_VALUE, - @SerialName("from_nick") val fromNickName: String? = null -) - -enum class MessageTempSource(val id: Int) { - Group(0), - Consultation(1), - Seek(2), - QQMovie(3), - HotChat(4), - VerifyMsg(6), - Discussion(7), - Dating(8), - Contact(9), - Unknown(-1), -} - -@Serializable -internal data class Anonymous( - @SerialName("name") val name: String -) - -@Serializable -internal enum class MemberRole { - @SerialName("owner") Owner, - @SerialName("admin") Admin, - @SerialName("member") Member, - @SerialName("stranger") Stranger, - @SerialName("unknown") Unknown -} - -@Serializable -internal data class Sender( - @SerialName("user_id") val userId: Long, - @SerialName("nickname") val nickname: String, - @SerialName("card") val card: String, - @SerialName("role") val role: MemberRole?, - @SerialName("title") val title: String, - @SerialName("level") val level: String, - @SerialName("tiny_id") val tinyId: String = "0", -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/NoticeEvent.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/NoticeEvent.kt deleted file mode 100644 index 8287182..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/NoticeEvent.kt +++ /dev/null @@ -1,153 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data.push - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal enum class NoticeType { - @SerialName("group_ban") GroupBan, - @SerialName("group_admin") GroupAdminChange, - @SerialName("group_decrease") GroupMemDecrease, - @SerialName("group_increase") GroupMemIncrease, - @SerialName("group_recall") GroupRecall, - @SerialName("group_card") GroupCard, - @SerialName("essence") Essence, - @SerialName("friend_recall") FriendRecall, - @SerialName("notify") Notify, - @SerialName("group_upload") GroupUpload, - @SerialName("private_upload") PrivateUpload -} - -@Serializable -internal enum class RequestType { - @SerialName("friend") Friend, - @SerialName("group") Group, -} - -@Serializable -internal enum class NoticeSubType { - @SerialName("none") None, - - @SerialName("ban") Ban, - @SerialName("lift_ban") LiftBan, - - @SerialName("set") Set, - @SerialName("unset") UnSet, - - @SerialName("add") Add, - @SerialName("invite") Invite, - @SerialName("approve") Approve, - @SerialName("leave") Leave, - @SerialName("kick") Kick, - @SerialName("kick_me") KickMe, - - @SerialName("poke") Poke, - @SerialName("sign") Sign, - - - @SerialName("title") Title, - @SerialName("delete") Delete, - -} - -@Serializable -internal enum class RequestSubType { - @SerialName("none") None, - @SerialName("add") Add, - @SerialName("invite") Invite, -} - -/** - * 不要使用继承的方式实现通用字段,那样会很难维护! - */ -@Serializable -internal data class NoticeEvent( - @SerialName("time") val time: Long, - @SerialName("self_id") val selfId: Long, - @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 = 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 = 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, - - // 群名片 - @SerialName("card_new") val cardNew: String? = null, - @SerialName("card_old") val cardOld: String? = null, - - // 群头衔 - @SerialName("title") val title: String? = null, - - // 戳一戳 - @SerialName("poke_detail") val pokeDetail: PokeDetail? = null, - - // 群打卡 - @SerialName("sign_detail") val signDetail: SignDetail? = null, - - ) - -/** - * 不要使用继承的方式实现通用字段,那样会很难维护! - */ -@Serializable -internal data class RequestEvent( - @SerialName("time") val time: Long, - @SerialName("self_id") val selfId: Long, - @SerialName("post_type") val postType: PostType, - @SerialName("request_type") val type: RequestType, - @SerialName("sub_type") val subType: RequestSubType = RequestSubType.None, - @SerialName("group_id") val groupId: Long = -1, - @SerialName("user_id") val userId: Long = -1, - @SerialName("user_uid") val userUid: String = "", - @SerialName("comment") val comment: String = "", - @SerialName("flag") val flag: String? = null, -) - - -@Serializable -internal data class GroupFileMsg( - val id: String, - val name: String, - val size: Long, - val busid: Long, - val url: String, -) - -@Serializable -internal data class PrivateFileMsg( - val id: String, - val name: String, - val size: Long, - @SerialName("sub_id") val subId: String, - val url: String, - val expire: Long, -) - -@Serializable -internal data class PokeDetail ( - val action: String? = "戳了戳", - val suffix: String? = "", - @SerialName("action_img_url") - val actionImg: String? = "https://tianquan.gtimg.cn/nudgeaction/item/0/expression.jpg", -) - -@Serializable -internal data class SignDetail ( - val action: String? = "今日第1个打卡", - @SerialName("rank_img") - val rankImg: String? = "", -) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/PushMetaEvent.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/PushMetaEvent.kt deleted file mode 100644 index 1fdd3a9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/PushMetaEvent.kt +++ /dev/null @@ -1,32 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.data.push - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import moe.fuqiuluo.shamrock.remote.service.data.BotStatus - -@Serializable -internal enum class MetaEventType { - @SerialName("lifecycle") LifeCycle, - @SerialName("heartbeat") Heartbeat -} - -@Serializable -internal enum class MetaSubType { - @SerialName("enable") Enable, - @SerialName("disable") Disable, - @SerialName("connect") Connect, -} - -/** - * 不要使用继承的方式实现通用字段,那样会很难维护! - */ -@Serializable -internal data class PushMetaEvent( - @SerialName("time") val time: Long, - @SerialName("self_id") val selfId: Long, - @SerialName("post_type") val postType: PostType, - @SerialName("meta_event_type") val type: MetaEventType, - @SerialName("sub_type") val subType: MetaSubType, - @SerialName("status") val status: BotStatus, - @SerialName("interval") val interval: Long = 0, -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt deleted file mode 100644 index 864d528..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt +++ /dev/null @@ -1,582 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.remote.service.listener - -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.MessageTempHandler -import moe.fuqiuluo.qqinterface.servlet.msg.toCQCode -import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.db.MessageDB -import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter -import moe.fuqiuluo.qqinterface.servlet.transfile.RichMediaUploadHandler -import moe.fuqiuluo.shamrock.remote.service.data.push.MessageTempSource -import moe.fuqiuluo.shamrock.remote.service.data.push.PostType -import java.util.ArrayList -import java.util.Collections -import kotlin.collections.HashMap - -internal object AioListener : IKernelMsgListener { - override fun onRecvMsg(msgList: ArrayList) { - if (msgList.isEmpty()) return - - GlobalScope.launch { - msgList.forEach { - handleMsg(it) - } - } - } - - private suspend fun handleMsg(record: MsgRecord) { - try { - if (MessageTempHandler.notify(record)) return - if (record.msgSeq < 0) return - - val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) - - val peerId = when (record.chatType) { - MsgConstant.KCHATTYPEGUILD -> record.guildId - else -> record.peerUin.toString() - } - MessageHelper.saveMsgMapping( - hash = msgHash, - qqMsgId = record.msgId, - chatType = record.chatType, - subChatType = record.chatType, - peerId = peerId, - msgSeq = record.msgSeq.toInt(), - time = record.msgTime, - subPeerId = record.channelId ?: peerId - ) - - val rawMsg = record.elements.toCQCode(record.chatType, peerId, record.channelId ?: peerId) - if (rawMsg.isEmpty()) return - - if (ShamrockConfig.aliveReply() && rawMsg == "ping") { - 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) - //} - - when (record.chatType) { - MsgConstant.KCHATTYPEGROUP -> { - LogCenter.log("群消息(group = ${record.peerName}(${record.peerUin}), uin = ${record.senderUin}, id = $msgHash, seq = ${record.msgSeq}, msg = $rawMsg)") - ShamrockConfig.getGroupMsgRule()?.let { rule -> - if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return - if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return - } - - if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage( - record, record.elements, rawMsg, msgHash, postType - ) - ) { - LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN) - } - } - - MsgConstant.KCHATTYPEC2C -> { - LogCenter.log("私聊消息(private = ${record.senderUin}, id = [$msgHash | ${record.msgId} | ${record.msgSeq}], msg = $rawMsg)") - ShamrockConfig.getPrivateRule()?.let { rule -> - if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return - if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return - } - - if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage( - record, record.elements, rawMsg, msgHash, postType - ) - ) { - LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN) - } - } - - MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> { - if (!ShamrockConfig.allowTempSession()) return - - ShamrockConfig.getPrivateRule()?.let { rule -> - if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return - if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return - } - - var groupCode = 0L - var fromNick = "" - MsgSvc.getTempChatInfo(record.chatType, record.senderUid).onSuccess { - groupCode = it.groupCode.toLong() - fromNick = it.fromNick - } - - LogCenter.log("私聊临时消息(private = ${record.senderUin}, groupId=$groupCode, id = $msgHash, msg = $rawMsg)") - - if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage( - record, - record.elements, - rawMsg, - msgHash, - tempSource = MessageTempSource.Group, - postType = postType, - groupId = groupCode, - fromNick = fromNick - ) - ) { - LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN) - } - } - - MsgConstant.KCHATTYPEGUILD -> { - LogCenter.log("频道消息(guildId = ${record.guildId}, sender = ${record.senderUid}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)") - if (!GlobalEventTransmitter.MessageTransmitter - .transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType) - ) { - LogCenter.log("频道消息推送失败 -> MessageTransmitter", Level.WARN) - } - } - - else -> LogCenter.log("不支持PUSH事件: ${record.chatType}") - } - } catch (e: Throwable) { - LogCenter.log(e.stackTraceToString(), Level.WARN) - } - } - - override fun onMsgRecall(chatType: Int, peerId: String, msgId: Long) { - LogCenter.log("onMsgRecall($chatType, $peerId, $msgId)") - } - - override fun onAddSendMsg(record: MsgRecord) { - if (record.peerUin == TicketSvc.getLongUin()) return // 发给自己的消息不处理 - - GlobalScope.launch { - try { - val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) - val peerId = when (record.chatType) { - MsgConstant.KCHATTYPEGUILD -> record.guildId - else -> record.peerUin.toString() - } - MessageHelper.saveMsgMapping( - hash = msgHash, - qqMsgId = record.msgId, - chatType = record.chatType, - subChatType = record.chatType, - peerId = peerId, - msgSeq = record.msgSeq.toInt(), - time = record.msgTime, - subPeerId = record.channelId ?: peerId - ) - - LogCenter.log("预发送消息($msgHash | ${record.msgSeq} | ${record.msgId})") - } catch (e: Throwable) { - LogCenter.log(e.stackTraceToString(), Level.WARN) - } - } - } - - override fun onMsgInfoListUpdate(msgList: ArrayList?) { - msgList?.forEach { record -> - if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED - || record.sendStatus == MsgConstant.KSENDSTATUSSENDING - ) { - return@forEach - } - - GlobalScope.launch { - val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) - val peerId = when (record.chatType) { - MsgConstant.KCHATTYPEGUILD -> record.guildId - else -> record.peerUin.toString() - } - - val mapping = MessageHelper.getMsgMappingByHash(msgHash) - if (mapping == null) { - MessageHelper.saveMsgMapping( - hash = msgHash, - qqMsgId = record.msgId, - chatType = record.chatType, - subChatType = record.chatType, - peerId = peerId, - msgSeq = record.msgSeq.toInt(), - time = record.msgTime, - subPeerId = record.channelId ?: peerId - ) - } else { - LogCenter.log("Update message info from ${mapping.msgSeq} to ${record.msgSeq}", Level.INFO) - MessageDB.getInstance().messageMappingDao() - .updateMsgSeqByMsgHash(msgHash, record.msgSeq.toInt()) - } - - if (!ShamrockConfig.enableSelfMsg() - || record.senderUin != TicketSvc.getLongUin() - || record.peerUin == TicketSvc.getLongUin() - ) return@launch - - val rawMsg = record.elements.toCQCode(record.chatType, peerId, record.channelId ?: peerId) - if (rawMsg.isEmpty()) return@launch - LogCenter.log("自发消息(target = ${record.peerUin}, id = $msgHash, msg = $rawMsg)") - - when (record.chatType) { - MsgConstant.KCHATTYPEGROUP -> { - 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) - ) { - 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 - ) - ) { - LogCenter.log("自发私聊临时消息推送失败 -> MessageTransmitter", Level.WARN) - } - } - - else -> LogCenter.log("不支持SELF PUSH事件: ${record.chatType}") - } - } - } - } - - override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo) { - - } - - override fun onMsgAbstractUpdate(arrayList: ArrayList?) { - //arrayList?.forEach { - // LogCenter.log("onMsgAbstractUpdate($it)", Level.WARN) - //} - } - - override fun onRecvMsgSvrRspTransInfo( - j2: Long, - contact: Contact?, - i2: Int, - i3: Int, - str: String?, - bArr: ByteArray? - ) { - LogCenter.log("onRecvMsgSvrRspTransInfo($j2, $contact, $i2, $i3, $str)", Level.DEBUG) - } - - override fun onRecvS2CMsg(arrayList: ArrayList?) { - LogCenter.log("onRecvS2CMsg(${arrayList.toString()})", Level.DEBUG) - } - - override fun onRecvSysMsg(arrayList: ArrayList?) { - LogCenter.log("onRecvSysMsg(${arrayList.toString()})", Level.DEBUG) - } - - override fun onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {} - - override fun onBroadcastHelperProgerssUpdate(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {} - - override fun onChannelFreqLimitInfoUpdate( - contact: Contact?, - z: Boolean, - freqLimitInfo: FreqLimitInfo? - ) { - - } - - override fun onContactUnreadCntUpdate(unreadMap: HashMap>) { - // 推送未读消息数量 - } - - override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) { - LogCenter.log("onCustomWithdrawConfigUpdate: " + customWithdrawConfig.toString(), Level.DEBUG) - } - - override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList?, j2: Long) { - LogCenter.log("onDraftUpdate: " + contact.toString() + "|" + arrayList + "|" + j2.toString(), Level.DEBUG) - } - - override fun onEmojiDownloadComplete(emojiNotifyInfo: EmojiNotifyInfo?) { - - } - - override fun onEmojiResourceUpdate(emojiResourceInfo: EmojiResourceInfo?) { - - } - - override fun onFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { - - } - - override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { - - } - - override fun onFirstViewGroupGuildMapping(arrayList: ArrayList?) { - - } - - override fun onGrabPasswordRedBag( - i2: Int, - str: String?, - i3: Int, - recvdOrder: RecvdOrder?, - msgRecord: MsgRecord? - ) { - - } - - override fun onKickedOffLine(kickedInfo: KickedInfo?) { - LogCenter.log("onKickedOffLine($kickedInfo)") - } - - override fun onFileMsgCome(arrayList: ArrayList?) { - arrayList?.forEach { record -> - GlobalScope.launch { - when (record.chatType) { - MsgConstant.KCHATTYPEGROUP -> onGroupFileMsg(record) - MsgConstant.KCHATTYPEC2C -> onC2CFileMsg(record) - else -> LogCenter.log("不支持该来源的文件上传事件:${record}", Level.WARN) - } - } - } - } - - private suspend fun onC2CFileMsg(record: MsgRecord) { - val userId = record.senderUin - val fileMsg = record.elements.firstOrNull { - it.elementType == MsgConstant.KELEMTYPEFILE - }?.fileElement ?: kotlin.run { - LogCenter.log("消息为私聊文件消息但不包含文件消息,来自:${record.peerUin}", Level.WARN) - return - } - - val fileName = fileMsg.fileName - val fileSize = fileMsg.fileSize - val expireTime = fileMsg.expireTime ?: 0 - val fileId = fileMsg.fileUuid - val fileSubId = fileMsg.fileSubId ?: "" - val url = RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) - - if (!GlobalEventTransmitter.FileNoticeTransmitter - .transPrivateFileEvent(record.msgTime, userId, fileId, fileSubId, fileName, fileSize, expireTime, url) - ) { - LogCenter.log("私聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN) - } - } - - private suspend fun onGroupFileMsg(record: MsgRecord) { - val groupId = record.peerUin - val userId = record.senderUin - val fileMsg = record.elements.firstOrNull { - it.elementType == MsgConstant.KELEMTYPEFILE - }?.fileElement ?: kotlin.run { - LogCenter.log("消息为群聊文件消息但不包含文件消息,来自:${record.peerUin}", Level.WARN) - return - } - //val fileMd5 = fileMsg.fileMd5 - val fileName = fileMsg.fileName - val fileSize = fileMsg.fileSize - val uuid = fileMsg.fileUuid - val bizId = fileMsg.fileBizId - - val url = RichProtoSvc.getGroupFileDownUrl(record.peerUin, uuid, bizId) - - if (!GlobalEventTransmitter.FileNoticeTransmitter - .transGroupFileEvent(record.msgTime, userId, groupId, uuid, fileName, fileSize, bizId, url) - ) { - LogCenter.log("群聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN) - } - } - - override fun onRichMediaUploadComplete(notifyInfo: FileTransNotifyInfo) { - LogCenter.log({ "[BDH] 资源上传完成(${notifyInfo.trasferStatus}, ${notifyInfo.fileId}, ${notifyInfo.msgId}, ${notifyInfo.commonFileInfo})" }, Level.DEBUG) - RichMediaUploadHandler.notify(notifyInfo) - } - - override fun onRecvOnlineFileMsg(arrayList: ArrayList?) { - LogCenter.log(("onRecvOnlineFileMsg" + arrayList?.joinToString { ", " }), Level.DEBUG) - } - - override fun onRichMediaDownloadComplete(fileTransNotifyInfo: FileTransNotifyInfo) { - - } - - override fun onRichMediaProgerssUpdate(fileTransNotifyInfo: FileTransNotifyInfo) { - - } - - override fun onSearchGroupFileInfoUpdate(searchGroupFileResult: SearchGroupFileResult?) { - LogCenter.log("onSearchGroupFileInfoUpdate($searchGroupFileResult)", Level.DEBUG) - } - - override fun onGroupFileInfoAdd(groupItem: GroupItem?) { - LogCenter.log("onGroupFileInfoAdd: " + groupItem.toString(), Level.DEBUG) - } - - override fun onGroupFileInfoUpdate(groupFileListResult: GroupFileListResult?) { - LogCenter.log("onGroupFileInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG) - } - - override fun onGroupGuildUpdate(groupGuildNotifyInfo: GroupGuildNotifyInfo?) { - LogCenter.log("onGroupGuildUpdate: " + groupGuildNotifyInfo.toString(), Level.DEBUG) - } - - override fun onGroupTransferInfoAdd(groupItem: GroupItem?) { - LogCenter.log("onGroupTransferInfoAdd: " + groupItem.toString(), Level.DEBUG) - } - - override fun onGroupTransferInfoUpdate(groupFileListResult: GroupFileListResult?) { - LogCenter.log("onGroupTransferInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG) - } - - override fun onGuildInteractiveUpdate(guildInteractiveNotificationItem: GuildInteractiveNotificationItem?) { - - } - - override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) { - - } - - override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) { - - } - - override fun onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: DownloadRelateEmojiResultInfo?) { - - } - - override fun onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: HitRelatedEmojiWordsResult?) { - - } - - override fun onHitRelatedEmojiResult(relatedWordEmojiInfo: RelatedWordEmojiInfo?) { - - } - - override fun onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: ImportOldDbMsgNotifyInfo?) { - - } - - override fun onInputStatusPush(inputStatusInfo: InputStatusInfo?) { - - } - - override fun onLineDev(devList: ArrayList?) { - //LogCenter.log("onLineDev($arrayList)") - } - - override fun onLogLevelChanged(newLevel: Long) { - - } - - override fun onMsgBoxChanged(arrayList: ArrayList?) { - - } - - override fun onMsgDelete(contact: Contact?, arrayList: ArrayList?) { - - } - - override fun onMsgEventListUpdate(hashMap: HashMap>?) { - - } - - override fun onMsgInfoListAdd(arrayList: ArrayList?) { - - } - - override fun onMsgQRCodeStatusChanged(i2: Int) { - - } - - override fun onMsgSecurityNotify(msgRecord: MsgRecord?) { - LogCenter.log("onMsgSecurityNotify($msgRecord)") - } - - override fun onMsgSettingUpdate(msgSetting: MsgSetting?) { - - } - - override fun onNtFirstViewMsgSyncEnd() { - - } - - override fun onNtMsgSyncEnd() { - LogCenter.log("NTKernel同步消息完成", Level.DEBUG) - } - - override fun onNtMsgSyncStart() { - LogCenter.log("NTKernel同步消息开始", Level.DEBUG) - } - - override fun onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { - - } - - override fun onRecvGroupGuildFlag(i2: Int) { - - } - - override fun onRecvUDCFlag(i2: Int) { - LogCenter.log("onRecvUDCFlag($i2)", Level.DEBUG) - } - - override fun onSendMsgError(j2: Long, contact: Contact?, i2: Int, str: String?) { - LogCenter.log("onSendMsgError($j2, $contact, $j2, $str)", Level.DEBUG) - } - - override fun onSysMsgNotification(i2: Int, j2: Long, j3: Long, arrayList: ArrayList?) { - LogCenter.log("onSysMsgNotification($i2, $j2, $j3, $arrayList)", Level.DEBUG) - } - - override fun onUnreadCntAfterFirstView(hashMap: HashMap>?) { - - } - - override fun onUnreadCntUpdate(hashMap: HashMap>?) { - - } - - override fun onUserChannelTabStatusChanged(z: Boolean) { - - } - - override fun onUserOnlineStatusChanged(z: Boolean) { - - } - - override fun onUserTabStatusChanged(arrayList: ArrayList?) { - LogCenter.log("onUserTabStatusChanged($arrayList)", Level.DEBUG) - } - - override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) { - - } - - override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) { - - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/GroupEventListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/GroupEventListener.kt deleted file mode 100644 index 9f9d7c0..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/GroupEventListener.kt +++ /dev/null @@ -1,136 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.remote.service.listener - -import com.tencent.qqnt.kernel.nativeinterface.BulletinFeedsDownloadInfo -import com.tencent.qqnt.kernel.nativeinterface.DataSource -import com.tencent.qqnt.kernel.nativeinterface.GroupBulletin -import com.tencent.qqnt.kernel.nativeinterface.GroupBulletinListResult -import com.tencent.qqnt.kernel.nativeinterface.GroupDetailInfo -import com.tencent.qqnt.kernel.nativeinterface.GroupListUpdateType -import com.tencent.qqnt.kernel.nativeinterface.GroupMemberInfoListId -import com.tencent.qqnt.kernel.nativeinterface.GroupMemberListChangeInfo -import com.tencent.qqnt.kernel.nativeinterface.GroupMsgMaskInfo -import com.tencent.qqnt.kernel.nativeinterface.GroupNotifyMsg -import com.tencent.qqnt.kernel.nativeinterface.GroupSimpleInfo -import com.tencent.qqnt.kernel.nativeinterface.GroupStatisticInfo -import com.tencent.qqnt.kernel.nativeinterface.IKernelGroupListener -import com.tencent.qqnt.kernel.nativeinterface.JoinGroupNotifyMsg -import com.tencent.qqnt.kernel.nativeinterface.MemberInfo -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import moe.fuqiuluo.qqinterface.servlet.GroupSvc -import moe.fuqiuluo.shamrock.helper.LogCenter -import java.util.ArrayList -import java.util.HashMap - -internal object GroupEventListener: IKernelGroupListener { - override fun onGetGroupBulletinListResult( - j2: Long, - str: String?, - groupBulletinListResult: GroupBulletinListResult? - ) { - LogCenter.log("onGetGroupBulletinListResult($j2, $str, $groupBulletinListResult)") - } - - override fun onGroupAvatarUrlChange(j2: Long, str: String?) { - LogCenter.log("onGroupAvatarUrlChange($j2, $str)") - } - - override fun onGroupBulletinChange(j2: Long, groupBulletin: GroupBulletin?) { - LogCenter.log("onGroupBulletinChange($j2, $groupBulletin)") - } - - override fun onGroupBulletinRichMediaDownloadComplete(bulletinFeedsDownloadInfo: BulletinFeedsDownloadInfo?) { - LogCenter.log("onGroupBulletinRichMediaDownloadComplete($bulletinFeedsDownloadInfo)") - } - - override fun onGroupBulletinRichMediaProgressUpdate(bulletinFeedsDownloadInfo: BulletinFeedsDownloadInfo?) { - LogCenter.log("onGroupBulletinRichMediaProgressUpdate($bulletinFeedsDownloadInfo)") - } - - override fun onGroupConfMemberChange(j2: Long, arrayList: ArrayList?) { - LogCenter.log("onGroupConfMemberChange($j2, $arrayList)") - } - - override fun onGroupDetailInfoChange(groupDetailInfo: GroupDetailInfo?) { - LogCenter.log("onGroupDetailInfoChange($groupDetailInfo)") - } - - override fun onGroupListUpdate( - groupListUpdateType: GroupListUpdateType?, - arrayList: ArrayList? - ) { - LogCenter.log("onGroupListUpdate($groupListUpdateType, $arrayList)") - } - - override fun onGroupNotifiesUnreadCountUpdated(z: Boolean, j2: Long, i2: Int) { - LogCenter.log("onGroupNotifiesUnreadCountUpdated($z, $j2, $i2)") - } - - override fun onGroupNotifiesUpdated(z: Boolean, arrayList: ArrayList?) { - LogCenter.log("onGroupNotifiesUpdated($z, $arrayList)") - } - - override fun onGroupPortraitChange( - j2: Long, - arrayList: ArrayList?, - arrayList2: ArrayList? - ) { - LogCenter.log("onGroupPortraitChange($j2, $arrayList, $arrayList2)") - } - - override fun onGroupSingleScreenNotifies( - z: Boolean, - j2: Long, - arrayList: ArrayList? - ) { - LogCenter.log("onGroupSingleScreenNotifies($z, $j2, $arrayList)") - } - - override fun onGroupStatisticInfoChange(j2: Long, groupStatisticInfo: GroupStatisticInfo?) { - LogCenter.log("onGroupStatisticInfoChange($j2, $groupStatisticInfo)") - } - - override fun onGroupsMsgMaskResult(arrayList: ArrayList?) { - LogCenter.log("onGroupsMsgMaskResult($arrayList)") - } - - override fun onJoinGroupNotify(joinGroupNotifyMsg: JoinGroupNotifyMsg?) { - LogCenter.log("onJoinGroupNotify($joinGroupNotifyMsg)") - } - - override fun onMemberInfoChange( - groupCode: Long, - dataSource: DataSource, - hashMap: HashMap - ) { - /*GlobalScope.launch { - hashMap.values.forEach { memberInfo -> - GroupSvc.getTroopMemberInfoByUid(groupCode, memberInfo.uid).onSuccess { - LogCenter.log("onMemberInfoChange($groupCode, $dataSource, $it, $memberInfo)") - }.onFailure { - LogCenter.log("onMemberInfoChange($groupCode, $dataSource, $it, $memberInfo)") - } - } - }*/ - } - - override fun onMemberListChange(groupMemberListChangeInfo: GroupMemberListChangeInfo?) { - LogCenter.log("onMemberListChange($groupMemberListChangeInfo)") - } - - override fun onSearchMemberChange( - str: String?, - str2: String?, - arrayList: ArrayList?, - hashMap: HashMap? - ) { - LogCenter.log("onSearchMemberChange($str, $str2, $arrayList, $hashMap)") - } - - override fun onShutUpMemberListChanged(j2: Long, arrayList: ArrayList?) { - LogCenter.log("onShutUpMemberListChanged($j2, $arrayList)") - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/KernelGuildListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/KernelGuildListener.kt deleted file mode 100644 index 5785004..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/KernelGuildListener.kt +++ /dev/null @@ -1,753 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.listener - -import com.tencent.qqnt.kernel.nativeinterface.GProAVChannelConfig -import com.tencent.qqnt.kernel.nativeinterface.GProAVRoomOptPushInfo -import com.tencent.qqnt.kernel.nativeinterface.GProAVUserStateChangeInfo -import com.tencent.qqnt.kernel.nativeinterface.GProCategoryChannelIdList -import com.tencent.qqnt.kernel.nativeinterface.GProChannel -import com.tencent.qqnt.kernel.nativeinterface.GProChannelState -import com.tencent.qqnt.kernel.nativeinterface.GProChannelUserChangeInfo -import com.tencent.qqnt.kernel.nativeinterface.GProClientIdentity -import com.tencent.qqnt.kernel.nativeinterface.GProCreateGuildGuideInfo -import com.tencent.qqnt.kernel.nativeinterface.GProDailyRecommendPush -import com.tencent.qqnt.kernel.nativeinterface.GProDiscoveryStateChangedMsg -import com.tencent.qqnt.kernel.nativeinterface.GProGlobalBanner -import com.tencent.qqnt.kernel.nativeinterface.GProGuild -import com.tencent.qqnt.kernel.nativeinterface.GProGuildData -import com.tencent.qqnt.kernel.nativeinterface.GProGuildInit -import com.tencent.qqnt.kernel.nativeinterface.GProGuildListSortInfo -import com.tencent.qqnt.kernel.nativeinterface.GProGuildMemberCountInfo -import com.tencent.qqnt.kernel.nativeinterface.GProGuildRole -import com.tencent.qqnt.kernel.nativeinterface.GProGuildSpeakableThreshold -import com.tencent.qqnt.kernel.nativeinterface.GProGuildStateRspInfo -import com.tencent.qqnt.kernel.nativeinterface.GProGuildUserProfile -import com.tencent.qqnt.kernel.nativeinterface.GProHeartbeatRsq -import com.tencent.qqnt.kernel.nativeinterface.GProLiveRoomInfo -import com.tencent.qqnt.kernel.nativeinterface.GProMedal -import com.tencent.qqnt.kernel.nativeinterface.GProMember -import com.tencent.qqnt.kernel.nativeinterface.GProNoticeRedPoint -import com.tencent.qqnt.kernel.nativeinterface.GProPollingChannelState -import com.tencent.qqnt.kernel.nativeinterface.GProPollingData -import com.tencent.qqnt.kernel.nativeinterface.GProPreventAddictionPushInfo -import com.tencent.qqnt.kernel.nativeinterface.GProProgramInfo -import com.tencent.qqnt.kernel.nativeinterface.GProQQMsgListChannel -import com.tencent.qqnt.kernel.nativeinterface.GProQQMsgListGuild -import com.tencent.qqnt.kernel.nativeinterface.GProRecommendGuildInfo -import com.tencent.qqnt.kernel.nativeinterface.GProRecommendGuildPersonalSetting -import com.tencent.qqnt.kernel.nativeinterface.GProRoleMemberChangeInfo -import com.tencent.qqnt.kernel.nativeinterface.GProSchemeConfig -import com.tencent.qqnt.kernel.nativeinterface.GProScreenShareUser -import com.tencent.qqnt.kernel.nativeinterface.GProSecurityResult -import com.tencent.qqnt.kernel.nativeinterface.GProSendGiftEventData -import com.tencent.qqnt.kernel.nativeinterface.GProStickyChannel -import com.tencent.qqnt.kernel.nativeinterface.GProTopMsg -import com.tencent.qqnt.kernel.nativeinterface.GProUser -import com.tencent.qqnt.kernel.nativeinterface.GProUserChannelState -import com.tencent.qqnt.kernel.nativeinterface.GProUserGiftRankInfo -import com.tencent.qqnt.kernel.nativeinterface.GProVoiceSmobaGameRoomManageSysMsg -import com.tencent.qqnt.kernel.nativeinterface.GProVoiceSmobaGameUserActionPush -import com.tencent.qqnt.kernel.nativeinterface.GProWorldState -import com.tencent.qqnt.kernel.nativeinterface.GProYLGameTeamInfo -import com.tencent.qqnt.kernel.nativeinterface.IKernelGuildListener -import moe.fuqiuluo.shamrock.helper.LogCenter -import java.util.ArrayList -import java.util.HashMap - -internal object KernelGuildListener: IKernelGuildListener { - override fun onAVChannelThemeUpdate( - j2: Long, - j3: Long, - gProAVChannelConfig: GProAVChannelConfig? - ) { - - } - - override fun onAVUserInfoChangeNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onAllGuildChannelListFetchCompleted() { - - } - - override fun onAnchorStatusChange(j2: Long, j3: Long, i2: Int) { - - } - - override fun onAppChannelPreInfosUpdated( - j2: Long, - arrayList: ArrayList?, - arrayList2: ArrayList? - ) { - - } - - override fun onAppInfosUpdated() { - - } - - override fun onBatchChannelListUpdated(arrayList: ArrayList?) { - - } - - override fun onBatchFetchRoleListUpdate(j2: Long, arrayList: ArrayList?) { - - } - - override fun onBroadcastRoomClose(j2: Long, i2: Int, str: String?) { - - } - - override fun onBroadcastUserCountUpdate(j2: Long, i2: Int) { - - } - - override fun onChangeGuildNumber(j2: Long, str: String?, i2: Int) { - - } - - override fun onChannelInfoUpdated(i2: Int, str: String?, gProChannel: GProChannel?) { - - } - - override fun onChannelListUpdated( - i2: Int, - str: String?, - j2: Long, - i3: Int, - hashMap: HashMap?, - gProCategoryChannelIdList: GProCategoryChannelIdList?, - arrayList: ArrayList? - ) { - - } - - override fun onChannelUserPermissionChange(j2: Long, arrayList: ArrayList?) { - - } - - override fun onDiscoveryStateChanged(gProDiscoveryStateChangedMsg: GProDiscoveryStateChangedMsg?) { - - } - - override fun onEnterSpeakQueueNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onGetSelfTinyId(j2: Long) { - - } - - override fun onGlobalBannerRemoved(gProGlobalBanner: GProGlobalBanner?) { - - } - - override fun onGlobalBannerUpdated(gProGlobalBanner: GProGlobalBanner?) { - - } - - override fun onGuildCreatorGuideUpdated(gProCreateGuildGuideInfo: GProCreateGuildGuideInfo?) { - - } - - override fun onGuildInfoUpdated(i2: Int, str: String?, gProGuild: GProGuild?, z: Boolean) { - - } - - override fun onGuildListUpdated( - retcode: Int, - why: String?, - force: Boolean, - guildIdList: ArrayList?, - guildList: ArrayList?, - sortInfo: GProGuildListSortInfo? - ) { - - } - - override fun onGuildUserAvatarMetasUpdated(j2: Long, arrayList: ArrayList?) { - - } - - override fun onGuildUserAvatarPendantsUpdated(hashMap: HashMap?) { - - } - - override fun onGuildUserChannelCategoryAdminListUpdated(j2: Long, arrayList: ArrayList?) { - - } - - override fun onGuildUserClientIdentitiesUpdated(j2: Long, arrayList: ArrayList?) { - - } - - override fun onGuildUserLevelRolesChanged(j2: Long, hashMap: HashMap?) { - - } - - override fun onGuildUserMedalsUpdated(hashMap: HashMap?) { - - } - - override fun onGuildUserMemberNamesUpdated(j2: Long, hashMap: HashMap?) { - - } - - override fun onGuildUserNicknamesUpdated(hashMap: HashMap?) { - - } - - override fun onGuildUserTopRolesChanged(j2: Long, arrayList: ArrayList?) { - - } - - override fun onLocalMemberCountUpdate(j2: Long, j3: Long) { - - } - - override fun onMemberCountUpdate(gProGuildMemberCountInfo: GProGuildMemberCountInfo?) { - - } - - override fun onNativeUpdateSimpleProfileByMsg( - j2: Long, - j3: Long, - j4: Long, - j5: Long, - j6: Long, - j7: Long, - i2: Int, - str: String?, - str2: String?, - str3: String?, - gProClientIdentity: GProClientIdentity?, - j8: Long, - str4: String?, - gProMedal: GProMedal? - ) { - - } - - override fun onNoticeListUpdate(gProNoticeRedPoint: GProNoticeRedPoint?, bArr: ByteArray?) { - - } - - override fun onOpenTelemetryMetricCountReport( - str: String?, - hashMap: HashMap?, - z: Boolean - ) { - - } - - override fun onOpenTelemetryMetricTimeCostReport( - str: String?, - hashMap: HashMap?, - j2: Long - ) { - - } - - override fun onOpenTelemetryTraceReport(str: String?, hashMap: HashMap?) { - - } - - override fun onPollingResult(arrayList: ArrayList?) { - - } - - override fun onPollingYLGameTeamInfo(gProYLGameTeamInfo: GProYLGameTeamInfo?) { - - } - - override fun onPushAVChannelAppMsg(str: String?) { - - } - - override fun onPushAVChannelConfigUpdate( - j2: Long, - j3: Long, - gProAVChannelConfig: GProAVChannelConfig? - ) { - - } - - override fun onPushAVChannelPlayListChange( - j2: Long, - j3: Long, - str: String?, - i2: Int, - str2: String? - ) { - - } - - override fun onPushAVHeartbeatRsp(i2: Int, str: String?, gProHeartbeatRsq: GProHeartbeatRsq?) { - - } - - override fun onPushAVRoomOptChange(gProAVRoomOptPushInfo: GProAVRoomOptPushInfo?) { - - } - - override fun onPushAVUserStateChange(gProAVUserStateChangeInfo: GProAVUserStateChangeInfo?) { - - } - - override fun onPushAddChannelSpeakPermission(j2: Long, j3: Long) { - - } - - override fun onPushAdminChanged(j2: Long, z: Boolean, j3: Long, j4: Long) { - - } - - override fun onPushAllowScreenShareInGuild(j2: Long, j3: Long, z: Boolean, j4: Long) { - - } - - override fun onPushAudioChannelUserEnter(j2: Long, j3: Long, gProUser: GProUser?, j4: Long) { - - } - - override fun onPushAudioChannelUserExit(j2: Long, j3: Long, j4: Long, j5: Long) { - - } - - override fun onPushAudioChannelUserPlatSwitch( - j2: Long, - j3: Long, - j4: Long, - i2: Int, - i3: Int, - str: String?, - j5: Long - ) { - - } - - override fun onPushBannedStatusChanged( - j2: Long, - z: Boolean, - z2: Boolean, - z3: Boolean, - j3: Long - ) { - - } - - override fun onPushBatchJoinChannel(j2: Long, arrayList: ArrayList?) { - - } - - override fun onPushBatchLeaveChannel(j2: Long, arrayList: ArrayList?) { - - } - - override fun onPushBussinessConfig(i2: Int, bArr: ByteArray?) { - - } - - override fun onPushCanceledSpeak(gProAVUserStateChangeInfo: GProAVUserStateChangeInfo?) { - - } - - override fun onPushChangeRoleMember(j2: Long, arrayList: ArrayList?) { - - } - - override fun onPushChannelAdminChange( - j2: Long, - j3: Long, - i2: Int, - arrayList: ArrayList? - ) { - - } - - override fun onPushChannelCategoryChanged(gProGuildInit: GProGuildInit?) { - - } - - override fun onPushChannelCreated(j2: Long, j3: Long, gProGuildInit: GProGuildInit?) { - - } - - override fun onPushChannelDestroy(j2: Long, arrayList: ArrayList?, j3: Long) { - - } - - override fun onPushChannelStateChange(arrayList: ArrayList?) { - - } - - override fun onPushChannelTopMsgUpdated( - j2: Long, - j3: Long, - j4: Long, - arrayList: ArrayList?, - arrayList2: ArrayList? - ) { - - } - - override fun onPushChannelVisibleChanged(j2: Long, j3: Long, i2: Int, i3: Int) { - - } - - override fun onPushCreateGuild( - j2: Long, - gProGuild: GProGuild?, - hashMap: HashMap?, - gProCategoryChannelIdList: GProCategoryChannelIdList?, - arrayList: ArrayList? - ) { - - } - - override fun onPushCreateRole(j2: Long, arrayList: ArrayList?) { - - } - - override fun onPushDeleteRole(j2: Long, arrayList: ArrayList?) { - - } - - override fun onPushDestroyGuild(j2: Long) { - - } - - override fun onPushDirectMsgSwitchUpdate(i2: Int) { - - } - - override fun onPushGuildPermissionChanged(j2: Long, arrayList: ArrayList?) { - - } - - override fun onPushGuildStateChange( - i2: Int, - str: String?, - gProGuildStateRspInfo: GProGuildStateRspInfo? - ) { - - } - - override fun onPushInvitationRefused(j2: Long, j3: Long, j4: Long, str: String?) { - - } - - override fun onPushInviteMemberEvent( - j2: Long, - j3: Long, - j4: Long, - gProSchemeConfig: GProSchemeConfig? - ) { - - } - - override fun onPushInvitedToSpeak(gProAVUserStateChangeInfo: GProAVUserStateChangeInfo?) { - - } - - override fun onPushJoinChannel(j2: Long, j3: Long, arrayList: ArrayList?) { - - } - - override fun onPushJoinGuild( - j2: Long, - j3: Long, - j4: Long, - i2: Int, - gProGuild: GProGuild?, - hashMap: HashMap?, - gProCategoryChannelIdList: GProCategoryChannelIdList?, - arrayList: ArrayList?, - i3: Int - ) { - - } - - override fun onPushJoinGuildFail(j2: Long, j3: Long, i2: Int, i3: Int, str: String?) { - - } - - override fun onPushKickOffGuild(j2: Long, j3: Long, j4: Long, i2: Int) { - - } - - override fun onPushKickOutAudioChannel( - j2: Long, - j3: Long, - j4: Long, - j5: Long, - i2: Int, - str: String?, - i3: Int, - j6: Long - ) { - - } - - override fun onPushLeaveChannel(j2: Long, j3: Long, arrayList: ArrayList?) { - - } - - override fun onPushLiveChannelAnchorIdentityChange(j2: Long, j3: Long, i2: Int) { - - } - - override fun onPushLiveRoomInfoChange(j2: Long, j3: Long, str: String?, str2: String?) { - - } - - override fun onPushLiveRoomStatusChangeMsg( - j2: Long, - j3: Long, - j4: Long, - str: String?, - j5: Long, - i2: Int, - i3: Int, - gProProgramInfo: GProProgramInfo?, - j6: Long, - gProLiveRoomInfo: GProLiveRoomInfo? - ) { - - } - - override fun onPushMemberTopRoleChanged(j2: Long, j3: Long, gProGuildRole: GProGuildRole?) { - - } - - override fun onPushMemberTopRoleInChannelChanged( - j2: Long, - j3: Long, - j4: Long, - gProGuildRole: GProGuildRole? - ) { - - } - - override fun onPushModifyRole(j2: Long, j3: Long, gProGuildRole: GProGuildRole?) { - - } - - override fun onPushMsgRecvTypeChanged(j2: Long, j3: Long, j4: Long, i2: Int) { - - } - - override fun onPushNotifySwitchUpdate(j2: Long, j3: Long, i2: Int) { - - } - - override fun onPushPermissionInGuild(j2: Long, j3: Long, z: Boolean, j4: Long) { - - } - - override fun onPushPreventAddictionInstructions(gProPreventAddictionPushInfo: GProPreventAddictionPushInfo?) { - - } - - override fun onPushQuitGuild(j2: Long, j3: Long) { - - } - - override fun onPushRemoveChannelSpeakPermission(j2: Long, j3: Long) { - - } - - override fun onPushSecurityResult(gProSecurityResult: GProSecurityResult?) { - - } - - override fun onPushSelfBannedSpeakChange(j2: Long, j3: Long, j4: Long, i2: Int) { - - } - - override fun onPushSendGiftEventNotify(gProSendGiftEventData: GProSendGiftEventData?) { - - } - - override fun onPushShutUpStateChanged(j2: Long, j3: Long) { - - } - - override fun onPushSortRole(j2: Long) { - - } - - override fun onPushSwitchLiveRoom( - j2: Long, - j3: Long, - arrayList: ArrayList?, - i2: Int - ) { - - } - - override fun onPushUserChannelStateChange(gProUserChannelState: GProUserChannelState?) { - - } - - override fun onPushUserGiftRankChangeNotify(gProUserGiftRankInfo: GProUserGiftRankInfo?) { - - } - - override fun onPushUserHandUpResult(gProAVUserStateChangeInfo: GProAVUserStateChangeInfo?) { - - } - - override fun onPushUserMuteSeatInGuild(j2: Long, j3: Long, z: Boolean, j4: Long) { - - } - - override fun onPushUserScreenShare( - j2: Long, - j3: Long, - arrayList: ArrayList?, - bArr: ByteArray? - ) { - - } - - override fun onPushWorldStateChange(gProWorldState: GProWorldState?) { - - } - - override fun onQQMsgListChannelDataReady() { - - } - - override fun onQQMsgListChannelUpdated( - arrayList: ArrayList?, - arrayList2: ArrayList? - ) { - - } - - override fun onQQMsgListGuildUpdated( - arrayList: ArrayList?, - arrayList2: ArrayList? - ) { - - } - - override fun onRecommendGuildChannelListUpdate(gProGuildData: GProGuildData?) { - - } - - override fun onRecommendGuildEntryUpdateNotify() { - - } - - override fun onRecommendGuildInfoUpdate(gProRecommendGuildInfo: GProRecommendGuildInfo?) { - - } - - override fun onRecommendGuildJumpChannelNotify(gProDailyRecommendPush: GProDailyRecommendPush?) { - - } - - override fun onRecommendGuildPersonalSettingUpdate(gProRecommendGuildPersonalSetting: GProRecommendGuildPersonalSetting?) { - - } - - override fun onRecommendGuildPollingResult(arrayList: ArrayList?) { - - } - - override fun onRecommendGuildStickyTopUpdated( - arrayList: ArrayList?, - arrayList2: ArrayList?, - arrayList3: ArrayList? - ) { - - } - - override fun onRefreshGuildUserProfileInfo( - i2: Int, - str: String?, - j2: Long, - j3: Long, - gProGuildUserProfile: GProGuildUserProfile? - ) { - - } - - override fun onReportSqliteError(i2: Int, str: String?) { - - } - - override fun onRobotStateChangeNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onSessionInitComplete(i2: Int, j2: Long) { - - } - - override fun onSmobaGameUserChangeNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onSpeakableThresholdUpdate( - arrayList: ArrayList?, - arrayList2: ArrayList? - ) { - - } - - override fun onStickyChannelUpdated( - j2: Long, - arrayList: ArrayList?, - arrayList2: ArrayList?, - arrayList3: ArrayList? - ) { - - } - - override fun onTabRedPointPollingResult(z: Boolean, str: String?) { - - } - - override fun onUserAVStateResetNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onUserEnterListNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onUserHandUpRequestNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onUserLeaveListNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onUserListFetchFinish(i2: Int, str: String?, j2: Long) { - - } - - override fun onUserSpeakingNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onUserWaitingToSpeakNotifyForAll(gProChannelUserChangeInfo: GProChannelUserChangeInfo?) { - - } - - override fun onVoiceHeartbeatTimeout(j2: Long, j3: Long) { - - } - - override fun onVoiceSmobaGameRooManageUpdate(gProVoiceSmobaGameRoomManageSysMsg: GProVoiceSmobaGameRoomManageSysMsg?) { - - } - - override fun onVoiceSmobaGameUserActionUpdate(gProVoiceSmobaGameUserActionPush: GProVoiceSmobaGameUserActionPush?) { - - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/NetworkListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/NetworkListener.kt deleted file mode 100644 index cc91791..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/NetworkListener.kt +++ /dev/null @@ -1,11 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.listener - -import com.tencent.qqnt.kernel.nativeinterface.IQQNTWrapperNetworkListener -import com.tencent.qqnt.kernel.nativeinterface.NetStatusType -import moe.fuqiuluo.shamrock.helper.LogCenter - -internal object NetworkListener: IQQNTWrapperNetworkListener { - override fun onNetworkStatusChanged(o: NetStatusType, n: NetStatusType) { - LogCenter.log("网络波动: $o -> $n") - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt deleted file mode 100644 index 1c5d4bb..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt +++ /dev/null @@ -1,625 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.service.listener - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.io.core.ByteReadPacket -import kotlinx.io.core.discardExact -import kotlinx.io.core.readBytes -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -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.ContactHelper -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter -import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeSubType -import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeType -import moe.fuqiuluo.shamrock.remote.service.data.push.RequestSubType -import moe.fuqiuluo.shamrock.tools.asJsonObject -import moe.fuqiuluo.shamrock.tools.asString -import moe.fuqiuluo.shamrock.tools.readBuf32Long -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.xposed.helper.PacketHandler -import moe.fuqiuluo.symbols.decodeProtobuf -import protobuf.message.ContentHead -import protobuf.message.MsgBody -import protobuf.message.ResponseHead -import protobuf.push.* - -internal object PrimitiveListener { - fun registerListener() { - PacketHandler.register("trpc.msg.olpush.OlPushService.MsgPush") { _, buffer -> - GlobalScope.launch { - try { - val push = buffer.slice(4).decodeProtobuf() - onMsgPush(push) - } catch (e: Exception) { - LogCenter.log(e.stackTraceToString(), Level.WARN) - } - } - } - } - - private suspend fun onMsgPush(push: MessagePush) { - if ( - push.msgBody == null || - push.msgBody!!.contentHead == null || - push.msgBody!!.body == null || - push.msgBody!!.contentHead!!.msgTime == null - ) return - val msgBody = push.msgBody!! - val contentHead = msgBody.contentHead!! - val msgType = contentHead.msgType - val subType = contentHead.msgSubType - val msgTime = contentHead.msgTime!! - val body = msgBody.body!! - try { - when (msgType) { - 33 -> onGroupMemIncreased(msgTime, body) - 34 -> onGroupMemberDecreased(msgTime, body) - 44 -> onGroupAdminChange(msgTime, body) - 82 -> onGroupMessage(msgTime, body) - 84 -> onGroupApply(msgTime, contentHead, body) - 87 -> onInviteGroup(msgTime, msgBody.msgHead!!, body) - 528 -> when (subType) { - 35 -> onFriendApply(msgTime, push.clientInfo!!, body) - 39 -> onCardChange(msgTime, body) - 68 -> onGroupApply(msgTime, contentHead, body) - 138 -> onC2CRecall(msgTime, body) - 290 -> onC2CPoke(msgTime, body) - } - - 732 -> when (subType) { - 12 -> onGroupBan(msgTime, body) - 16 -> onGroupUniqueTitleChange(msgTime, body) - 17 -> onGroupRecall(msgTime, body) - 20 -> onGroupCommonTips(msgTime, body) - 21 -> onEssenceMessage(msgTime, push.clientInfo, body) - } - } - } catch (e: Exception) { - LogCenter.log("onMsgPush(msgType: $msgType, subType: $subType): " + e.stackTraceToString(), Level.WARN) - } - } - - private fun onGroupMessage(msgTime: Long, body: MsgBody) { - runCatching { - - } - } - - private suspend fun onC2CPoke(msgTime: Long, body: MsgBody) { - val event = body.msgContent!!.decodeProtobuf() - if (event.params == null) return - - val params = event.params!!.associate { - it.key to it.value - } - - val target = params["uin_str2"] ?: return - val operation = params["uin_str1"] ?: return - val suffix = params["suffix_str"] ?: "" - val actionImg = params["action_img_url"] ?: "" - val action = params["alt_str1"] ?: "" - - LogCenter.log("私聊戳一戳: $operation $action $target $suffix") - - if (!GlobalEventTransmitter.PrivateNoticeTransmitter - .transPrivatePoke(msgTime, operation.toLong(), target.toLong(), action, suffix, actionImg) - ) { - LogCenter.log("私聊戳一戳推送失败!", Level.WARN) - } - } - - private suspend fun onFriendApply( - msgTime: Long, - clientInfo: MessagePushClientInfo, - body: MsgBody - ) { - val event = body.msgContent!!.decodeProtobuf() - if (event.head == null) return - val head = event.head!! - val applierUid = head.applierUid - val msg = head.applyMsg ?: "" - val source = head.source ?: "" - var applier = ContactHelper.getUinByUidAsync(applierUid).toLong() - if (applier == 0L) { - applier = clientInfo.liteHead?.sender?.toLong() ?: 0 - } - val src = head.srcId - val subSrc = head.subSrc - val flag: String = try { - val reqs = requestFriendSystemMsgNew(20, 0, 0) - val req = reqs?.first { - it.msg_time.get() == msgTime - } - val seq = req?.msg_seq?.get() - "$seq;$src;$subSrc;$applier" - } catch (err: Throwable) { - "$msgTime;$src;$subSrc;$applier" - } - LogCenter.log("来自$applier 的好友申请:$msg ($source)") - if (!GlobalEventTransmitter.RequestTransmitter - .transFriendApp(msgTime, applier, msg, flag) - ) { - LogCenter.log("好友申请推送失败!", Level.WARN) - } - } - - - private suspend fun onCardChange(msgTime: Long, body: MsgBody) { -// val event = runCatching { -// body.msgContent!!.decodeProtobuf() -// }.getOrElse { -// val readPacket = ByteReadPacket(body.msgContent!!) -// readPacket.readBuf32Long() -// readPacket.discardExact(1) -// -// readPacket.readBytes(readPacket.readShort().toInt()).also { -// readPacket.release() -// }.decodeProtobuf() -// } -// -// val targetId = detail[1, 13, 2].asUtf8String -// val newCardList = detail[1, 13, 3].asList -// var newCard = "" -// newCardList -// .value -// .forEach { -// if (it[1].asInt == 1) { -// newCard = it[2].asUtf8String -// } -// } -// val groupId = detail[1, 13, 4].asLong -// var oldCard = "" -// val targetQQ = ContactHelper.getUinByUidAsync(targetId).toLong() -// LogCenter.log("群组[$groupId]成员$targetQQ 群名片变动 -> $newCard") -// // oldCard暂时获取不到 -// if (!GlobalEventTransmitter.GroupNoticeTransmitter -// .transCardChange(msgTime, targetQQ, oldCard, newCard, groupId) -// ) { -// LogCenter.log("群名片变动推送失败!", Level.WARN) -// } - } - - private suspend fun onGroupUniqueTitleChange(msgTime: Long, body: MsgBody) { - val event = runCatching { - body.msgContent!!.decodeProtobuf() - }.getOrElse { - val readPacket = ByteReadPacket(body.msgContent!!) - readPacket.readBuf32Long() - readPacket.discardExact(1) - - readPacket.readBytes(readPacket.readShort().toInt()).also { - readPacket.release() - }.decodeProtobuf() - } - val groupId = event.groupCode.toLong() - val detail = event.uniqueTitleChangeDetail!!.first() - - //detail = if (detail[5] is ProtoList) { - // (detail[5] as ProtoList).value[0] - //} else { - // detail[5] - // } - - val targetUin = detail.targetUin.toLong() - - // 恭喜<{\"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.wording - if (titleChangeInfo.indexOf("群主授予") == -1) { - return - } - val titleJson = titleChangeInfo.split("获得群主授予的<")[1].replace(">头衔", "") - val titleJsonObj = Json.decodeFromString(titleJson).asJsonObject - val title = titleJsonObj["text"].asString - - LogCenter.log("群组[$groupId]成员$targetUin 获得群头衔 -> $title") - - if (!GlobalEventTransmitter.GroupNoticeTransmitter - .transTitleChange(msgTime, targetUin, title, groupId) - ) { - LogCenter.log("群头衔变动推送失败!", Level.WARN) - } - } - - private suspend fun onEssenceMessage( - msgTime: Long, - clientInfo: MessagePushClientInfo?, - body: MsgBody - ) { - if (clientInfo == null) return - val event = runCatching { - body.msgContent!!.decodeProtobuf() - }.getOrElse { - val readPacket = ByteReadPacket(body.msgContent!!) - readPacket.readBuf32Long() - readPacket.discardExact(1) - - readPacket.readBytes(readPacket.readShort().toInt()).also { - readPacket.release() - }.decodeProtobuf() - } - val groupId = event.groupCode.toLong() - val detail = event.essenceMsgInfo!!.first() - - val megSeq = event.msgSeq.toInt() - val senderUin = detail.sender.toLong() - val operatorUin = detail.operator.toLong() - val msgHashId = MessageHelper.getMsgMappingBySeq(MsgConstant.KCHATTYPEGROUP, groupId.toString(), megSeq).also { - if (it == null) { - LogCenter.log("精华消息变动推送失败!找不到消息映射关系!", Level.WARN) - return - } - }!!.msgHashId - - val subType = when (val type = detail.type) { - 1u -> { - LogCenter.log("群设精消息(groupId=$groupId, sender=$senderUin, msgId=$msgHashId, operator=$operatorUin)") - NoticeSubType.Add - } - - 2u -> { - LogCenter.log("群撤精消息(groupId=$groupId, sender=$senderUin, msgId=$msgHashId, operator=$operatorUin)") - NoticeSubType.Delete - } - - else -> error("onEssenceMessage unknown type: $type") - } - - if (!GlobalEventTransmitter.GroupNoticeTransmitter - .transEssenceChange(msgTime, senderUin, operatorUin, msgHashId, groupId, subType) - ) { - LogCenter.log("精华消息变动推送失败!", Level.WARN) - } - } - - - private suspend fun onGroupCommonTips(time: Long, body: MsgBody) { - val event = runCatching { - body.msgContent!!.decodeProtobuf() - }.getOrElse { - val readPacket = ByteReadPacket(body.msgContent!!) - readPacket.discardExact(4) - readPacket.discardExact(1) - - readPacket.readBytes(readPacket.readShort().toInt()).also { - readPacket.release() - }.decodeProtobuf() - } - val groupId = event.groupCode.toLong() - val detail = event.baseTips!!.first() - - val params = detail.params!!.associate { - it.key to it.value - } - - val target = params["uin_str2"] ?: params["mqq_uin"] ?: return - val operation = params["uin_str1"] ?: return - val suffix = params["suffix_str"] ?: "" - val actionImg = params["action_img_url"] ?: "" - val action = params["alt_str1"] - ?: params["action_str"] - ?: params["user_sign"] - ?: "" - val rankImg = params["rank_img"] ?: "" - - when (detail.type) { - 1061u -> { - LogCenter.log("群戳一戳($groupId): $operation $action $target $suffix") - if (!GlobalEventTransmitter.GroupNoticeTransmitter - .transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupId) - ) { - LogCenter.log("群戳一戳推送失败!", Level.WARN) - } - } - - 1068u -> { - 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.type}", Level.WARN) - } - } - } - - private suspend fun onC2CRecall(time: Long, body: MsgBody) { - val event = body.msgContent!!.decodeProtobuf() - val head = event.head!! - - val operationUid = head.operator!! - val operator = ContactHelper.getUinByUidAsync(operationUid).toLong() - - val msgSeq = head.msgSeq - val tipText = head.wording?.wording ?: "" - - val mapping = MessageHelper.getMsgMappingBySeq(MsgConstant.KCHATTYPEC2C, operator.toString(), msgSeq.toInt()) - if (mapping == null) { - LogCenter.log("由于缺失消息映射关系,消息撤回事件无法推送!", Level.WARN) - return - } - - LogCenter.log("私聊消息撤回: $operator, seq = $msgSeq, hash = ${mapping.msgHashId}, tip = $tipText") - - if (!GlobalEventTransmitter.PrivateNoticeTransmitter - .transPrivateRecall(time, operator, mapping.msgHashId, tipText) - ) { - LogCenter.log("私聊消息撤回推送失败!", Level.WARN) - } - } - - private suspend fun onGroupMemIncreased(time: Long, body: MsgBody) { - val event = body.msgContent!!.decodeProtobuf() - val groupCode = event.groupCode - val targetUid = event.memberUid - val type = event.type - - GroupSvc.getGroupMemberList(groupCode, true).onFailure { - LogCenter.log("新成员加入刷新群成员列表失败: $groupCode", Level.WARN) - }.onSuccess { - LogCenter.log("新成员加入刷新群成员列表成功,群成员数量: ${it.size}", Level.INFO) - } - - val operatorUid = event.operatorUid - val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong() - val target = ContactHelper.getUinByUidAsync(targetUid).toLong() - LogCenter.log("群成员增加($groupCode): $target, type = $type") - - if (!GlobalEventTransmitter.GroupNoticeTransmitter - .transGroupMemberNumChanged( - time, - target, - targetUid, - groupCode, - operator, - operatorUid, - NoticeType.GroupMemIncrease, - when (type) { - 130 -> NoticeSubType.Approve - 131 -> NoticeSubType.Invite - else -> NoticeSubType.Approve - } - ) - ) { - LogCenter.log("群成员增加推送失败!", Level.WARN) - } - } - - private suspend fun onGroupMemberDecreased(time: Long, body: MsgBody) { - val event = body.msgContent!!.decodeProtobuf() - val groupCode = event.groupCode - val targetUid = event.memberUid - val type = event.type - val operatorUid = event.operatorUid - - GroupSvc.getGroupMemberList(groupCode, 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 - 131 -> NoticeSubType.Kick - 3 -> NoticeSubType.KickMe - else -> { - NoticeSubType.Kick - } - } - - LogCenter.log("群成员减少($groupCode): $target, type = $subtype ($type)") - - if (!GlobalEventTransmitter.GroupNoticeTransmitter - .transGroupMemberNumChanged( - time, - target, - targetUid, - groupCode, - operator, - operatorUid, - NoticeType.GroupMemDecrease, - subtype - ) - ) { - LogCenter.log("群成员减少推送失败!", Level.WARN) - } - } - - private suspend fun onGroupAdminChange(msgTime: Long, body: MsgBody) { - val event = body.msgContent!!.decodeProtobuf() - val groupCode = event.groupCode - if (event.operation == null) return - val operation = event.operation!! - if (operation.setInfo == null && operation.unsetInfo == null) return - - val isSetAdmin: Boolean - val targetUid: String - if (operation.setInfo == null) { - isSetAdmin = false - targetUid = operation.unsetInfo!!.targetUid!! - } else { - isSetAdmin = true - targetUid = operation.setInfo!!.targetUid!! - } - - val target = ContactHelper.getUinByUidAsync(targetUid).toLong() - LogCenter.log("群管理员变动($groupCode): $target, isSetAdmin = $isSetAdmin") - - if (!GlobalEventTransmitter.GroupNoticeTransmitter - .transGroupAdminChanged(msgTime, target, targetUid, groupCode, isSetAdmin) - ) { - LogCenter.log("群管理员变动推送失败!", Level.WARN) - } - } - - private suspend fun onGroupBan(msgTime: Long, body: MsgBody) { - val event = body.msgContent!!.decodeProtobuf() - val groupCode = event.groupCode.toLong() - val operatorUid = event.operatorUid - val wholeBan = event.target?.target?.targetUid == null - val targetUid = event.target?.target?.targetUid ?: "" - val rawDuration = event.target?.target?.rawDuration?.toInt() ?: 0 - - 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, subType, operator, operatorUid, target, targetUid, groupCode, duration) - ) { - LogCenter.log("群禁言推送失败!", Level.WARN) - } - } - - private suspend fun onGroupRecall(time: Long, body: MsgBody) { - val event = runCatching { - body.msgContent!!.decodeProtobuf() - }.getOrElse { - val readPacket = ByteReadPacket(body.msgContent!!) - readPacket.discardExact(4) - readPacket.discardExact(1) - readPacket.readBytes(readPacket.readShort().toInt()).also { - readPacket.release() - }.decodeProtobuf() - } - val groupCode = event.groupCode.toLong() - val detail = event.recallDetails!! - val operatorUid = detail.operatorUid - val targetUid = detail.msgInfo!!.senderUid - val msgSeq = detail.msgInfo!!.msgSeq.toLong() - val tipText = detail.wording?.wording ?: "" - val mapping = MessageHelper.getMsgMappingBySeq(MsgConstant.KCHATTYPEGROUP, groupCode.toString(), msgSeq.toInt()) - if (mapping == null) { - LogCenter.log("由于缺失消息映射关系(seq = $msgSeq),消息撤回事件无法推送!", Level.WARN) - return - } - val msgHash = mapping.msgHashId - val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong() - val target = ContactHelper.getUinByUidAsync(targetUid).toLong() - LogCenter.log("群消息撤回($groupCode): $operator -> $target, seq = $msgSeq, hash = $msgHash, tip = $tipText") - - if (!GlobalEventTransmitter.GroupNoticeTransmitter - .transGroupMsgRecall(time, operator, target, groupCode, msgHash, tipText) - ) { - LogCenter.log("群消息撤回推送失败!", Level.WARN) - } - } - - private suspend fun onGroupApply(time: Long, contentHead: ContentHead, body: MsgBody) { - when (contentHead.msgType) { - 84 -> { - val event = body.msgContent!!.decodeProtobuf() - val groupCode = event.groupCode - val applierUid = event.applierUid - val reason = event.applyMsg ?: "" - var applier = ContactHelper.getUinByUidAsync(applierUid).toLong() - if (applier == getLongUin()) { - return - } - val msgSeq = contentHead.msgSeq - val flag = try { - var reqs = requestGroupSystemMsgNew(10, 1) - val riskReqs = requestGroupSystemMsgNew(5, 2) - reqs = reqs + riskReqs - val req = reqs.firstOrNull { - it.msg_time.get() == time && it.msg?.group_code?.get() == groupCode - } - val seq = req?.msg_seq?.get() ?: time - if (applier == 0L) { - applier = req?.req_uin?.get() ?: 0L - } - "$seq;$groupCode;$applier" - } catch (err: Throwable) { - "$time;$groupCode;$applier" - } - LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq") - if (!GlobalEventTransmitter.RequestTransmitter - .transGroupApply(time, applier, applierUid, reason, groupCode, flag, RequestSubType.Add) - ) { - LogCenter.log("入群申请推送失败!", Level.WARN) - } - } - - 528 -> { - val event = body.msgContent!!.decodeProtobuf() - val groupCode = event.applyInfo?.groupCode ?: return - val applierUid = event.applyInfo?.applierUid ?: return - var applier = ContactHelper.getUinByUidAsync(applierUid).toLong() - if (applier == getLongUin()) { - return - } - if ((event.applyInfo?.type ?: return) < 3) { - // todo - return - } - val flag = try { - var reqs = requestGroupSystemMsgNew(10, 1) - val riskReqs = requestGroupSystemMsgNew(5, 2) - reqs = reqs + riskReqs - val req = reqs.firstOrNull() { - it.msg_time.get() == time - } - val seq = req?.msg_seq?.get() ?: time - if (applier == 0L) { - applier = req?.req_uin?.get() ?: 0L - } - "$seq;$groupCode;$applier" - } catch (err: Throwable) { - "$time;$groupCode;$applier" - } - LogCenter.log("邀请入群申请($groupCode): $applier") - if (!GlobalEventTransmitter.RequestTransmitter - .transGroupApply(time, applier, applierUid, "", groupCode, flag, RequestSubType.Add) - ) { - LogCenter.log("邀请入群申请推送失败!", Level.WARN) - } - } - } - } - - private suspend fun onInviteGroup(time: Long, msgHead: ResponseHead, body: MsgBody) { - val event = body.msgContent!!.decodeProtobuf() - val groupCode = event.groupCode - val invitorUid = event.inviterUid - val invitor = ContactHelper.getUinByUidAsync(invitorUid).toLong() - val uin = msgHead.receiver - LogCenter.log("邀请入群: $groupCode, 邀请者: \"$invitor\"") - val flag = try { - var reqs = requestGroupSystemMsgNew(10, 1) - val riskReqs = requestGroupSystemMsgNew(10, 2) - reqs = reqs + riskReqs - val req = reqs.firstOrNull { - it.msg_time.get() == time - } - val seq = req?.msg_seq?.get() ?: time - "$seq;$groupCode;$uin" - } catch (err: Throwable) { - "$time;$groupCode;$uin" - } - if (!GlobalEventTransmitter.RequestTransmitter - .transGroupApply(time, invitor, invitorUid, "", groupCode, flag, RequestSubType.Invite) - ) { - LogCenter.log("邀请入群推送失败!", Level.WARN) - } - } - -} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/CommonResult.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/CommonResult.kt deleted file mode 100644 index 89ece55..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/CommonResult.kt +++ /dev/null @@ -1,56 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.structures - -import kotlinx.serialization.Contextual -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement - -enum class Status( - val code: Int -) { - Ok(0), - BadRequest(10001), - ErrorToken(403), - UnsupportedAction(10002), - BadParam(10003), - UnsupportedParam(10004), - UnsupportedSegment(10005), - UnsupportedSegmentData(10007), - BadSegmentData(10006), - WhoAmI(10101), - UnknownSelf(10102), - BadHandler(20001), - InternalHandlerError(20002), - DatabaseError(31000), - FilesystemError(32000), - NetworkError(33000), - PlatformError(34000), - LogicError(35000), - IAmTired(36000), -} - -@Serializable -data class CommonResult( - var status: String, - var retcode: Int, - @Contextual - var data: T, - var message: String = "", - var echo: JsonElement? = null -) - -@Serializable -object EmptyObject - -internal inline fun resultToString( - isOk: Boolean, - code: Status, - data: T, - msg: String = "", - echo: JsonElement -): String { - return Json.encodeToString( - CommonResult(if (isOk) "ok" else "failed", code.code, data, msg, echo) - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/CurrentAccount.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/CurrentAccount.kt deleted file mode 100644 index f62c34b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/CurrentAccount.kt +++ /dev/null @@ -1,17 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.structures - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class CurrentAccount( - var uin: Long, - var isLogin: Boolean, - var nick: String = "" -) - -@Serializable -data class StdAccount( - @SerialName("user_id") var userId: Long, - @SerialName("nickname") var nick: String = "" -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/ErrorCatch.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/ErrorCatch.kt deleted file mode 100644 index add48fd..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/ErrorCatch.kt +++ /dev/null @@ -1,9 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.structures - -import kotlinx.serialization.Serializable - -@Serializable -data class ErrorCatch( - var url: String, - var error: String -) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/IndexData.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/IndexData.kt deleted file mode 100644 index 6f15f9b..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/IndexData.kt +++ /dev/null @@ -1,11 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.structures - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class IndexData( - var version: String, - var startTime: Long, - @SerialName("http_version") var httpVersion: String -) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/Protocol.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/Protocol.kt deleted file mode 100644 index b660b46..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/Protocol.kt +++ /dev/null @@ -1,43 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.structures - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Protocol( - var processName: String, - - var subAppId: Long, - var qua: String, - var ntVersion: Int, - - var msfConnNetType: Int, - - var qimei: String, - var svnVersion: String, - - var androidId: String, - var guid: String, - var ksid: String, - var netType: Int, - - - var pingVersion: Byte, - var ssoVer: Int, - - var ssoVersion: Int, - var dbVersion: Int, - - var SSOVer: Int, - var tgtgtVer: Int, - - var androidDevInfo: String, - - @SerialName("signDtConfig") var qSignDtConfig: QSignDtConfig? = null -) - -@Serializable -data class QSignDtConfig( - val en: Array, - val de: Array -) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/SendMsgResult.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/SendMsgResult.kt deleted file mode 100644 index 976185a..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/structures/SendMsgResult.kt +++ /dev/null @@ -1,8 +0,0 @@ -package moe.fuqiuluo.shamrock.remote.structures - -data class SendMsgResult( - val msgHashId: Int, - val qqMsgId: Long, - var msgTime: Long, - var isTimeout: Boolean = false -) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/AndroidX.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/AndroidX.kt new file mode 100644 index 0000000..52d99ad --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/AndroidX.kt @@ -0,0 +1,16 @@ +package moe.fuqiuluo.shamrock.tools + +import android.content.Context +import android.os.Handler +import android.widget.Toast +import de.robv.android.xposed.XposedBridge + +lateinit var GlobalUi: Handler + +internal fun Context.toast(msg: String, flag: Int = Toast.LENGTH_SHORT) { + XposedBridge.log(msg) + if (!::GlobalUi.isInitialized) { + return + } + GlobalUi.post { Toast.makeText(this, msg, flag).show() } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt index f04a36a..a44278b 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt @@ -1,6 +1,5 @@ package moe.fuqiuluo.shamrock.tools -import io.github.xn32.json5k.Json5 import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement @@ -27,15 +26,6 @@ val GlobalJson = Json { coerceInputValues = true // 强制输入值 } - -val GlobalJson5 = Json5 { - prettyPrint = true - indentationWidth = 2 - //useSingleQuotes = true - //quoteMemberNames = true - //encodeDefaults = true -} - val String.asJson: JsonElement get() = Json.parseToJsonElement(this) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/KtorServer.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/KtorServer.kt deleted file mode 100644 index a88d101..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/KtorServer.kt +++ /dev/null @@ -1,366 +0,0 @@ -package moe.fuqiuluo.shamrock.tools - -import io.ktor.http.ContentType -import io.ktor.http.Parameters -import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call -import io.ktor.server.request.contentType -import io.ktor.server.request.receiveParameters -import io.ktor.server.request.receiveText -import io.ktor.server.response.respond -import io.ktor.server.routing.Routing -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.util.AttributeKey -import io.ktor.util.pipeline.PipelineContext -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.jsonObject -import moe.fuqiuluo.shamrock.helper.ParamsException -import io.ktor.http.HttpMethod -import io.ktor.http.parseUrlEncodedParameters -import io.ktor.server.request.httpMethod -import io.ktor.server.routing.route -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.shamrock.remote.structures.CommonResult -import moe.fuqiuluo.shamrock.remote.structures.EmptyObject -import moe.fuqiuluo.shamrock.remote.structures.Status - -@DslMarker -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE) -@Retention(AnnotationRetention.BINARY) -@MustBeDocumented -annotation class ShamrockDsl - - -private val keyIsJson = AttributeKey("isJson") -private val keyJsonObject = AttributeKey("paramsJson") -private val keyJsonArray = AttributeKey("paramsJsonArray") -private val keyJsonElement = AttributeKey("paramsJsonElement") -private val keyParts = AttributeKey("paramsParts") - -suspend fun ApplicationCall.fetch(key: String): String { - val isPost = request.httpMethod == HttpMethod.Post - return if (isPost) { - fetchPost(key) - } else { - fetchGet(key) - } -} - -suspend fun ApplicationCall.fetchOrNull(key: String): String? { - val isPost = request.httpMethod == HttpMethod.Post - return if (isPost) { - fetchPostOrNull(key) - } else { - fetchGetOrNull(key) - } -} - -suspend fun ApplicationCall.fetchOrThrow(key: String): String { - val isPost = request.httpMethod == HttpMethod.Post - return if (isPost) { - fetchPostOrThrow(key) - } else { - fetchGetOrThrow(key) - } -} - -fun ApplicationCall.fetchGet(key: String): String { - return parameters[key]!! -} - -fun ApplicationCall.fetchGetOrNull(key: String): String? { - return parameters[key] -} - -fun ApplicationCall.fetchGetOrThrow(key: String): String { - return parameters[key] ?: throw ParamsException(key) -} - -suspend fun ApplicationCall.fetchPost(key: String): String { - return fetchPostOrNull(key)!! -} - -suspend fun ApplicationCall.fetchPostOrThrow(key: String): String { - return fetchPostOrNull(key) ?: throw ParamsException(key) -} - -fun ApplicationCall.isJsonData(): Boolean { - return ContentType.Application.Json == request.contentType() || ContentType.Application.ProblemJson == request.contentType() -} - -suspend fun ApplicationCall.fetchPostOrNull(key: String): String? { - if (attributes.contains(keyJsonObject)) { - return attributes[keyJsonObject][key].asStringOrNull - } - if (attributes.contains(keyParts)) { - return attributes[keyParts][key] - } - return kotlin.runCatching { - if (isJsonData()) { - Json.parseToJsonElement(receiveText()).jsonObject.also { - attributes.put(keyJsonObject, it) - attributes.put(keyIsJson, true) - }[key].asStringOrNull - } else if ( - ContentType.Application.FormUrlEncoded == request.contentType() - ) { - receiveParameters().also { - attributes.put(keyParts, it) - }[key] - } else { - receiveTextAsUnknown(key) - } - }.getOrElse { - throw IllegalArgumentException("JSON数据格式不合法") - } -} - -private suspend fun ApplicationCall.receiveTextAsUnknown(key: String): String? { - return receiveText().let { text -> - if (text.startsWith("{") && text.endsWith("}")) { - Json.parseToJsonElement(text).jsonObject.also { - attributes.put(keyJsonObject, it) - attributes.put(keyIsJson, true) - }[key].asStringOrNull - } else { - text.parseUrlEncodedParameters().also { - attributes.put(keyParts, it) - attributes.put(keyIsJson, false) - }[key] - } - } // receiveText -} - -suspend fun PipelineContext.fetch(key: String): String { - return call.fetch(key) -} - -suspend fun PipelineContext.fetchOrNull(key: String): String? { - return call.fetchOrNull(key) -} - -suspend fun PipelineContext.fetchOrThrow(key: String): String { - return call.fetchOrThrow(key) -} - -fun PipelineContext.fetchGet(key: String): String { - return call.parameters[key]!! -} - -fun PipelineContext.fetchGetOrNull(key: String): String? { - return call.parameters[key] -} - -fun PipelineContext.fetchGetOrThrow(key: String): String { - return call.parameters[key] ?: throw ParamsException(key) -} - - -suspend fun PipelineContext.fetchPost(key: String): String { - return fetchPostOrNull(key)!! -} - -suspend fun PipelineContext.fetchPostOrThrow(key: String): String { - return fetchPostOrNull(key) ?: throw ParamsException(key) -} - -fun PipelineContext.isJsonData(): Boolean { - return ContentType.Application.Json == call.request.contentType() - || (keyIsJson in call.attributes && call.attributes[keyIsJson]) - || (keyJsonElement in call.attributes) -} - -suspend fun PipelineContext.isJsonString(key: String): Boolean { - if (!isJsonData()) return true - val data = if (keyJsonObject in call.attributes) { - call.attributes[keyJsonObject] - } else { - Json.parseToJsonElement(call.receiveText()).jsonObject.also { - call.attributes.put(keyJsonObject, it) - call.attributes.put(keyIsJson, true) - } - } - return data[key] is JsonPrimitive -} - -suspend fun PipelineContext.isJsonObject(key: String): Boolean { - if (!isJsonData()) return false - val data = if (call.attributes.contains(keyJsonObject)) { - call.attributes[keyJsonObject] - } else { - Json.parseToJsonElement(call.receiveText()).jsonObject.also { - call.attributes.put(keyJsonObject, it) - } - } - return data[key] is JsonObject -} - -suspend fun PipelineContext.isJsonArray(key: String): Boolean { - if (!isJsonData()) return false - val data = if (call.attributes.contains(keyJsonObject)) { - call.attributes[keyJsonObject] - } else { - Json.parseToJsonElement(call.receiveText()).jsonObject.also { - call.attributes.put(keyJsonObject, it) - } - } - return data[key] is JsonArray -} - -suspend fun PipelineContext.fetchPostJsonString(key: String): String { - val data = if (call.attributes.contains(keyJsonObject)) { - call.attributes[keyJsonObject] - } else { - Json.parseToJsonElement(call.receiveText()).jsonObject.also { - call.attributes.put(keyJsonObject, it) - } - } - return data[key].asString -} - -suspend fun PipelineContext.fetchPostJsonElement(key: String): JsonElement { - val data = if (call.attributes.contains(keyJsonObject)) { - call.attributes[keyJsonObject] - } else { - Json.parseToJsonElement(call.receiveText()).jsonObject.also { - call.attributes.put(keyJsonObject, it) - } - } - return data[key]!! -} - -suspend fun PipelineContext.fetchPostJsonObject(key: String): JsonObject { - val data = if (call.attributes.contains(keyJsonObject)) { - call.attributes[keyJsonObject] - } else { - Json.parseToJsonElement(call.receiveText()).jsonObject.also { - call.attributes.put(keyJsonObject, it) - } - } - return data[key].asJsonObject -} - -suspend fun PipelineContext.fetchPostJsonObjectOrNull(key: String): JsonObject? { - val data = if (call.attributes.contains(keyJsonObject)) { - call.attributes[keyJsonObject] - } else { - Json.parseToJsonElement(call.receiveText()).jsonObject.also { - call.attributes.put(keyIsJson, true) - call.attributes.put(keyJsonObject, it) - } - } - return data[key].asJsonObjectOrNull -} - -suspend fun PipelineContext.fetchPostJsonElementOrNull(): JsonElement? { - return runCatching { - if (call.attributes.contains(keyJsonObject)) { - call.attributes[keyJsonObject] - } else if (call.attributes.contains(keyJsonArray)) { - call.attributes[keyJsonArray] - } else if (call.attributes.contains(keyJsonElement)) { - call.attributes[keyJsonElement] - } else { - Json.parseToJsonElement(call.receiveText()).also { - call.attributes.put(keyJsonElement, it) - if (it is JsonObject) { - call.attributes.put(keyJsonObject, it) - } else if (it is JsonArray) { - call.attributes.put(keyJsonArray, it) - } - } - } - }.getOrNull() -} - -suspend fun PipelineContext.fetchPostJsonArray(key: String): JsonArray { - val data = if (call.attributes.contains(keyJsonObject)) { - call.attributes[keyJsonObject] - } else { - Json.parseToJsonElement(call.receiveText()).jsonObject.also { - call.attributes.put(keyJsonObject, it) - call.attributes.put(keyIsJson, true) - } - } - return data[key].asJsonArray -} - -suspend fun PipelineContext.fetchPostOrNull(key: String): String? { - return call.fetchPostOrNull(key) -} - -@io.ktor.util.KtorDsl -fun Routing.getOrPost(path: String, body: suspend PipelineContext.(Unit) -> Unit) { - route(path) { - get(body) - post(body) - } -} - -@io.ktor.util.KtorDsl -fun Routing.getOrPost(path: Regex, body: suspend PipelineContext.(Unit) -> Unit) { - route(path) { - get(body) - post(body) - } -} - -@ShamrockDsl -internal suspend inline fun PipelineContext.respond( - isOk: Boolean, - code: Status, - msg: String = "", - echo: JsonElement = EmptyJsonString -) { - call.respond( - CommonResult( - if (isOk) "ok" else "failed", - code.code, - EmptyObject, - msg, - echo - ) - ) -} - -@ShamrockDsl -internal suspend inline fun PipelineContext.respond( - isOk: Boolean, - code: Status, - data: T, - msg: String = "", - echo: JsonElement = EmptyJsonString -) { - call.respond( - CommonResult( - if (isOk) "ok" else "failed", - code.code, - data, - msg, - echo - ) - ) -} - -@ShamrockDsl -internal suspend inline fun PipelineContext.respond( - isOk: Boolean, - code: Int, - data: T, - msg: String = "", - echo: JsonElement = EmptyJsonString -) { - call.respond( - CommonResult( - if (isOk) "ok" else "failed", - code, - data, - msg, - echo - ) - ) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/MutBroadcast.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/MutBroadcast.kt deleted file mode 100644 index 900bd0f..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/MutBroadcast.kt +++ /dev/null @@ -1,14 +0,0 @@ -package moe.fuqiuluo.shamrock.tools - -import android.content.Context -import android.content.Intent - -/** - * 指定向某个进程发送广播 - */ -internal fun Context.broadcast(processName: String, intentBuilder: Intent.() -> Unit) { - val intent = Intent() - intent.action = "moe.fuqiuluo.$processName.dynamic" - intent.intentBuilder() - sendBroadcast(intent) -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Version.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Version.kt index e5a1c38..8bc766b 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Version.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Version.kt @@ -2,9 +2,7 @@ package moe.fuqiuluo.shamrock.tools import mqq.app.MobileQQ -private val context = MobileQQ.getContext() -private val packageManager = context.packageManager - -private fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName, 0) - -val ShamrockVersion: String = getPackageInfo("moe.fuqiuluo.shamrock.hided").versionName +val ShamrockVersion: String by lazy { + MobileQQ.getContext().packageManager + .getPackageInfo("moe.fuqiuluo.shamrock.hided", 0).versionName +} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/AudioUtils.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/AudioUtils.kt index d7c07b1..f8783d0 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/AudioUtils.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/AudioUtils.kt @@ -8,7 +8,7 @@ import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFprobeKit import com.arthenica.ffmpegkit.ReturnCode import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil -import moe.fuqiuluo.shamrock.helper.LocalCacheHelper +import qq.service.contact.LocalCacheHelper import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import java.io.File diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt index 6bd1b3c..f763f97 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt @@ -1,32 +1,28 @@ package moe.fuqiuluo.shamrock.xposed import android.content.Context -import android.os.Process +import android.os.Build +import android.os.Handler import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.XposedBridge.log -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig import moe.fuqiuluo.shamrock.utils.MMKVFetcher -import moe.fuqiuluo.shamrock.xposed.loader.KeepAlive +import moe.fuqiuluo.shamrock.xposed.helper.KeepAlive import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader import moe.fuqiuluo.shamrock.tools.FuzzySearchClass +import moe.fuqiuluo.shamrock.tools.GlobalUi import moe.fuqiuluo.shamrock.tools.afterHook import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.hooks.runFirstActions import mqq.app.MobileQQ import java.lang.reflect.Field import java.lang.reflect.Modifier -import kotlin.system.exitProcess +import moe.fuqiuluo.shamrock.xposed.actions.runFirstActions private const val PACKAGE_NAME_QQ = "com.tencent.mobileqq" -private const val PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi" -private const val PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite" + private const val PACKAGE_NAME_TIM = "com.tencent.tim" -private val uselessProcess = listOf("peak", "tool", "mini", "qzone") internal class XposedEntry: IXposedHookLoadPackage { companion object { @@ -36,7 +32,6 @@ internal class XposedEntry: IXposedHookLoadPackage { var secStaticNativehookInited = false external fun injected(): Boolean - external fun hasEnv(): Boolean } @@ -140,22 +135,14 @@ internal class XposedEntry: IXposedHookLoadPackage { MMKVFetcher.initMMKV(ctx) } - runCatching { - if (ShamrockConfig.forbidUselessProcess()) { - if(uselessProcess.any { - processName.contains(it, ignoreCase = true) - }) { - log("[Shamrock] Useless process detected: $processName, exit.") - Process.killProcess(Process.myPid()) - exitProcess(0) - } - } else { - log("[Shamrock] Useless process detection is disabled.") - } - } - log("Process Name = $processName") + GlobalUi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + Handler.createAsync(ctx.mainLooper) + } else { + Handler(ctx.mainLooper) + } + runFirstActions(ctx) } } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/AntiDetection.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt similarity index 96% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/AntiDetection.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt index 507a3b1..d32c221 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/AntiDetection.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt @@ -1,5 +1,5 @@ @file:Suppress("UNCHECKED_CAST", "LocalVariableName") -package moe.fuqiuluo.shamrock.xposed.hooks +package moe.fuqiuluo.shamrock.xposed.actions import android.content.ContentResolver import android.content.Context @@ -10,9 +10,10 @@ import android.os.Looper import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XSharedPreferences import de.robv.android.xposed.XposedHelpers +import moe.fuqiuluo.shamrock.config.AntiJvmTrace +import moe.fuqiuluo.shamrock.config.ShamrockConfig import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig import moe.fuqiuluo.shamrock.tools.MethodHooker import moe.fuqiuluo.shamrock.tools.hookMethod import moe.fuqiuluo.shamrock.xposed.XposedEntry @@ -31,13 +32,12 @@ class AntiDetection: IAction { antiGetPackageGidsDetection(ctx) antiProviderDetection() antiNativeDetection() - if (ShamrockConfig.isAntiTrace()) + if (ShamrockConfig[AntiJvmTrace]) antiTrace() antiMemoryWalking() } private fun antiGetPackageGidsDetection(ctx: Context) { - //通过 android.content.pm.PackageManager->getPackageGids(Ljava/lang/String;)[I 扫 moe.fuqiuluo.shamrock ctx.packageManager::class.java.hookMethod("getPackageGids").before { val packageName = it.args[0] as String if (packageName == "moe.fuqiuluo.shamrock") { @@ -55,9 +55,6 @@ class AntiDetection: IAction { it.result = null LogCenter.log("AntiDetection: 检测到对Shamrock的检测,欺骗ContentResolver", Level.WARN) } - //else { - // LogCenter.log(uri) - //} } } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/DynamicBroadcast.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/DynamicBroadcast.kt new file mode 100644 index 0000000..20e5292 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/DynamicBroadcast.kt @@ -0,0 +1,44 @@ +package moe.fuqiuluo.shamrock.xposed.actions + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import de.robv.android.xposed.XposedBridge +import moe.fuqiuluo.shamrock.xposed.actions.interacts.SwitchStatus +import moe.fuqiuluo.shamrock.xposed.actions.interacts.Init +import moe.fuqiuluo.symbols.Process +import moe.fuqiuluo.symbols.XposedHook +import mqq.app.MobileQQ + +@XposedHook(priority = 1, process = Process.MAIN) +class DynamicBroadcast: IAction { + @SuppressLint("UnspecifiedRegisterReceiverFlag") + override fun invoke(ctx: Context) { + val intentFilter = IntentFilter() + intentFilter.addAction("moe.fuqiuluo.kritor.dynamic") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + MobileQQ.getMobileQQ().registerReceiver( + DynamicReceiver, intentFilter, + Context.RECEIVER_EXPORTED + ) + } else { + MobileQQ.getMobileQQ().registerReceiver(DynamicReceiver, intentFilter) + } + XposedBridge.log("Register Main::Broadcast successfully.") + } + + private object DynamicReceiver: BroadcastReceiver() { + private val handlers = mapOf( + "init" to Init, + "switch_status" to SwitchStatus + ) + + override fun onReceive(context: Context, intent: Intent) { + val cmd = intent.getStringExtra("__cmd") ?: "" + handlers[cmd]?.invoke(intent) + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/FetchService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FetchService.kt similarity index 92% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/FetchService.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FetchService.kt index cebe42a..c8cf092 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/FetchService.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FetchService.kt @@ -1,5 +1,5 @@ @file:OptIn(DelicateCoroutinesApi::class) -package moe.fuqiuluo.shamrock.xposed.hooks +package moe.fuqiuluo.shamrock.xposed.actions import android.content.Context import com.tencent.qqnt.kernel.api.IKernelService @@ -11,7 +11,7 @@ import moe.fuqiuluo.shamrock.tools.hookMethod import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher +import moe.fuqiuluo.shamrock.internals.NTServiceFetcher import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader import moe.fuqiuluo.symbols.XposedHook diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/FixLibraryLoad.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FixAudioLibraryLoader.kt similarity index 85% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/FixLibraryLoad.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FixAudioLibraryLoader.kt index 1eaa4ce..024021d 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/FixLibraryLoad.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FixAudioLibraryLoader.kt @@ -1,4 +1,4 @@ -package moe.fuqiuluo.shamrock.xposed.hooks +package moe.fuqiuluo.shamrock.xposed.actions import android.content.Context import moe.fuqiuluo.shamrock.tools.hookMethod @@ -6,8 +6,8 @@ import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader import moe.fuqiuluo.symbols.XposedHook @XposedHook(priority = 0) -internal class FixLibraryLoad: IAction { - val redirectedLibrary =arrayOf( +internal class FixAudioLibraryLoader: IAction { + private val redirectedLibrary =arrayOf( "ffmpegkit_abidetect", "avutil", "swscale", diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/ForceTablet.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/ForceTablet.kt similarity index 87% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/ForceTablet.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/ForceTablet.kt index 834bda6..0e49988 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/ForceTablet.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/ForceTablet.kt @@ -1,11 +1,12 @@ @file:Suppress("UNUSED_VARIABLE", "LocalVariableName") -package moe.fuqiuluo.shamrock.xposed.hooks +package moe.fuqiuluo.shamrock.xposed.actions import android.content.Context import com.tencent.common.config.pad.DeviceType import com.tencent.qqnt.kernel.nativeinterface.InitSessionConfig import de.robv.android.xposed.XposedBridge -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig +import moe.fuqiuluo.shamrock.config.ForceTablet +import moe.fuqiuluo.shamrock.config.ShamrockConfig import moe.fuqiuluo.shamrock.tools.FuzzySearchClass import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.helper.LogCenter @@ -17,8 +18,7 @@ import moe.fuqiuluo.symbols.XposedHook @XposedHook(priority = 0) internal class ForceTablet: IAction { override fun invoke(ctx: Context) { - //if (!PlatformUtils.isMqqPackage()) return - if (ShamrockConfig.forceTablet()) { + if (ShamrockConfig[ForceTablet]) { if (PlatformUtils.isMainProcess()) { LogCenter.log("强制协议类型 (PAD)", toast = true) } @@ -56,10 +56,6 @@ internal class ForceTablet: IAction { InitSessionConfig::class.java.hookMethod("getDeviceType").after { it.result = com.tencent.qqnt.kernel.nativeinterface.DeviceType.KPAD } - - //InitSessionConfig::class.java.hookMethod("getPlatform").after { - // it.result = PlatformType.KMAC - //} } } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/IAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/IAction.kt similarity index 69% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/IAction.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/IAction.kt index c5d1f5b..9cc39c4 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/IAction.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/IAction.kt @@ -1,4 +1,4 @@ -package moe.fuqiuluo.shamrock.xposed.hooks +package moe.fuqiuluo.shamrock.xposed.actions import android.content.Context diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt new file mode 100644 index 0000000..937c114 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt @@ -0,0 +1,15 @@ +@file:OptIn(DelicateCoroutinesApi::class) + +package moe.fuqiuluo.shamrock.xposed.actions + +import android.content.Context +import kotlinx.coroutines.DelicateCoroutinesApi +import moe.fuqiuluo.symbols.Process +import moe.fuqiuluo.symbols.XposedHook + +@XposedHook(Process.MAIN, priority = 10) +internal class InitRemoteService : IAction { + override fun invoke(ctx: Context) { + + } +} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/ListenShamrockUpdate.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/ListenShamrockUpdate.kt similarity index 96% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/ListenShamrockUpdate.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/ListenShamrockUpdate.kt index 23d9fc1..f8b0180 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/ListenShamrockUpdate.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/ListenShamrockUpdate.kt @@ -1,4 +1,4 @@ -package moe.fuqiuluo.shamrock.xposed.hooks +package moe.fuqiuluo.shamrock.xposed.actions import android.content.BroadcastReceiver import android.content.Context diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/NoBackGround.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/NoBackGround.kt similarity index 96% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/NoBackGround.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/NoBackGround.kt index a3cd741..7ff90a9 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/NoBackGround.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/NoBackGround.kt @@ -1,4 +1,4 @@ -package moe.fuqiuluo.shamrock.xposed.hooks +package moe.fuqiuluo.shamrock.xposed.actions import android.content.Context import de.robv.android.xposed.XposedHelpers diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PullConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PullConfig.kt new file mode 100644 index 0000000..609ada6 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PullConfig.kt @@ -0,0 +1,14 @@ +package moe.fuqiuluo.shamrock.xposed.actions + +import android.content.Context +import moe.fuqiuluo.shamrock.utils.PlatformUtils +import moe.fuqiuluo.symbols.Process +import moe.fuqiuluo.symbols.XposedHook + +@XposedHook(Process.MAIN, priority = 1) +class PullConfig: IAction { + override fun invoke(ctx: Context) { + if (!PlatformUtils.isMainProcess()) return + + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/IInteract.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/IInteract.kt new file mode 100644 index 0000000..b9fadff --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/IInteract.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.xposed.actions.interacts + +import android.content.Intent + +interface IInteract { + operator fun invoke(intent: Intent) +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/Init.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/Init.kt new file mode 100644 index 0000000..c430558 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/Init.kt @@ -0,0 +1,24 @@ +package moe.fuqiuluo.shamrock.xposed.actions.interacts + +import android.content.Context +import android.content.Intent +import moe.fuqiuluo.shamrock.config.ShamrockConfig +import moe.fuqiuluo.shamrock.tools.toast +import moe.fuqiuluo.shamrock.xposed.actions.runServiceActions +import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader +import mqq.app.MobileQQ + +internal object Init: IInteract { + private external fun testNativeLibrary(): String + + override fun invoke(intent: Intent) { + ShamrockConfig.updateConfig(intent) + initAppService(MobileQQ.getMobileQQ()) + } + + private fun initAppService(ctx: Context) { + NativeLoader.load("shamrock") + ctx.toast(testNativeLibrary()) + runServiceActions(ctx) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/SwitchStatus.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/SwitchStatus.kt new file mode 100644 index 0000000..aee1936 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/interacts/SwitchStatus.kt @@ -0,0 +1,19 @@ +package moe.fuqiuluo.shamrock.xposed.actions.interacts + +import android.content.Intent +import com.tencent.mobileqq.app.QQAppInterface +import moe.fuqiuluo.shamrock.tools.ShamrockVersion +import moe.fuqiuluo.shamrock.xposed.helper.AppTalker +import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader +import qq.service.QQInterfaces + +object SwitchStatus: IInteract, QQInterfaces() { + override fun invoke(intent: Intent) { + AppTalker.talk("switch_status") { + put("account", app.currentAccountUin) + put("nickname", if (app is QQAppInterface) app.currentNickname else "unknown") + put("voice", NativeLoader.isVoiceLoaded) + put("core_version", ShamrockVersion) + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppTalker.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppTalker.kt index 09e2bfc..63374e4 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppTalker.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppTalker.kt @@ -3,11 +3,10 @@ package moe.fuqiuluo.shamrock.xposed.helper import android.content.ContentValues import android.net.Uri import mqq.app.MobileQQ -import kotlin.random.Random internal object AppTalker { - val uriName = "content://moe.fuqiuluo.108.provider" // 你是真的闲,这都上个检测 - val URI = Uri.parse(uriName) + private const val uriName = "content://moe.fuqiuluo.108.provider" // 你是真的闲,这都上个检测 + private val URI = Uri.parse(uriName) fun talk(values: ContentValues, onFailure: ((Throwable) -> Unit)? = null) { val ctx = MobileQQ.getContext() @@ -17,4 +16,12 @@ internal object AppTalker { onFailure?.invoke(e) } } + + fun talk(action: String, bodyBuilder: ContentValues.() -> Unit) { + val values = ContentValues() + values.put("__cmd", action) + values.put("__hash", 0) + bodyBuilder.invoke(values) + talk(values) + } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/KeepAlive.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/KeepAlive.kt similarity index 91% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/KeepAlive.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/KeepAlive.kt index a61a39a..6b4f477 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/KeepAlive.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/KeepAlive.kt @@ -1,15 +1,12 @@ -@file:Suppress("UNUSED_VARIABLE", "LocalVariableName") -package moe.fuqiuluo.shamrock.xposed.loader +package moe.fuqiuluo.shamrock.xposed.helper import android.content.pm.ApplicationInfo import android.os.Build -import android.os.Process import de.robv.android.xposed.XSharedPreferences import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers import moe.fuqiuluo.shamrock.tools.hookMethod import java.lang.reflect.Method -import kotlin.concurrent.timer internal object KeepAlive { private val KeepPackage = arrayOf( @@ -30,9 +27,17 @@ internal object KeepAlive { private fun hookDoze(pref: XSharedPreferences, loader: ClassLoader) { if (pref.file.canRead() && pref.getBoolean("hook_doze", false)) { val result = runCatching { - val DeviceIdleController = XposedHelpers.findClass("com.android.server.DeviceIdleController", loader) + val DeviceIdleController = XposedHelpers.findClass( + "com.android.server.DeviceIdleController", + loader + ) ?: return@runCatching -1 - val becomeActiveLocked = XposedHelpers.findMethodBestMatch(DeviceIdleController, "becomeActiveLocked", String::class.java, Integer.TYPE) + val becomeActiveLocked = XposedHelpers.findMethodBestMatch( + DeviceIdleController, + "becomeActiveLocked", + String::class.java, + Integer.TYPE + ) ?: return@runCatching -2 if (!becomeActiveLocked.isAccessible) { becomeActiveLocked.isAccessible = true @@ -58,7 +63,8 @@ internal object KeepAlive { private fun hookAMS(pref: XSharedPreferences, loader: ClassLoader) { kotlin.runCatching { - val ActivityManagerService = XposedHelpers.findClass("com.android.server.am.ActivityManagerService", loader) + val ActivityManagerService = + XposedHelpers.findClass("com.android.server.am.ActivityManagerService", loader) ActivityManagerService.hookMethod("newProcessRecordLocked").after { increaseAdj(it.result) } @@ -147,7 +153,7 @@ internal object KeepAlive { val newState = clazz.getDeclaredField("mState").also { if (!it.isAccessible) it.isAccessible = true }.get(record) - val MethodSetMaxAdj = newState.javaClass.getDeclaredMethod("setMaxAdj", Int::class.java).also { + newState.javaClass.getDeclaredMethod("setMaxAdj", Int::class.java).also { if (!it.isAccessible) it.isAccessible = true }.invoke(newState, 1) }.onFailure { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/PacketHandler.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/PacketHandler.kt deleted file mode 100644 index 5326364..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/PacketHandler.kt +++ /dev/null @@ -1,59 +0,0 @@ -package moe.fuqiuluo.shamrock.xposed.helper - -import moe.fuqiuluo.shamrock.tools.broadcast -import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver -import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest -import mqq.app.MobileQQ - -internal object PacketHandler { - /* - MSF 进程包处理是否就绪 - */ - var isInit = false - - internal fun initPacketHandler() { - DynamicReceiver.register("msf_waiter", IPCRequest { - isInit = true - }) - } - - /** - * 注册常驻包处理器 - */ - fun register(cmd: String, callback: (Int, ByteArray) -> Unit) { - // 在本地广播接收器注册对应处理器 - DynamicReceiver.register(cmd, IPCRequest { - val buffer = it.getByteArrayExtra("buffer")!! - val seq = it.getIntExtra("seq", 0) - callback(seq, buffer) - }) - if (!isInit) return - // 向MSF进程广播要求添加处理器 - MobileQQ.getContext().broadcast("msf") { - putExtra("__cmd", "register_handler_cmd") - putExtra("handler_cmd", cmd) - } - } - - suspend fun registerLessHandler(cmd: String, seq: Int, callback: (Int, ByteArray) -> Unit): Int { - DynamicReceiver.register(IPCRequest(cmd, seq) { - val buffer = it.getByteArrayExtra("buffer")!! - val currSeq = it.getIntExtra("seq", 0) - callback(currSeq, buffer) - }) - return seq - } - - suspend fun unregisterLessHandler(seq: Int) { - DynamicReceiver.unregister(seq) - } - - fun unregister(cmd: String) { - DynamicReceiver.unregister(cmd) - if (!isInit) return - MobileQQ.getContext().broadcast("msf") { - putExtra("__cmd", "unregister_handler_cmd") - putExtra("handler_cmd", cmd) - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/internal/DataRequester.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/internal/DataRequester.kt deleted file mode 100644 index f2a66c9..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/internal/DataRequester.kt +++ /dev/null @@ -1,113 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.xposed.helper.internal - -import android.content.ContentValues -import android.content.Intent -import kotlinx.atomicfu.atomic -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import moe.fuqiuluo.shamrock.xposed.helper.AppTalker -import java.util.Timer -import kotlin.concurrent.timer - -/** - * 数据请求中心 - * 支持应用内数据传递,以及外部向内部传入 - */ -object DataRequester { - private val seqFactory = atomic(0) - private val seq: Int - get() { - if (seqFactory.value > 1000000) { - seqFactory.lazySet(0) - } - return seqFactory.incrementAndGet() - } - - suspend fun request( - cmd: String, - currSeq: Int = seq, - values: Map? = null, - onFailure: ((Throwable) -> Unit)? = null, - callback: ICallback? = null - ): Int { - return request(cmd, currSeq, bodyBuilder = { - values?.forEach { (key, value) -> - when (value) { - is Int -> this.put(key, value) - is Long -> this.put(key, value) - is Short -> this.put(key, value) - is Byte -> this.put(key, value) - is String -> this.put(key, value) - is ByteArray -> this.put(key, value) - is Boolean -> this.put(key, value) - is Float -> this.put(key, value) - is Double -> this.put(key, value) - } - } - }, onFailure, callback) - } - - suspend fun request( - cmd: String, - currentSeq: Int = seq, - bodyBuilder: (ContentValues.() -> Unit)? = null, - onFailure: ((Throwable) -> Unit)? = null, - callback: ICallback? = null - ): Int { - val values = ContentValues() - bodyBuilder?.invoke(values) - values.put("__hash", (cmd + currentSeq).hashCode()) - values.put("__cmd", cmd) - AppTalker.talk(values, onFailure) - if (callback != null) { - val timer: Timer = timer(initialDelay = 3000L, period = 5000L) { - GlobalScope.launch(Dispatchers.Default) { - DynamicReceiver.unregister(currentSeq) - cancel() - } - } - val request = IPCRequest(cmd, currentSeq, values) { - try { - timer.cancel() - } finally { - callback.handle(it) - } - } - DynamicReceiver.register(request) - } - return currentSeq - } -} - -fun interface ICallback { - suspend fun handle(intent: Intent) -} - -data class IPCRequest( - val cmd: String = "", - val seq: Int = -1, - val values: ContentValues? = null, - var callback: ICallback? = null, -) { - override fun hashCode(): Int { - return (cmd + seq).hashCode() - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as IPCRequest - - if (cmd != other.cmd) return false - if (seq != other.seq) return false - if (values != other.values) return false - if (callback != other.callback) return false - - return true - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/internal/DynamicReceiver.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/internal/DynamicReceiver.kt deleted file mode 100644 index aa04160..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/internal/DynamicReceiver.kt +++ /dev/null @@ -1,78 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) -package moe.fuqiuluo.shamrock.xposed.helper.internal - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter - -/** - * 动态广播 - */ -internal object DynamicReceiver: BroadcastReceiver() { - private val hashHandler = mutableSetOf() - private val cmdHandler = mutableMapOf() - private val mutex = Mutex() // 滥用的锁,尽量减少使用 - - override fun onReceive(ctx: Context, intent: Intent) { - GlobalScope.launch(Dispatchers.Default) { - val hash = intent.getIntExtra("__hash", -1) - val cmd = intent.getStringExtra("__cmd") ?: "" - try { - if (cmd.isNotBlank()) { - cmdHandler[cmd].also { - if (it == null) - LogCenter.log("无常驻包处理器: $cmd, main = ${PlatformUtils.isMainProcess()}", Level.ERROR) - }?.callback?.handle(intent) - } else if (hash != -1) { - mutex.withLock { - hashHandler.removeIf { - if (hash == it.hashCode()) { - GlobalScope.launch { - it.callback?.handle(intent) - } - return@removeIf it.seq != -1 - } - return@removeIf false - } - } - } - } catch (e: Throwable) { - LogCenter.log("包处理器[$cmd]错误: $e", Level.ERROR) - } - } - } - - fun register(cmd: String, request: IPCRequest) { - cmdHandler[cmd] = request - } - - fun unregister(cmd: String) { - cmdHandler.remove(cmd) - } - - /*** - * 注册临时包处理器 - */ - suspend fun register(request: IPCRequest) { - LogCenter.log({ "registerHandler[${request.hashCode()}](cmd = ${request.cmd}, seq = ${request.seq})" }, Level.DEBUG) - - mutex.withLock { - hashHandler.add(request) - } - } - - suspend fun unregister(seq: Int) { - mutex.withLock { - hashHandler.removeIf { it.seq == seq } - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/DataReceiver.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/DataReceiver.kt deleted file mode 100644 index fedd7b7..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/DataReceiver.kt +++ /dev/null @@ -1,67 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) -package moe.fuqiuluo.shamrock.xposed.hooks - -import android.annotation.SuppressLint -import android.content.Context -import android.content.IntentFilter -import android.os.Build -import android.os.Handler -import android.widget.Toast -import de.robv.android.xposed.XposedBridge -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver -import moe.fuqiuluo.symbols.XposedHook -import mqq.app.MobileQQ - -internal lateinit var GlobalUi: Handler - -internal fun Context.toast(msg: String, flag: Int = Toast.LENGTH_SHORT) { - XposedBridge.log(msg) - if (!::GlobalUi.isInitialized) { - return - } - GlobalUi.post { Toast.makeText(this, msg, flag).show() } -} - -@XposedHook(priority = 0) -internal class DataReceiver: IAction { - @SuppressLint("UnspecifiedRegisterReceiverFlag") - override fun invoke(ctx: Context) { - kotlin.runCatching { - MobileQQ.getMobileQQ().unregisterReceiver(DynamicReceiver) - } - - if (PlatformUtils.isMainProcess()) { - GlobalUi = Handler(ctx.mainLooper) - GlobalScope.launch { - val intentFilter = IntentFilter() - intentFilter.addAction("moe.fuqiuluo.xqbot.dynamic") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - MobileQQ.getMobileQQ().registerReceiver( - DynamicReceiver, intentFilter, - Context.RECEIVER_EXPORTED - ) - } else { - MobileQQ.getMobileQQ().registerReceiver(DynamicReceiver, intentFilter) - } - XposedBridge.log("Register Main::Broadcast successfully.") - } - } else if (PlatformUtils.isMsfProcess()) { - val intentFilter = IntentFilter() - intentFilter.addAction("moe.fuqiuluo.msf.dynamic") - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - MobileQQ.getMobileQQ().registerReceiver( - DynamicReceiver, intentFilter, - Context.RECEIVER_EXPORTED - ) - } else { - MobileQQ.getMobileQQ().registerReceiver(DynamicReceiver, intentFilter) - } - } - } -} - diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/GuidLock.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/GuidLock.kt deleted file mode 100644 index 54a36b3..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/GuidLock.kt +++ /dev/null @@ -1,75 +0,0 @@ -package moe.fuqiuluo.shamrock.xposed.hooks - -import android.content.Context -import com.tencent.beacon.event.open.BeaconReport -import com.tencent.mobileqq.qsec.qsecurity.QSecConfig -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.tools.hex2ByteArray -import moe.fuqiuluo.shamrock.tools.hookMethod -import moe.fuqiuluo.shamrock.utils.MMKVFetcher -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.symbols.XposedHook -import oicq.wlogin_sdk.tools.util - -@XposedHook(priority = 10) -internal class GuidLock: IAction { - companion object { - var qimei: String = "" - } - - override fun invoke(ctx: Context) { - val guildLock = MMKVFetcher.mmkvWithId("guid") - val utilClass = util::class.java - utilClass.hookMethod("needChangeGuid").before { - if (guildLock.getString("guid", null) != null) { - it.result = false - } - } - utilClass.hookMethod("getGuidFromFile").before { - val guid = guildLock.getString("guid", null) - if (guid != null) { - it.result = guid.hex2ByteArray() - } - } - utilClass.hookMethod("saveGuidToFile").before { - val guid = guildLock.getString("guid", null) - if (guid != null) { - it.args[1] = guid.hex2ByteArray() - } - } - - utilClass.hookMethod("get_last_guid").before { - val guid = guildLock.getString("guid", null) - if (guid != null) { - it.result = guid.hex2ByteArray() - } - } - - utilClass.hookMethod("generateGuid").before { - val guid = guildLock.getString("guid", null) - if (guid != null) { - it.result = guid.hex2ByteArray() - } - } - - QSecConfig::class.java.hookMethod("setupBusinessInfo").before { - val guid = guildLock.getString("guid", null) - if (guid != null) { - it.args[2] = guid.hex2ByteArray() - } - } - - if (PlatformUtils.isMqqPackage()) { - BeaconReport.getInstance().getQimei("0S200MNJT807V3GE", ctx) { qimei -> - LogCenter.log("QIMEI获取: ${qimei.qimei36}") - GuidLock.qimei = qimei.qimei36 - } - } else { - BeaconReport.getInstance().getQimei { qimei -> - LogCenter.log("QIMEI获取: ${qimei.qimei36}") - GuidLock.qimei = qimei.qimei36 - } - } - - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/HookForDebug.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/HookForDebug.kt deleted file mode 100644 index 24e5536..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/HookForDebug.kt +++ /dev/null @@ -1,73 +0,0 @@ -@file:Suppress("UNUSED_VARIABLE", "LocalVariableName") - -package moe.fuqiuluo.shamrock.xposed.hooks - -import android.content.Context -import com.tencent.mobileqq.perf.block.BinderMethodProxy -import com.tencent.mobileqq.qmmkv.MMKVOptionEntity -import com.tencent.mobileqq.qroute.QRoute -import com.tencent.qqnt.aio.api.IAIOPicDownloaderProvider -import de.robv.android.xposed.XposedBridge -import epic.EIPCClient -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.tools.beforeHook -import moe.fuqiuluo.shamrock.tools.hookMethod -import moe.fuqiuluo.shamrock.tools.toInnerValuesString -import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader -import moe.fuqiuluo.symbols.Process -import moe.fuqiuluo.symbols.XposedHook -import java.lang.reflect.Modifier - -@XposedHook(priority = -1, process = Process.ALL) -internal class HookForDebug: IAction { - override fun invoke(ctx: Context) { - //val LibraDownloader = QRoute.api(IAIOPicDownloaderProvider::class.java).provideDownloader().javaClass - //LibraDownloader.hookMethod("downLoad").before { - // val option = it.args[0] - // LogCenter.log("LibraDownloader.downLoad(${option.toInnerValuesString()})") - //} - } -} - -/*val NtDnsManager = LuoClassloader.load("com.tencent.qqnt.dns.NtDnsManager")!! - val NtDnsInternal = NtDnsManager.declaredMethods.first { - !Modifier.isStatic(it.modifiers) && it.parameterCount == 0 - }.returnType - XposedBridge.hookMethod(NtDnsInternal.declaredMethods.first { - it.parameterCount == 2 - && it.parameterTypes[0] == String::class.java - && it.parameterTypes[1] == Int::class.java - && it.returnType == ArrayList::class.java - }, beforeHook { - val domain = it.args[0] as String - val type = it.args[1] as Int - LogCenter.log("NtDnsManager: reqDomain2IpList($domain, $type)") - LogCenter.log(Exception().stackTraceToString()) - })*/ -/* - val httpEngineService = AppRuntimeFetcher.appRuntime - .getRuntimeService(IHttpEngineService::class.java, "all") - httpEngineService.javaClass.hookMethod("sendReq").before { - if (it.args[0] is HttpNetReq) { - val req = it.args[0] as HttpNetReq - if (req.mReqProperties["Shamrock"] == "true") { - return@before - } - LogCenter.log("已记录一个IHttpEngineService请求") - LogCenter.log("请求地址: ${req.mReqUrl}") - LogCenter.log("请求: ${req.toInnerValuesString(NetReq::class.java)}") - } - } - BinderMethodProxy::class.java.hookMethod("callServer").before { - val action = it.args[2] as String - if (action == "reqDomain2IpList") { - LogCenter.log(Exception().stackTraceToString()) - } - } - EIPCClient::class.java.hookMethod("callServer").before { - val module = it.args[0] as String - val action = it.args[1] as String - if (action == "reqDomain2IpList" || module.contains("dns", ignoreCase = true)) { - LogCenter.log(Exception().stackTraceToString()) - } - }*/ diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/HookWrapperCodec.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/HookWrapperCodec.kt deleted file mode 100644 index 2b8a37d..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/HookWrapperCodec.kt +++ /dev/null @@ -1,139 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) -package moe.fuqiuluo.shamrock.xposed.hooks - -import android.content.Context -import com.tencent.msf.service.protocol.pb.SSOLoginMerge -import com.tencent.qphone.base.remote.FromServiceMsg -import com.tencent.qphone.base.remote.ToServiceMsg -import com.tencent.qphone.base.util.CodecWarpper -import kotlinx.atomicfu.atomic -import kotlinx.coroutines.DelicateCoroutinesApi -import moe.fuqiuluo.shamrock.remote.service.PacketReceiver -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY -import moe.fuqiuluo.shamrock.tools.hookMethod -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver -import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest -import moe.fuqiuluo.symbols.XposedHook - -private const val MAGIC_APP_ID = 114514 - -@XposedHook(priority = 2) -internal class HookWrapperCodec: IAction { - private val IgnoredCmd = arrayOf( - "trpc.sq_adv.official_account_adv_push.OfficialAccountAdvPush.AdvPush", - "LightAppSvc.mini_app_report_transfer.DataReport", - "JsApiSvr.webview.whitelist", - "trpc.commercial.access.access_sso.SsoAdGet", - "trpc.qpay.homepage2.Homepage2.SsoGetHomepage", - "trpc.qpay.value_added_info.Query.SsoGetPrivilege", - "trpc.qqshop.qgghomepage.Config.SsoGetBottomTab", - "ClubInfoSvc.queryPrivExt", - "OidbSvc.0xcf8", - "LbsSvc.lbs_report", - "OidbSvcTrpcTcp.0x88d_0", - "trpc.down.game_switch.GameSwitch.SsoGetDownloadConfig", - "trpc.zplan.aio_avatar.Mobile.SsoBatchGetSceneConfig", - "OidbSvcTrpcTcp.0x1127_5", - "OidbSvcTrpcTcp.0x10aa", - "WalletConfigSvr.getConfig", - "LightAppSvc.mini_app_userapp.GetDropdownAppList", - "LightAppSvc.mini_app_ad.GetAd", - "SQQzoneSvc.advReport", - "OidbSvc.0xbcb_0", // 内部浏览器URL检测 - //"ConfigurationService.ReqGetConfig" - ) - - override fun invoke(ctx: Context) { - try { - ToServiceMsg::class.java.hookMethod("setRequestSsoSeq").before { - val to = it.thisObject as ToServiceMsg - to.getAttribute("shamrock_seq")?.let { seq -> - it.args[0] = seq - } - } - - val isInit = atomic(false) - CodecWarpper::class.java.hookMethod("init").after { - if (isInit.value) return@after - hookReceive(it.thisObject, it.thisObject.javaClass) - isInit.lazySet(true) - } - CodecWarpper::class.java.hookMethod("nativeOnReceData").before { - if (isInit.value) return@before - hookReceive(it.thisObject, it.thisObject.javaClass) - isInit.lazySet(true) - } - } catch (e: Exception) { - LogCenter.log(e.stackTraceToString(), Level.ERROR) - } - } - - private fun hookReceive(thiz: Any, thizClass: Class<*>) { - val onResponse = thizClass.getDeclaredMethod("onResponse", Integer.TYPE, Any::class.java, Integer.TYPE) - //LogCenter.log("HookWrapperCodec: onResponse = $onResponse", Level.INFO) - DynamicReceiver.register("fake_packet", IPCRequest { - val uin = it.getStringExtra("package_uin")!! - val cmd = it.getStringExtra("package_cmd")!! - val seq = it.getIntExtra("package_seq", 0) - val buffer = it.getByteArrayExtra("package_buffer")!! - //LogCenter.log("伪造收包(cmd = $cmd)") - - val from = FromServiceMsg() - from.requestSsoSeq = seq - from.putWupBuffer(buffer) - from.serviceCmd = cmd - from.appId = MAGIC_APP_ID - from.setMsgSuccess() - from.uin = uin - from.appSeq = seq - onResponse.invoke(thiz, 0, from, 0) - }) - thizClass.hookMethod("onResponse").before { - val from = it.args[1] as FromServiceMsg - try { - if ("SSO.LoginMerge" == from.serviceCmd) { - val merge = SSOLoginMerge.BusiBuffData() - .mergeFrom(from.wupBuffer.slice(4)) - val busiBufVec = merge.BusiBuffVec.get() - busiBufVec.forEach { item -> - if (item.ServiceCmd.get() in IgnoredCmd && ShamrockConfig.isInjectPacket()) { - busiBufVec.remove(item) - } else { - pushOnReceive(FromServiceMsg().apply { - this.requestSsoSeq = item.SeqNo.get() - this.serviceCmd = item.ServiceCmd.get() - putWupBuffer(item.BusiBuff.get().toByteArray()) - }) - } - } - merge.BusiBuffVec.set(busiBufVec) - from.putWupBuffer(merge.toByteArray()) - } else if (from.appId != MAGIC_APP_ID) { - if (from.serviceCmd in IgnoredCmd && ShamrockConfig.isInjectPacket()) { - //from.serviceCmd = "ShamrockInjectedCmd_${from.serviceCmd}" - from.setBusinessFail(1) - from.putWupBuffer(EMPTY_BYTE_ARRAY) - } else if (from.serviceCmd.startsWith("trpc.o3.") && ShamrockConfig.isInjectPacket()) { - from.setBusinessFail(1) - from.putWupBuffer(EMPTY_BYTE_ARRAY) - } else { - pushOnReceive(from) - } - } - } finally { - it.args[1] = from - } - } - } - - private fun pushOnReceive(fromServiceMsg: FromServiceMsg) { - PacketReceiver.internalOnReceive(fromServiceMsg) - } -} - - - diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/InitRemoteService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/InitRemoteService.kt deleted file mode 100644 index 3de2ca2..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/InitRemoteService.kt +++ /dev/null @@ -1,133 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.xposed.hooks - -import android.content.Context -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import moe.fuqiuluo.shamrock.remote.service.WebSocketClientService -import moe.fuqiuluo.shamrock.remote.service.WebSocketService -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.remote.HTTPServer -import moe.fuqiuluo.shamrock.remote.service.HttpService -import moe.fuqiuluo.shamrock.tools.ShamrockVersion -import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher -import moe.fuqiuluo.symbols.Process -import moe.fuqiuluo.symbols.XposedHook -import mqq.app.MobileQQ -import kotlin.concurrent.timer -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -@XposedHook(Process.MAIN, priority = 10) -internal class InitRemoteService : IAction { - override fun invoke(ctx: Context) { - GlobalScope.launch { - try { - HTTPServer.start(ShamrockConfig.getPort()) - } catch (e: Throwable) { - LogCenter.log(e.stackTraceToString(), Level.ERROR) - } - } - - if (!PlatformUtils.isMqqPackage()) return - - - if (ShamrockConfig.allowWebHook()) { - HttpService.init() - } - - val runtime = AppRuntimeFetcher.appRuntime - if (!runtime.isLogin) { - LogCenter.log("未登录,不启动任何WebSocket服务,登录完成后,请重新启动QQ。", Level.WARN) - return - } - if (ShamrockConfig.openWebSocket()) { - startWebSocketServer() - } - - if (ShamrockConfig.openWebSocketClient()) { - val curUin = runtime.currentAccountUin - val defaultToken = ShamrockConfig.getToken() - ShamrockConfig.getWebSocketClientAddress().forEach { conn -> - if (!conn.address.isNullOrBlank()) { - val token = conn.token ?: defaultToken - val wsHeaders = hashMapOf( - "X-Client-Role" to "Universal", - "X-Self-ID" to curUin, - "User-Agent" to "Shamrock/$ShamrockVersion", - "X-QQ-Version" to PlatformUtils.getClientVersion(MobileQQ.getContext()), - "X-OneBot-Version" to "11", - "X-Impl" to "Shamrock", - "Sec-WebSocket-Protocol" to "11.Shamrock" - ) - if (token.isNotBlank()) { - wsHeaders["authorization"] = "bearer $token" - } - LogCenter.log("尝试连接WebSocketClient(url = ${conn.address})",Level.WARN) - startWebSocketClient(conn.address, conn.heartbeatInterval ?: 15000, wsHeaders) - } - } - } else { - LogCenter.log("未启用被动WebSocket,不会加载连接。") - } - } - - private fun startWebSocketServer() { - GlobalScope.launch { - try { - val config = ShamrockConfig.getActiveWebSocketConfig() ?: return@launch - config.address ?: kotlin.run { - LogCenter.log("WebSocketServer地址不合法", Level.ERROR) - return@launch - } - config.port ?: kotlin.run { - LogCenter.log("WebSocketServer端口不合法", Level.ERROR) - return@launch - } - require(config.port in 0 .. 65536) { "WebSocketServer端口不合法" } - val server = WebSocketService(config.address, config.port!!, config.heartbeatInterval ?: (15 * 1000)) - server.isReuseAddr = true - server.start() - } catch (e: Throwable) { - LogCenter.log(e.stackTraceToString(), Level.ERROR) - } - } - } - - private fun startWebSocketClient( - url: String, - interval: Long, - wsHeaders: HashMap - ) { - GlobalScope.launch { - try { - if (url.startsWith("ws://") || url.startsWith("wss://")) { - val wsClient = WebSocketClientService(url, interval, wsHeaders) - wsClient.connect() - wsClient.launch { - while (true) { - delay(5.seconds) - if (wsClient.isClosed || wsClient.isClosing) { - wsClient.reconnect() - } - } - } - } else { - LogCenter.log("被动WebSocket地址不合法: $url", Level.ERROR) - } - } catch (e: Throwable) { - if (e is RuntimeException) { - LogCenter.log(e.message ?: e.stackTraceToString(), Level.ERROR) - } else { - LogCenter.log(e.stackTraceToString(), Level.ERROR) - } - } - } - } -} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/IpcService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/IpcService.kt deleted file mode 100644 index 8777ee1..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/IpcService.kt +++ /dev/null @@ -1,44 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.xposed.hooks - -import android.content.Context -import android.os.Bundle -import kotlinx.coroutines.DelicateCoroutinesApi -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import moe.fuqiuluo.shamrock.tools.broadcast -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.xposed.helper.internal.* -import moe.fuqiuluo.shamrock.xposed.ipc.ShamrockIpc -import moe.fuqiuluo.symbols.Process -import moe.fuqiuluo.symbols.XposedHook - -@XposedHook(Process.MSF, priority = 0) -internal class IpcService: IAction { - override fun invoke(ctx: Context) { - if (!PlatformUtils.isMsfProcess()) return - initIPCFetcher(ctx) - } - - private fun initIPCFetcher(ctx: Context) { - LogCenter.log("IPC服务已启动:$ctx", Level.INFO) - DynamicReceiver.register("fetch_ipc", IPCRequest { - val name = it.getStringExtra("ipc_name") - LogCenter.log("IPC FETCH => $name,请注意是否泄露了API?") - GlobalScope.launch { - ShamrockIpc.get(name)?.also { binder -> - ctx.broadcast("xqbot") { - putExtra("__cmd", "ipc_callback") - putExtra("ipc", Bundle().also { - it.putString("name", name) - it.putBinder("binder", binder) - }) - } - } ?: LogCenter.log("无法获取IPC: $name", Level.WARN) - } - }) - } -} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/PullConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/PullConfig.kt deleted file mode 100644 index f2f522c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/hooks/PullConfig.kt +++ /dev/null @@ -1,95 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.xposed.hooks - -import android.content.Context -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import moe.fuqiuluo.shamrock.remote.HTTPServer -import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import moe.fuqiuluo.shamrock.xposed.helper.internal.DataRequester -import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver -import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest -import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader -import moe.fuqiuluo.symbols.Process -import moe.fuqiuluo.symbols.XposedHook -import mqq.app.MobileQQ -import kotlin.concurrent.thread -import kotlin.system.exitProcess - -@XposedHook(Process.MAIN, priority = 1) -class PullConfig: IAction { - companion object { - @JvmStatic - var isConfigOk = false - } - - private external fun testNativeLibrary(): String - - override fun invoke(ctx: Context) { - if (!PlatformUtils.isMainProcess()) return - - GlobalScope.launch(Dispatchers.Default) { - DynamicReceiver.register("fetchPort", IPCRequest { - DataRequester.request("success", values = mapOf( - "port" to HTTPServer.currServerPort, - "voice" to NativeLoader.isVoiceLoaded - )) - }) - DynamicReceiver.register("checkAndStartService", IPCRequest { - if (HTTPServer.isServiceStarted) { - HTTPServer.isServiceStarted = false - } - initAppService(MobileQQ.getContext()) - }) - DynamicReceiver.register("push_config", IPCRequest { - ctx.toast("动态推送配置文件成功。") - ShamrockConfig.updateConfig(it) - }) - DynamicReceiver.register("change_port", IPCRequest { - when (it.getStringExtra("type")) { - "port" -> { - ctx.toast("动态修改HTTP端口成功。") - HTTPServer.changePort(it.getIntExtra("port", 5700)) - } - "ws_port" -> { - ctx.toast("动态修改WS端口不支持。") - } - "restart" -> { - if(HTTPServer.isServiceStarted) { - ctx.toast("重启HTTPServer完成。") - HTTPServer.restart() - } - } - } - }) - - DataRequester.request("init", onFailure = { - if (!ShamrockConfig.isInit()) { - ctx.toast("请启动Shamrock主进程以初始化服务,进程将退出。") - ShamrockConfig.putDefaultSettings() - thread { - Thread.sleep(3000) - exitProcess(1) - } - } else { - ctx.toast("Shamrock进程未启动,不会推送配置文件。") - initAppService(ctx) - } - }, bodyBuilder = null) { - isConfigOk = true - ShamrockConfig.updateConfig(it) - initAppService(ctx) - } - } - } - - private fun initAppService(ctx: Context) { - NativeLoader.load("shamrock") - ctx.toast(testNativeLibrary()) - runServiceActions(ctx) - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/ShamrockIpc.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/ShamrockIpc.kt deleted file mode 100644 index 19f801c..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/ShamrockIpc.kt +++ /dev/null @@ -1,45 +0,0 @@ -package moe.fuqiuluo.shamrock.xposed.ipc - -import android.os.IBinder -import moe.fuqiuluo.shamrock.utils.PlatformUtils -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withTimeoutOrNull -import moe.fuqiuluo.shamrock.tools.broadcast -import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver -import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest -import moe.fuqiuluo.shamrock.xposed.ipc.bytedata.ByteDataCreator -import moe.fuqiuluo.shamrock.xposed.ipc.qsign.QSignGenerator -import mqq.app.MobileQQ -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -internal object ShamrockIpc { - const val IPC_QSIGN = "qsign" - const val IPC_BYTEDATA = "bytedata" - - private val IpcChannel = hashMapOf( - IPC_QSIGN to QSignGenerator, - IPC_BYTEDATA to ByteDataCreator - ) - - suspend fun get(name: String?): IBinder? { - return if (PlatformUtils.isMsfProcess()) { - IpcChannel[name] - } else { - withTimeoutOrNull(3000) { - suspendCoroutine { continuation -> - DynamicReceiver.register("ipc_callback", IPCRequest { - val bundle = it.getBundleExtra("ipc")!! - val binder = bundle.getBinder("binder") - continuation.resume(binder) - }) - MobileQQ.getContext().broadcast("msf") { - putExtra("__cmd", "fetch_ipc") - putExtra("ipc_name", name) - } - } - } - } - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/ByteData.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/ByteData.kt deleted file mode 100644 index 60fc6b8..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/ByteData.kt +++ /dev/null @@ -1,41 +0,0 @@ -package moe.fuqiuluo.shamrock.xposed.ipc.bytedata - -import android.os.Parcel -import android.os.Parcelable -import com.tencent.secprotocol.ByteData -import moe.fuqiuluo.shamrock.xposed.ipc.bytedata.IByteData - -data class IByteDataSign( - val sign: ByteArray? -): Parcelable { - constructor(parcel: Parcel) : this( - parcel.createByteArray() - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeByteArray(sign) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): IByteDataSign { - return IByteDataSign(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} - -internal object ByteDataCreator: IByteData.Stub() { - override fun sign(uin: String?, data: String?, salt: ByteArray?): IByteDataSign { - val byteData = ByteData.getInstance() - byteData.setDataEx(uin, data) - return IByteDataSign(byteData.getSign(uin, data, salt)) - } - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/qsign/QSign.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/qsign/QSign.kt deleted file mode 100644 index 0555f37..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/ipc/qsign/QSign.kt +++ /dev/null @@ -1,78 +0,0 @@ -package moe.fuqiuluo.shamrock.xposed.ipc.qsign - -import android.os.Parcel -import android.os.Parcelable -import com.tencent.mobileqq.fe.FEKit -import com.tencent.mobileqq.qsec.qsecdandelionsdk.Dandelion -import com.tencent.mobileqq.qsec.qsecurity.QSec -import com.tencent.qphone.base.util.BaseApplication -import moe.fuqiuluo.shamrock.tools.toHexString -import moe.fuqiuluo.shamrock.utils.MMKVFetcher -import moe.fuqiuluo.shamrock.xposed.hooks.GuidLock -import mqq.app.MobileQQ -import oicq.wlogin_sdk.tools.util - -data class IQSign( - val token: ByteArray, - val extra: ByteArray, - val sign: ByteArray -): Parcelable { - constructor(parcel: Parcel) : this( - parcel.createByteArray()!!, - parcel.createByteArray()!!, - parcel.createByteArray()!! - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeByteArray(token) - parcel.writeByteArray(extra) - parcel.writeByteArray(sign) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): IQSign { - return IQSign(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} - -private fun getFEKit(uin: String): FEKit { - val guildLock = MMKVFetcher.mmkvWithId("guid") - val ctx = BaseApplication.getContext() - val guid = guildLock.getString("guid", util.get_last_guid(ctx).toHexString()) - val fe = FEKit.getInstance() - fe.init( - BaseApplication.getContext(), - uin, - guid, "", GuidLock.qimei, - BaseApplication.getContext().qua - ) - return fe -} - -internal object QSignGenerator: IQSigner.Stub() { - override fun sign(cmd: String, seq: Int, uin: String, buffer: ByteArray): IQSign { - val sign = getFEKit(uin).getSign(cmd, buffer, seq, uin) - return IQSign(sign.token, sign.extra, sign.sign) - } - - override fun energy(module: String, salt: ByteArray): ByteArray { - return Dandelion.getInstance().fly(module, salt) - } - - override fun xwDebugId(uin: String, start: String, end: String): ByteArray { - return QSec.getInstance().getFeKitAttach(MobileQQ.getContext(), uin, start, end) - } - - override fun getCmdWhiteList(): List { - return FEKit.getInstance().cmdWhiteList - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppRuntimeFetcher.kt b/xposed/src/main/java/qq/service/QQInterfaces.kt similarity index 51% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppRuntimeFetcher.kt rename to xposed/src/main/java/qq/service/QQInterfaces.kt index e91df25..b37a104 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppRuntimeFetcher.kt +++ b/xposed/src/main/java/qq/service/QQInterfaces.kt @@ -1,13 +1,15 @@ -package moe.fuqiuluo.shamrock.xposed.helper +package qq.service import moe.fuqiuluo.shamrock.utils.PlatformUtils -import mqq.app.AppRuntime import mqq.app.MobileQQ -internal object AppRuntimeFetcher { - val appRuntime: AppRuntime - get() = if (PlatformUtils.isMqqPackage()) +abstract class QQInterfaces { + + + companion object { + val app = if (PlatformUtils.isMqqPackage()) MobileQQ.getMobileQQ().waitAppRuntime() else MobileQQ.getMobileQQ().waitAppRuntime(null) + } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/contact/LocalCacheHelper.kt b/xposed/src/main/java/qq/service/contact/LocalCacheHelper.kt new file mode 100644 index 0000000..17ed79f --- /dev/null +++ b/xposed/src/main/java/qq/service/contact/LocalCacheHelper.kt @@ -0,0 +1,23 @@ +package qq.service.contact + +import moe.fuqiuluo.shamrock.utils.FileUtils +import mqq.app.MobileQQ +import qq.service.QQInterfaces +import java.io.File + +internal object LocalCacheHelper: QQInterfaces() { + // 获取外部储存data目录 + private val dataDir = MobileQQ.getContext().getExternalFilesDir(null)!! + .parentFile!!.resolve("Tencent") + + fun getCurrentPttPath(): File { + return dataDir.resolve("MobileQQ/${app.currentAccountUin}/ptt").also { + if (!it.exists()) it.mkdirs() + } + } + + fun getCachePttFile(md5: String): File { + val file = FileUtils.getFileByMd5(md5) + return if (file.exists()) file else getCurrentPttPath().resolve("$md5.amr") + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/NTServiceHelper.kt b/xposed/src/main/java/qq/service/internals/NTServiceHelper.kt similarity index 93% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/NTServiceHelper.kt rename to xposed/src/main/java/qq/service/internals/NTServiceHelper.kt index c987758..3dbb389 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/NTServiceHelper.kt +++ b/xposed/src/main/java/qq/service/internals/NTServiceHelper.kt @@ -1,4 +1,4 @@ -package moe.fuqiuluo.shamrock.xposed.helper +package qq.service.internals import com.tencent.qqnt.kernel.api.IKernelService import com.tencent.qqnt.kernel.api.impl.MsgService From 69cdbad643558480d090a5435a76c2691c8fafad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Fri, 8 Mar 2024 01:19:26 +0800 Subject: [PATCH 02/20] `Shamrock`: rm CIO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- app/build.gradle.kts | 3 +- xposed/build.gradle.kts | 3 +- .../moe/fuqiuluo/shamrock/tools/KtorClient.kt | 34 ++----------------- 3 files changed, 5 insertions(+), 35 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 64fffd2..f203135 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -202,8 +202,7 @@ dependencies { implementation(kotlinx("io-jvm", "0.1.16")) implementation(ktor("client", "core")) - implementation(ktor("client", "content-negotiation")) - implementation(ktor("client", "cio")) + implementation(ktor("client", "okhttp")) implementation(ktor("serialization", "kotlinx-json")) implementation(project(":xposed")) diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index 8c5c967..9dd0046 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -82,8 +82,7 @@ dependencies { implementation(kotlinx("serialization-protobuf", "1.6.2")) implementation(ktor("client", "core")) - implementation(ktor("client", "content-negotiation")) - implementation(ktor("client", "cio")) + implementation(ktor("client", "okhttp")) implementation(ktor("serialization", "kotlinx-json")) implementation(ktor("network", "tls-certificates")) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/KtorClient.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/KtorClient.kt index 5abeb84..f5f4d7d 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/KtorClient.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/KtorClient.kt @@ -1,43 +1,15 @@ package moe.fuqiuluo.shamrock.tools import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpTimeout -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.serialization.kotlinx.json.json -import kotlinx.serialization.json.Json -val GlobalClient: HttpClient by lazy { - HttpClient { - //install(HttpCookies) +val GlobalClient by lazy { + HttpClient(OkHttp) { install(HttpTimeout) { requestTimeoutMillis = 15000 connectTimeoutMillis = 15000 socketTimeoutMillis = 15000 } - install(ContentNegotiation) { - json(Json { - prettyPrint = true - isLenient = true - }) - } - } -} - -val GlobalClientNoRedirect: HttpClient by lazy { - HttpClient { - //install(HttpCookies) - followRedirects = false - - install(HttpTimeout) { - requestTimeoutMillis = 15000 - connectTimeoutMillis = 15000 - socketTimeoutMillis = 15000 - } - install(ContentNegotiation) { - json(Json { - prettyPrint = true - isLenient = true - }) - } } } \ No newline at end of file From 5637db43bece6d2ef670d74a97903e9ad1d2866e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sun, 10 Mar 2024 00:33:26 +0800 Subject: [PATCH 03/20] =?UTF-8?q?`Shamrock`:=20=E9=87=8D=E6=9E=84=E6=94=B6?= =?UTF-8?q?=E5=8C=85=E8=B5=B7=EF=BC=8C=E5=87=8F=E5=B0=91=E6=8B=B7=E8=B4=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .../moe/fuqiuluo/shamrock/MainActivity.kt | 2 +- .../ui/service/handlers/ModuleHandler.kt | 1 + buildSrc/src/main/kotlin/Dependencies.kt | 3 +- .../com/tencent/common/app/AppInterface.java | 4 ++ .../mobileqq/app/BaseBusinessHandler.java | 8 +++ .../tencent/mobileqq/app/BusinessHandler.java | 2 + .../mobileqq/msf/sdk/MsfMessagePair.java | 18 +++++++ .../src/main/java/mqq/app/AppRuntime.java | 7 +++ .../src/main/java/mqq/app/NewIntent.java | 29 +++++++++++ .../moe/fuqiuluo/shamrock/config/IsInit.kt | 7 +++ .../shamrock/config/ShamrockConfig.kt | 3 +- .../shamrock/xposed/actions/AntiDetection.kt | 8 ++- .../xposed/actions/DynamicBroadcast.kt | 9 +++- .../shamrock/xposed/actions/PatchMsfCore.kt | 45 ++++++++++++++++ .../shamrock/xposed/actions/PullConfig.kt | 24 +++++++++ .../shamrock/xposed/helper/AppTalker.kt | 8 +++ .../java/qq/service/internals/MSFHandler.kt | 52 +++++++++++++++++++ 17 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 qqinterface/src/main/java/com/tencent/mobileqq/msf/sdk/MsfMessagePair.java create mode 100644 qqinterface/src/main/java/mqq/app/NewIntent.java create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/IsInit.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PatchMsfCore.kt create mode 100644 xposed/src/main/java/qq/service/internals/MSFHandler.kt diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt b/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt index 83abc6a..52f4e80 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt @@ -91,7 +91,7 @@ class MainActivity : ComponentActivity() { setContent { LaunchedEffect(Unit) { while (true) { - delay(15_000) // Delay in milliseconds + delay(5_000) // Delay in milliseconds broadcastToModule { putExtra("__cmd", "switch_status") } diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/ModuleHandler.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/ModuleHandler.kt index 547b074..c13b655 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/ModuleHandler.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/ModuleHandler.kt @@ -29,6 +29,7 @@ abstract class ModuleHandler { } } } + putExtra("__cmd", cmd) putExtra("__hash", callbackId) } } diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 13e0d4c..9717f3b 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -7,9 +7,8 @@ val DEPENDENCY_ANDROIDX = arrayOf( "androidx.activity:activity-compose:1.7.2", ) -const val DEPENDENCY_JSON5K = "io.github.xn32:json5k:0.3.0" + const val DEPENDENCY_PROTOBUF = "com.google.protobuf:protobuf-java:3.24.0" -const val DEPENDENCY_JAVA_WEBSOCKET = "org.java-websocket:Java-WebSocket:1.5.4" fun room(name: String) = "androidx.room:room-$name:${Versions.roomVersion}" diff --git a/qqinterface/src/main/java/com/tencent/common/app/AppInterface.java b/qqinterface/src/main/java/com/tencent/common/app/AppInterface.java index 694bd70..80a3d99 100644 --- a/qqinterface/src/main/java/com/tencent/common/app/AppInterface.java +++ b/qqinterface/src/main/java/com/tencent/common/app/AppInterface.java @@ -6,9 +6,13 @@ import com.tencent.mobileqq.app.BusinessObserver; import com.tencent.mobileqq.app.MessageHandler; import com.tencent.qphone.base.remote.ToServiceMsg; +import java.util.concurrent.ConcurrentHashMap; + import mqq.app.AppRuntime; public abstract class AppInterface extends AppRuntime { + private final ConcurrentHashMap allHandler = new ConcurrentHashMap<>(); + public String getCurrentNickname() { return ""; } diff --git a/qqinterface/src/main/java/com/tencent/mobileqq/app/BaseBusinessHandler.java b/qqinterface/src/main/java/com/tencent/mobileqq/app/BaseBusinessHandler.java index 4287a5a..a5292b0 100644 --- a/qqinterface/src/main/java/com/tencent/mobileqq/app/BaseBusinessHandler.java +++ b/qqinterface/src/main/java/com/tencent/mobileqq/app/BaseBusinessHandler.java @@ -13,6 +13,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper { return null; } + public void addBusinessObserver(ToServiceMsg toServiceMsg, BusinessObserver businessObserver, boolean z) { + + } + public final T decodePacket(byte[] data, String name, T obj) { UniPacket uniPacket = new UniPacket(true); try { @@ -24,6 +28,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper { } } + public boolean msgCmdFilter(String str) { + return false; + } + protected abstract Set getCommandList(); protected abstract Set getPushCommandList(); diff --git a/qqinterface/src/main/java/com/tencent/mobileqq/app/BusinessHandler.java b/qqinterface/src/main/java/com/tencent/mobileqq/app/BusinessHandler.java index 970dcec..74cde56 100644 --- a/qqinterface/src/main/java/com/tencent/mobileqq/app/BusinessHandler.java +++ b/qqinterface/src/main/java/com/tencent/mobileqq/app/BusinessHandler.java @@ -8,6 +8,8 @@ public abstract class BusinessHandler extends BaseBusinessHandler { public BusinessHandler(AppInterface appInterface) { } + protected abstract Class observerClass(); + @Override public Set getCommandList() { return null; diff --git a/qqinterface/src/main/java/com/tencent/mobileqq/msf/sdk/MsfMessagePair.java b/qqinterface/src/main/java/com/tencent/mobileqq/msf/sdk/MsfMessagePair.java new file mode 100644 index 0000000..e6c82db --- /dev/null +++ b/qqinterface/src/main/java/com/tencent/mobileqq/msf/sdk/MsfMessagePair.java @@ -0,0 +1,18 @@ +package com.tencent.mobileqq.msf.sdk; + +import com.tencent.qphone.base.remote.FromServiceMsg; +import com.tencent.qphone.base.remote.ToServiceMsg; + +public class MsfMessagePair { + public FromServiceMsg fromServiceMsg; + public String sendProcess; + public ToServiceMsg toServiceMsg; + + public MsfMessagePair(String str, ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) { + + } + + public MsfMessagePair(ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) { + + } +} \ No newline at end of file diff --git a/qqinterface/src/main/java/mqq/app/AppRuntime.java b/qqinterface/src/main/java/mqq/app/AppRuntime.java index 5562807..1693142 100644 --- a/qqinterface/src/main/java/mqq/app/AppRuntime.java +++ b/qqinterface/src/main/java/mqq/app/AppRuntime.java @@ -66,6 +66,13 @@ public abstract class AppRuntime { } } + public MobileQQ getApplication() { + return null; + } + + public void startServlet(NewIntent newIntent) { + } + public T getRuntimeService(Class cls, String namespace) { throw new UnsupportedOperationException(); } diff --git a/qqinterface/src/main/java/mqq/app/NewIntent.java b/qqinterface/src/main/java/mqq/app/NewIntent.java new file mode 100644 index 0000000..91f8577 --- /dev/null +++ b/qqinterface/src/main/java/mqq/app/NewIntent.java @@ -0,0 +1,29 @@ +package mqq.app; + +import android.content.Context; +import android.content.Intent; + +import com.tencent.mobileqq.app.BusinessObserver; + +public class NewIntent extends Intent { + public boolean runNow; + + public NewIntent(Context context, Class cls) { + super(context, cls); + } + + public BusinessObserver getObserver() { + return null; + } + + public boolean isWithouLogin() { + return false; + } + + public void setObserver(BusinessObserver businessObserver) { + + } + + public void setWithouLogin(boolean z) { + } +} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/IsInit.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/IsInit.kt new file mode 100644 index 0000000..02adf33 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/IsInit.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.config + +object IsInit: ConfigKey() { + override fun name(): String = "is_init" + + override fun default(): Boolean = false +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt index f03c5f8..4866b18 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt @@ -8,7 +8,7 @@ private val configDir = MobileQQ.getContext().getExternalFilesDir(null)!! .parentFile!!.resolve("Tencent/Shamrock").also { if (!it.exists()) it.mkdirs() } -private val configFile = configDir.resolve("config.properties") +private val configFile = configDir.resolve("config.prop") private val configKeys = setOf( ActiveRPC, @@ -42,6 +42,7 @@ internal object ShamrockConfig: Properties() { val value = intent.getStringExtra(key.name()) if (value != null) setProperty(key.name(), value) } + setProperty(IsInit.name(), "true") } configFile.outputStream().use { store(it, "Shamrock Config ${System.currentTimeMillis()}") diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt index d32c221..878cc10 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt @@ -16,6 +16,7 @@ import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.tools.MethodHooker import moe.fuqiuluo.shamrock.tools.hookMethod +import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.xposed.XposedEntry import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader @@ -85,7 +86,12 @@ class AntiDetection: IAction { LogCenter.log("[Shamrock] Shamrock反检测启动失败(env=$env, injected=$injected)", Level.ERROR) } else { XposedEntry.secStaticNativehookInited = true - LogCenter.log("[Shamrock] Shamrock反检测启动成功: ${antiNativeDetections()}", Level.INFO) + if (PlatformUtils.isMainProcess()) { + LogCenter.log( + "[Shamrock] Shamrock反检测启动成功: ${antiNativeDetections()}", + Level.INFO + ) + } } } catch (e: Throwable) { LogCenter.log("[Shamrock] Shamrock反检测启动失败,请检查LSPosed版本使用大于100: ${e.message}", Level.ERROR) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/DynamicBroadcast.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/DynamicBroadcast.kt index 20e5292..98e5ac8 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/DynamicBroadcast.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/DynamicBroadcast.kt @@ -7,6 +7,8 @@ import android.content.Intent import android.content.IntentFilter import android.os.Build import de.robv.android.xposed.XposedBridge +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.xposed.actions.interacts.SwitchStatus import moe.fuqiuluo.shamrock.xposed.actions.interacts.Init import moe.fuqiuluo.symbols.Process @@ -38,7 +40,12 @@ class DynamicBroadcast: IAction { override fun onReceive(context: Context, intent: Intent) { val cmd = intent.getStringExtra("__cmd") ?: "" - handlers[cmd]?.invoke(intent) + val handler = handlers[cmd] + if (handler == null) { + LogCenter.log("DynamicReceiver.onReceive: unknown cmd=$cmd", Level.ERROR) + } else { + handler(intent) + } } } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PatchMsfCore.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PatchMsfCore.kt new file mode 100644 index 0000000..fcf823a --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PatchMsfCore.kt @@ -0,0 +1,45 @@ +@file:Suppress("UNCHECKED_CAST", "UNUSED_VARIABLE", "LocalVariableName") +package moe.fuqiuluo.shamrock.xposed.actions + +import android.content.Context +import com.tencent.common.app.AppInterface +import com.tencent.mobileqq.msf.sdk.MsfMessagePair +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.hookMethod +import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader +import moe.fuqiuluo.symbols.XposedHook +import qq.service.QQInterfaces +import qq.service.internals.MSFHandler.onPush +import qq.service.internals.MSFHandler.onResp + + +@XposedHook(priority = 10) +class PatchMsfCore: IAction { + override fun invoke(ctx: Context) { + val app = QQInterfaces.app + require(app is AppInterface) { "QQInterface.app must be AppInterface" } + + runCatching { + val MSFRespHandleTask = LuoClassloader.load("mqq.app.msghandle.MSFRespHandleTask") + if (MSFRespHandleTask == null) { + LogCenter.log("无法注入MSFRespHandleTask!", Level.ERROR) + } else { + val msfPair = MSFRespHandleTask.declaredFields.first { + it.type == MsfMessagePair::class.java + } + msfPair.isAccessible = true + MSFRespHandleTask.hookMethod("run").before { + val pair = msfPair.get(it.thisObject) as MsfMessagePair + if (pair.toServiceMsg == null) { + onPush(pair.fromServiceMsg) + } else { + onResp(pair.toServiceMsg, pair.fromServiceMsg) + } + } + } + }.onFailure { + LogCenter.log(it.stackTraceToString(), Level.ERROR) + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PullConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PullConfig.kt index 609ada6..34b0081 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PullConfig.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/PullConfig.kt @@ -1,14 +1,38 @@ +@file:OptIn(DelicateCoroutinesApi::class) package moe.fuqiuluo.shamrock.xposed.actions import android.content.Context +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import moe.fuqiuluo.shamrock.config.IsInit +import moe.fuqiuluo.shamrock.config.ShamrockConfig +import moe.fuqiuluo.shamrock.tools.toast import moe.fuqiuluo.shamrock.utils.PlatformUtils +import moe.fuqiuluo.shamrock.xposed.helper.AppTalker import moe.fuqiuluo.symbols.Process import moe.fuqiuluo.symbols.XposedHook +import kotlin.system.exitProcess +import kotlin.time.Duration.Companion.seconds @XposedHook(Process.MAIN, priority = 1) class PullConfig: IAction { override fun invoke(ctx: Context) { if (!PlatformUtils.isMainProcess()) return + val isInit = ShamrockConfig[IsInit] + AppTalker.talk("init", onFailure = { + if (isInit) { + ctx.toast("Shamrock主进程未启动,将不会同步配置!") + } else { + ctx.toast("Shamrock主进程未启动,初始化失败!") + GlobalScope.launch { + delay(3.seconds) + exitProcess(1) + } + } + }) + ctx.toast("同步配置中...") } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppTalker.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppTalker.kt index 63374e4..7803197 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppTalker.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/helper/AppTalker.kt @@ -24,4 +24,12 @@ internal object AppTalker { bodyBuilder.invoke(values) talk(values) } + + fun talk(action: String, onFailure: ((Throwable) -> Unit)? = null, bodyBuilder: ContentValues.() -> Unit = {}) { + val values = ContentValues() + values.put("__cmd", action) + values.put("__hash", 0) + bodyBuilder.invoke(values) + talk(values, onFailure) + } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/internals/MSFHandler.kt b/xposed/src/main/java/qq/service/internals/MSFHandler.kt new file mode 100644 index 0000000..900ecf6 --- /dev/null +++ b/xposed/src/main/java/qq/service/internals/MSFHandler.kt @@ -0,0 +1,52 @@ +package qq.service.internals + +import com.tencent.qphone.base.remote.FromServiceMsg +import com.tencent.qphone.base.remote.ToServiceMsg +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +typealias MsfPush = (FromServiceMsg) -> Unit +typealias MsfResp = (ToServiceMsg, FromServiceMsg) -> Unit + +internal object MSFHandler { + private val mPushHandlers = hashMapOf() + private val mRespHandler = hashMapOf() + private val mPushLock = Mutex() + private val mRespLock = Mutex() + + suspend fun registerPush(cmd: String, push: MsfPush) { + mPushLock.withLock { + mPushHandlers[cmd] = push + } + } + + suspend fun unregisterPush(cmd: String) { + mPushLock.withLock { + mPushHandlers.remove(cmd) + } + } + + suspend fun registerResp(cmd: Int, resp: MsfResp) { + mRespLock.withLock { + mRespHandler[cmd] = resp + } + } + + suspend fun unregisterResp(cmd: Int) { + mRespLock.withLock { + mRespHandler.remove(cmd) + } + } + + fun onPush(fromServiceMsg: FromServiceMsg) { + val cmd = fromServiceMsg.serviceCmd + val push = mPushHandlers[cmd] + push?.invoke(fromServiceMsg) + } + + fun onResp(toServiceMsg: ToServiceMsg, fromServiceMsg: FromServiceMsg) { + val cmd = toServiceMsg.getAttribute("respkey") as Int + val resp = mRespHandler[cmd] + resp?.invoke(toServiceMsg, fromServiceMsg) + } +} \ No newline at end of file From 13a49dd70b2f03c45bc73822aaa1ee88b0fe3041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sun, 10 Mar 2024 01:00:20 +0800 Subject: [PATCH 04/20] =?UTF-8?q?`Shamrock`:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?kritor=E7=9A=84=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .gitmodules | 3 +++ kritor | 1 + settings.gradle.kts | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 .gitmodules create mode 160000 kritor diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..430402e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "kritor"] + path = kritor + url = https://github.com/KarinJS/kritor diff --git a/kritor b/kritor new file mode 160000 index 0000000..7bb50f4 --- /dev/null +++ b/kritor @@ -0,0 +1 @@ +Subproject commit 7bb50f4de3e64d6c1b9e00716c13870f14054b52 diff --git a/settings.gradle.kts b/settings.gradle.kts index 9693397..e5f6130 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,3 +39,6 @@ include( include(":protobuf") include(":processor") include(":annotations") +include(":kritor") + +project(":kritor").projectDir = file("kritor/protos") \ No newline at end of file From c3934778c75beaa3f7abe12093375637ab30d06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sun, 10 Mar 2024 01:56:02 +0800 Subject: [PATCH 05/20] =?UTF-8?q?`Shamrock`:=20=E4=BF=AE=E6=AD=A3=E9=A2=91?= =?UTF-8?q?=E9=81=93proto=E6=96=87=E4=BB=B6=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .../ui/service/handlers/InitHandler.kt | 11 +- xposed/build.gradle.kts | 36 +- .../main/java/kritor/server/KritorServer.kt | 20 ++ .../moe/fuqiuluo/shamrock/config/ConfigKey.kt | 3 + .../shamrock/config/ShamrockConfig.kt | 3 +- .../shamrock/internals/NTServiceFetcher.kt | 3 +- .../xposed/actions/InitRemoteService.kt | 11 + .../java/qq/service/internals/AioListener.kt | 314 ++++++++++++++++++ 8 files changed, 395 insertions(+), 6 deletions(-) create mode 100644 xposed/src/main/java/kritor/server/KritorServer.kt create mode 100644 xposed/src/main/java/qq/service/internals/AioListener.kt diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt index 148b6c4..349d555 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt @@ -18,12 +18,17 @@ internal object InitHandler: ModuleHandler() { val maps = hashMapOf() - RPCPort.update(context, maps) - RPCAddress.update(context, maps) - ForceTablet.update(context, maps) ActiveRPC.update(context, maps) + AliveReply.update(context, maps) + AntiJvmTrace.update(context, maps) + DebugMode.update(context, maps) + EnableOldBDH.update(context, maps) + EnableSelfMessage.update(context, maps) + ForceTablet.update(context, maps) PassiveRPC.update(context, maps) ResourceGroup.update(context, maps) + RPCAddress.update(context, maps) + RPCPort.update(context, maps) callback(context, 1, maps) } diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index 9dd0046..c8d9842 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("org.jetbrains.kotlin.android") id("kotlin-kapt") id("com.google.devtools.ksp") version "1.9.22-1.0.17" + id("com.google.protobuf") version "0.9.4" kotlin("plugin.serialization") version "1.9.22" } @@ -63,6 +64,8 @@ dependencies { compileOnly ("de.robv.android.xposed:api:82") compileOnly (project(":qqinterface")) + protobuf(project(":kritor")) + implementation(project(":protobuf")) implementation(project(":annotations")) ksp(project(":processor")) @@ -84,7 +87,13 @@ dependencies { implementation(ktor("client", "core")) implementation(ktor("client", "okhttp")) implementation(ktor("serialization", "kotlinx-json")) - implementation(ktor("network", "tls-certificates")) + + implementation("io.grpc:grpc-stub:1.62.2") + implementation("io.grpc:grpc-protobuf:1.62.2") + implementation("com.google.protobuf:protobuf-java-util:3.25.1") + implementation("com.google.protobuf:protobuf-kotlin:3.25.3") + implementation("io.grpc:grpc-kotlin-stub:1.4.1") + implementation("io.grpc:grpc-okhttp:1.62.2") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") @@ -92,3 +101,28 @@ dependencies { androidTestImplementation(platform("androidx.compose:compose-bom:2023.06.01")) androidTestImplementation("androidx.compose.ui:ui-test-junit4") } + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.25.3" + } + plugins { + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:1.62.2" + } + create("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" + } + } + generateProtoTasks { + all().forEach { + it.plugins { + create("grpc") + create("grpckt") + } + it.builtins { + create("kotlin") + } + } + } +} diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt new file mode 100644 index 0000000..80c0706 --- /dev/null +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -0,0 +1,20 @@ +package kritor.server + +import io.grpc.ServerBuilder +import moe.fuqiuluo.shamrock.helper.LogCenter + +class KritorServer( + private val port: Int +) { + private val server = ServerBuilder + .forPort(port) + .build()!! + + fun start(block: Boolean = false) { + LogCenter.log("KritorServer started at port $port.") + server.start() + if (block) { + server.awaitTermination() + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ConfigKey.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ConfigKey.kt index b3e2d67..0e92222 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ConfigKey.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ConfigKey.kt @@ -10,3 +10,6 @@ abstract class ConfigKey { } } +internal inline fun > T.get(): Type { + return ShamrockConfig[this] +} diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt index 4866b18..9ca6dac 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt @@ -16,7 +16,8 @@ private val configKeys = setOf( PassiveRPC, ResourceGroup, RPCAddress, - RPCPort + RPCPort, + ) internal object ShamrockConfig: Properties() { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt index a6e3885..8f03741 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt @@ -10,6 +10,7 @@ import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.tools.hookMethod import moe.fuqiuluo.shamrock.utils.PlatformUtils +import qq.service.internals.AioListener import qq.service.internals.msgService internal object NTServiceFetcher { @@ -58,7 +59,7 @@ internal object NTServiceFetcher { try { LogCenter.log("Register MSG listener successfully.") - //msgService.addMsgListener(AioListener) + msgService.addMsgListener(AioListener) // 接口缺失 暂不使用 //groupService.addKernelGroupListener(GroupEventListener) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt index 937c114..7677640 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt @@ -4,12 +4,23 @@ package moe.fuqiuluo.shamrock.xposed.actions import android.content.Context import kotlinx.coroutines.DelicateCoroutinesApi +import kritor.server.KritorServer +import moe.fuqiuluo.shamrock.config.ActiveRPC +import moe.fuqiuluo.shamrock.config.RPCPort +import moe.fuqiuluo.shamrock.config.ShamrockConfig +import moe.fuqiuluo.shamrock.config.get import moe.fuqiuluo.symbols.Process import moe.fuqiuluo.symbols.XposedHook +private lateinit var server: KritorServer + @XposedHook(Process.MAIN, priority = 10) internal class InitRemoteService : IAction { override fun invoke(ctx: Context) { + if (ActiveRPC.get() && !::server.isInitialized) { + server = KritorServer(RPCPort.get()) + server.start() + } } } diff --git a/xposed/src/main/java/qq/service/internals/AioListener.kt b/xposed/src/main/java/qq/service/internals/AioListener.kt new file mode 100644 index 0000000..c14fe19 --- /dev/null +++ b/xposed/src/main/java/qq/service/internals/AioListener.kt @@ -0,0 +1,314 @@ +package qq.service.internals + +import com.tencent.qqnt.kernel.nativeinterface.BroadcastHelperTransNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.ContactMsgBoxInfo +import com.tencent.qqnt.kernel.nativeinterface.CustomWithdrawConfig +import com.tencent.qqnt.kernel.nativeinterface.DevInfo +import com.tencent.qqnt.kernel.nativeinterface.DownloadRelateEmojiResultInfo +import com.tencent.qqnt.kernel.nativeinterface.EmojiNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.EmojiResourceInfo +import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.FirstViewDirectMsgNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.FirstViewGroupGuildInfo +import com.tencent.qqnt.kernel.nativeinterface.FreqLimitInfo +import com.tencent.qqnt.kernel.nativeinterface.GroupFileListResult +import com.tencent.qqnt.kernel.nativeinterface.GroupGuildNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.GroupItem +import com.tencent.qqnt.kernel.nativeinterface.GuildInteractiveNotificationItem +import com.tencent.qqnt.kernel.nativeinterface.GuildMsgAbFlag +import com.tencent.qqnt.kernel.nativeinterface.GuildNotificationAbstractInfo +import com.tencent.qqnt.kernel.nativeinterface.HitRelatedEmojiWordsResult +import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener +import com.tencent.qqnt.kernel.nativeinterface.ImportOldDbMsgNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.InputStatusInfo +import com.tencent.qqnt.kernel.nativeinterface.KickedInfo +import com.tencent.qqnt.kernel.nativeinterface.MsgAbstract +import com.tencent.qqnt.kernel.nativeinterface.MsgElement +import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.kernel.nativeinterface.MsgSetting +import com.tencent.qqnt.kernel.nativeinterface.RecvdOrder +import com.tencent.qqnt.kernel.nativeinterface.RelatedWordEmojiInfo +import com.tencent.qqnt.kernel.nativeinterface.SearchGroupFileResult +import com.tencent.qqnt.kernel.nativeinterface.TabStatusInfo +import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo +import com.tencent.qqnt.kernel.nativeinterface.UnreadCntInfo +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter + +object AioListener: IKernelMsgListener { + override fun onRecvMsg(arrayList: ArrayList?) { + + } + + override fun onMsgRecall(chatType: Int, peerId: String, msgId: Long) { + LogCenter.log("onMsgRecall($chatType, $peerId, $msgId)") + } + + override fun onAddSendMsg(record: MsgRecord) { + + } + + override fun onMsgInfoListUpdate(msgList: ArrayList?) { + + } + + override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo) { + + } + + override fun onMsgAbstractUpdate(arrayList: ArrayList?) { + //arrayList?.forEach { + // LogCenter.log("onMsgAbstractUpdate($it)", Level.WARN) + //} + } + + override fun onRecvMsgSvrRspTransInfo( + j2: Long, + contact: Contact?, + i2: Int, + i3: Int, + str: String?, + bArr: ByteArray? + ) { + LogCenter.log("onRecvMsgSvrRspTransInfo($j2, $contact, $i2, $i3, $str)", Level.DEBUG) + } + + override fun onRecvS2CMsg(arrayList: ArrayList?) { + LogCenter.log("onRecvS2CMsg(${arrayList.toString()})", Level.DEBUG) + } + + override fun onRecvSysMsg(arrayList: ArrayList?) { + LogCenter.log("onRecvSysMsg(${arrayList.toString()})", Level.DEBUG) + } + + override fun onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {} + + override fun onBroadcastHelperProgerssUpdate(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {} + + override fun onChannelFreqLimitInfoUpdate( + contact: Contact?, + z: Boolean, + freqLimitInfo: FreqLimitInfo? + ) { + + } + + override fun onContactUnreadCntUpdate(unreadMap: HashMap>) { + // 推送未读消息数量 + } + + override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) { + LogCenter.log("onCustomWithdrawConfigUpdate: " + customWithdrawConfig.toString(), Level.DEBUG) + } + + override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList?, j2: Long) { + LogCenter.log("onDraftUpdate: " + contact.toString() + "|" + arrayList + "|" + j2.toString(), Level.DEBUG) + } + + override fun onEmojiDownloadComplete(emojiNotifyInfo: EmojiNotifyInfo?) { + + } + + override fun onEmojiResourceUpdate(emojiResourceInfo: EmojiResourceInfo?) { + + } + + override fun onFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { + + } + + override fun onFileMsgCome(arrayList: ArrayList?) { + + } + + override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { + + } + + override fun onFirstViewGroupGuildMapping(arrayList: ArrayList?) { + + } + + override fun onGrabPasswordRedBag( + i2: Int, + str: String?, + i3: Int, + recvdOrder: RecvdOrder?, + msgRecord: MsgRecord? + ) { + + } + + override fun onKickedOffLine(kickedInfo: KickedInfo?) { + LogCenter.log("onKickedOffLine($kickedInfo)") + } + + override fun onRichMediaUploadComplete(notifyInfo: FileTransNotifyInfo) { + LogCenter.log({ "[BDH] 资源上传完成(${notifyInfo.trasferStatus}, ${notifyInfo.fileId}, ${notifyInfo.msgId}, ${notifyInfo.commonFileInfo})" }, Level.DEBUG) + } + + override fun onRecvOnlineFileMsg(arrayList: ArrayList?) { + LogCenter.log(("onRecvOnlineFileMsg" + arrayList?.joinToString { ", " }), Level.DEBUG) + } + + override fun onRichMediaDownloadComplete(fileTransNotifyInfo: FileTransNotifyInfo) { + + } + + override fun onRichMediaProgerssUpdate(fileTransNotifyInfo: FileTransNotifyInfo) { + + } + + override fun onSearchGroupFileInfoUpdate(searchGroupFileResult: SearchGroupFileResult?) { + LogCenter.log("onSearchGroupFileInfoUpdate($searchGroupFileResult)", Level.DEBUG) + } + + override fun onGroupFileInfoAdd(groupItem: GroupItem?) { + LogCenter.log("onGroupFileInfoAdd: " + groupItem.toString(), Level.DEBUG) + } + + override fun onGroupFileInfoUpdate(groupFileListResult: GroupFileListResult?) { + LogCenter.log("onGroupFileInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG) + } + + override fun onGroupGuildUpdate(groupGuildNotifyInfo: GroupGuildNotifyInfo?) { + LogCenter.log("onGroupGuildUpdate: " + groupGuildNotifyInfo.toString(), Level.DEBUG) + } + + override fun onGroupTransferInfoAdd(groupItem: GroupItem?) { + LogCenter.log("onGroupTransferInfoAdd: " + groupItem.toString(), Level.DEBUG) + } + + override fun onGroupTransferInfoUpdate(groupFileListResult: GroupFileListResult?) { + LogCenter.log("onGroupTransferInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG) + } + + override fun onGuildInteractiveUpdate(guildInteractiveNotificationItem: GuildInteractiveNotificationItem?) { + + } + + override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) { + + } + + override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) { + + } + + override fun onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: DownloadRelateEmojiResultInfo?) { + + } + + override fun onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: HitRelatedEmojiWordsResult?) { + + } + + override fun onHitRelatedEmojiResult(relatedWordEmojiInfo: RelatedWordEmojiInfo?) { + + } + + override fun onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: ImportOldDbMsgNotifyInfo?) { + + } + + override fun onInputStatusPush(inputStatusInfo: InputStatusInfo?) { + + } + + override fun onLineDev(devList: ArrayList?) { + //LogCenter.log("onLineDev($arrayList)") + } + + override fun onLogLevelChanged(newLevel: Long) { + + } + + override fun onMsgBoxChanged(arrayList: ArrayList?) { + + } + + override fun onMsgDelete(contact: Contact?, arrayList: ArrayList?) { + + } + + override fun onMsgEventListUpdate(hashMap: HashMap>?) { + + } + + override fun onMsgInfoListAdd(arrayList: ArrayList?) { + + } + + override fun onMsgQRCodeStatusChanged(i2: Int) { + + } + + override fun onMsgSecurityNotify(msgRecord: MsgRecord?) { + LogCenter.log("onMsgSecurityNotify($msgRecord)") + } + + override fun onMsgSettingUpdate(msgSetting: MsgSetting?) { + + } + + override fun onNtFirstViewMsgSyncEnd() { + + } + + override fun onNtMsgSyncEnd() { + LogCenter.log("NTKernel同步消息完成", Level.DEBUG) + } + + override fun onNtMsgSyncStart() { + LogCenter.log("NTKernel同步消息开始", Level.DEBUG) + } + + override fun onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { + + } + + override fun onRecvGroupGuildFlag(i2: Int) { + + } + + override fun onRecvUDCFlag(i2: Int) { + LogCenter.log("onRecvUDCFlag($i2)", Level.DEBUG) + } + + override fun onSendMsgError(j2: Long, contact: Contact?, i2: Int, str: String?) { + LogCenter.log("onSendMsgError($j2, $contact, $j2, $str)", Level.DEBUG) + } + + override fun onSysMsgNotification(i2: Int, j2: Long, j3: Long, arrayList: ArrayList?) { + LogCenter.log("onSysMsgNotification($i2, $j2, $j3, $arrayList)", Level.DEBUG) + } + + override fun onUnreadCntAfterFirstView(hashMap: HashMap>?) { + + } + + override fun onUnreadCntUpdate(hashMap: HashMap>?) { + + } + + override fun onUserChannelTabStatusChanged(z: Boolean) { + + } + + override fun onUserOnlineStatusChanged(z: Boolean) { + + } + + override fun onUserTabStatusChanged(arrayList: ArrayList?) { + LogCenter.log("onUserTabStatusChanged($arrayList)", Level.DEBUG) + } + + override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) { + + } + + override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) { + + } +} \ No newline at end of file From ee6e13a5bb30f9409ea04f1c7a98f501a3f92d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sun, 10 Mar 2024 01:58:18 +0800 Subject: [PATCH 06/20] =?UTF-8?q?`Shamrock`:=20=E4=BF=AE=E6=AD=A3=E9=A2=91?= =?UTF-8?q?=E9=81=93proto=E6=96=87=E4=BB=B6=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- kritor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kritor b/kritor index 7bb50f4..1d0e13d 160000 --- a/kritor +++ b/kritor @@ -1 +1 @@ -Subproject commit 7bb50f4de3e64d6c1b9e00716c13870f14054b52 +Subproject commit 1d0e13d88729263caeded17889a5d21fa808cbaa From 07364c8298ea07202f8da2aa3624c63878241683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sun, 10 Mar 2024 02:26:53 +0800 Subject: [PATCH 07/20] =?UTF-8?q?`Shamrock`:=20=E4=BF=AE=E5=A4=8Dkritor?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- annotations/build.gradle.kts | 2 +- .../java/moe/fuqiuluo/symbols/Protobuf.kt | 2 ++ buildSrc/src/main/kotlin/Dependencies.kt | 3 -- kritor | 2 +- processor/build.gradle.kts | 2 +- protobuf/build.gradle.kts | 2 +- .../oidb/cmd0x11c5/NtV2RichMediaRsp.kt | 2 +- .../protobuf/oidb/cmd0x388/Cmd0x388Req.kt | 2 +- .../protobuf/oidb/cmd0x388/Cmd0x388Rsp.kt | 2 +- xposed/build.gradle.kts | 31 ++++++++++++++----- 10 files changed, 33 insertions(+), 17 deletions(-) diff --git a/annotations/build.gradle.kts b/annotations/build.gradle.kts index cebd7f6..dbb3902 100644 --- a/annotations/build.gradle.kts +++ b/annotations/build.gradle.kts @@ -9,6 +9,6 @@ java { } dependencies { - implementation(DEPENDENCY_PROTOBUF) + //implementation(DEPENDENCY_PROTOBUF) implementation(kotlinx("serialization-protobuf", "1.6.2")) } \ No newline at end of file diff --git a/annotations/src/main/java/moe/fuqiuluo/symbols/Protobuf.kt b/annotations/src/main/java/moe/fuqiuluo/symbols/Protobuf.kt index a9e148e..43ec4e3 100644 --- a/annotations/src/main/java/moe/fuqiuluo/symbols/Protobuf.kt +++ b/annotations/src/main/java/moe/fuqiuluo/symbols/Protobuf.kt @@ -5,6 +5,8 @@ import kotlinx.serialization.protobuf.ProtoBuf import kotlin.reflect.KClass +val EMPTY_BYTE_ARRAY = ByteArray(0) + interface Protobuf> inline fun > ByteArray.decodeProtobuf(to: KClass? = null): T { diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 9717f3b..b289728 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -7,9 +7,6 @@ val DEPENDENCY_ANDROIDX = arrayOf( "androidx.activity:activity-compose:1.7.2", ) - -const val DEPENDENCY_PROTOBUF = "com.google.protobuf:protobuf-java:3.24.0" - fun room(name: String) = "androidx.room:room-$name:${Versions.roomVersion}" fun kotlinx(name: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$name:$version" diff --git a/kritor b/kritor index 1d0e13d..0dd59dd 160000 --- a/kritor +++ b/kritor @@ -1 +1 @@ -Subproject commit 1d0e13d88729263caeded17889a5d21fa808cbaa +Subproject commit 0dd59dd2d43324ba13f14e2b8ebc5df07ed9821d diff --git a/processor/build.gradle.kts b/processor/build.gradle.kts index fc61b86..22df2e4 100644 --- a/processor/build.gradle.kts +++ b/processor/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15") implementation("com.squareup:kotlinpoet:1.14.2") - implementation(DEPENDENCY_PROTOBUF) + //implementation(DEPENDENCY_PROTOBUF) implementation(kotlinx("serialization-protobuf", "1.6.2")) ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") diff --git a/protobuf/build.gradle.kts b/protobuf/build.gradle.kts index 4125f0b..db8dae5 100644 --- a/protobuf/build.gradle.kts +++ b/protobuf/build.gradle.kts @@ -37,7 +37,7 @@ android { } dependencies { - implementation(DEPENDENCY_PROTOBUF) + //implementation(DEPENDENCY_PROTOBUF) implementation(kotlinx("serialization-protobuf", "1.6.2")) implementation(kotlinx("serialization-json", "1.6.2")) diff --git a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt index bf5fd13..96b8f28 100644 --- a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt +++ b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt @@ -1,7 +1,7 @@ @file:OptIn(ExperimentalSerializationApi::class) package protobuf.oidb.cmd0x11c5 -import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY +import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber diff --git a/protobuf/src/main/java/protobuf/oidb/cmd0x388/Cmd0x388Req.kt b/protobuf/src/main/java/protobuf/oidb/cmd0x388/Cmd0x388Req.kt index ea1f666..b7882fe 100644 --- a/protobuf/src/main/java/protobuf/oidb/cmd0x388/Cmd0x388Req.kt +++ b/protobuf/src/main/java/protobuf/oidb/cmd0x388/Cmd0x388Req.kt @@ -1,6 +1,6 @@ package protobuf.oidb.cmd0x388 -import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY +import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import moe.fuqiuluo.symbols.Protobuf diff --git a/protobuf/src/main/java/protobuf/oidb/cmd0x388/Cmd0x388Rsp.kt b/protobuf/src/main/java/protobuf/oidb/cmd0x388/Cmd0x388Rsp.kt index 8d7f107..75f441e 100644 --- a/protobuf/src/main/java/protobuf/oidb/cmd0x388/Cmd0x388Rsp.kt +++ b/protobuf/src/main/java/protobuf/oidb/cmd0x388/Cmd0x388Rsp.kt @@ -2,7 +2,7 @@ package protobuf.oidb.cmd0x388 -import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY +import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index c8d9842..8048a78 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -75,7 +75,7 @@ dependencies { DEPENDENCY_ANDROIDX.forEach { implementation(it) } - implementation(DEPENDENCY_PROTOBUF) + //implementation(DEPENDENCY_PROTOBUF) implementation(room("runtime")) kapt(room("compiler")) @@ -89,9 +89,8 @@ dependencies { implementation(ktor("serialization", "kotlinx-json")) implementation("io.grpc:grpc-stub:1.62.2") - implementation("io.grpc:grpc-protobuf:1.62.2") - implementation("com.google.protobuf:protobuf-java-util:3.25.1") - implementation("com.google.protobuf:protobuf-kotlin:3.25.3") + implementation("io.grpc:grpc-protobuf-lite:1.62.2") + implementation("com.google.protobuf:protobuf-kotlin-lite:3.25.3") implementation("io.grpc:grpc-kotlin-stub:1.4.1") implementation("io.grpc:grpc-okhttp:1.62.2") @@ -102,11 +101,20 @@ dependencies { androidTestImplementation("androidx.compose.ui:ui-test-junit4") } +tasks.withType().all { + kotlinOptions { + freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn") + } +} + protobuf { protoc { artifact = "com.google.protobuf:protoc:3.25.3" } plugins { + create("java") { + artifact = "io.grpc:protoc-gen-grpc-java:1.62.2" + } create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.62.2" } @@ -117,11 +125,20 @@ protobuf { generateProtoTasks { all().forEach { it.plugins { - create("grpc") - create("grpckt") + create("java") { + option("lite") + } + create("grpc") { + option("lite") + } + create("grpckt") { + option("lite") + } } it.builtins { - create("kotlin") + create("kotlin") { + option("lite") + } } } } From 638bf72392e4b997ab13281b741a4021ba24ed04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sun, 10 Mar 2024 07:44:02 +0800 Subject: [PATCH 08/20] =?UTF-8?q?`Shamrock`:=20=E4=BF=AE=E5=A4=8D=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6=E4=B8=8B=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .../src/main/java/kritor/service/Grpc.kt | 7 +++ kritor | 2 +- xposed/src/main/assets/config.properties | 43 ++++++++++++++++ .../main/java/kritor/server/KritorServer.kt | 14 +++++- .../java/kritor/service/Authentication.kt | 49 +++++++++++++++++++ .../fuqiuluo/shamrock/config/ActiveTicket.kt | 7 +++ .../shamrock/config/ShamrockConfig.kt | 42 ++++++++++++++-- .../xposed/actions/InitRemoteService.kt | 11 +++-- .../shamrock/xposed/loader/NativeLoader.kt | 2 - .../java/qq/service/internals/MSFHandler.kt | 13 +++-- 10 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 annotations/src/main/java/kritor/service/Grpc.kt create mode 100644 xposed/src/main/assets/config.properties create mode 100644 xposed/src/main/java/kritor/service/Authentication.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/config/ActiveTicket.kt diff --git a/annotations/src/main/java/kritor/service/Grpc.kt b/annotations/src/main/java/kritor/service/Grpc.kt new file mode 100644 index 0000000..0a66477 --- /dev/null +++ b/annotations/src/main/java/kritor/service/Grpc.kt @@ -0,0 +1,7 @@ +package kritor.service + +@Target(AnnotationTarget.FUNCTION) +annotation class Grpc( + val serviceName: String, + val funcName: String +) \ No newline at end of file diff --git a/kritor b/kritor index 0dd59dd..f4fa157 160000 --- a/kritor +++ b/kritor @@ -1 +1 @@ -Subproject commit 0dd59dd2d43324ba13f14e2b8ebc5df07ed9821d +Subproject commit f4fa15754e266182b5f2c08c54c88c21c61eb065 diff --git a/xposed/src/main/assets/config.properties b/xposed/src/main/assets/config.properties new file mode 100644 index 0000000..6505ef1 --- /dev/null +++ b/xposed/src/main/assets/config.properties @@ -0,0 +1,43 @@ +# Shamrock Config + +# 资源上传群组 +resource_group=883536416 + +# 强制使用平板模式 +force_tablet=false + +# 被动(反向)RPC开关 +passive_rpc=false +# 被动(反向)RPC地址 +rpc_address= +# 第一个被动RPC鉴权token +rpc_address.ticket= +# 如果有多个请使用 +# 我是第二个地址 +#rpc_address.1= +# 第二个被动RPC鉴权token +#rpc_address.1.ticket= + +# 主动(正向)RPC开关 +active_rpc=false +# 主动(正向)RPC端口 +rpc_port=5700 +# 主动RPC鉴权token +active_ticket= +# 多鉴权token支持 +# 第二个主动RPC鉴权token +#active_ticket.1= + +# 自回复开关 +#alive_reply=false +# 自回复消息 +enable_self_message=false + +# 旧BDH兼容开关 +enable_old_bdh=false + +# 反JVM调用栈跟踪 +anti_jvm_trace=true + +# 调试模式 +#debug=false \ No newline at end of file diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index 80c0706..7aab8e0 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -1,20 +1,32 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) package kritor.server import io.grpc.ServerBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kritor.service.* import moe.fuqiuluo.shamrock.helper.LogCenter +import kotlin.coroutines.CoroutineContext class KritorServer( private val port: Int -) { +): CoroutineScope { private val server = ServerBuilder .forPort(port) + .addService(Authentication) .build()!! fun start(block: Boolean = false) { LogCenter.log("KritorServer started at port $port.") server.start() + if (block) { server.awaitTermination() } } + + override val coroutineContext: CoroutineContext = + Dispatchers.IO.limitedParallelism(12) } \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/Authentication.kt b/xposed/src/main/java/kritor/service/Authentication.kt new file mode 100644 index 0000000..a6c7814 --- /dev/null +++ b/xposed/src/main/java/kritor/service/Authentication.kt @@ -0,0 +1,49 @@ +package kritor.service + +import io.kritor.AuthCode +import io.kritor.AuthReq +import io.kritor.AuthRsp +import io.kritor.AuthenticationGrpcKt +import io.kritor.authRsp +import moe.fuqiuluo.shamrock.config.ActiveTicket +import moe.fuqiuluo.shamrock.config.ShamrockConfig +import qq.service.QQInterfaces + +object Authentication: AuthenticationGrpcKt.AuthenticationCoroutineImplBase() { + @Grpc("Authentication", "Auth") + override suspend fun auth(request: AuthReq): AuthRsp { + if (QQInterfaces.app.account != request.account) { + return authRsp { + code = AuthCode.NO_ACCOUNT + msg = "No such account" + } + } + + val activeTicketName = ActiveTicket.name() + var index = 0 + while (true) { + val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null) + if (ticket.isNullOrEmpty()) { + if (index == 0) { + return authRsp { + code = AuthCode.OK + msg = "OK" + } + } else { + break + } + } else if (ticket == request.ticket) { + return authRsp { + code = AuthCode.OK + msg = "OK" + } + } + index++ + } + + return authRsp { + code = AuthCode.NO_TICKET + msg = "Invalid ticket" + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ActiveTicket.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ActiveTicket.kt new file mode 100644 index 0000000..4354770 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ActiveTicket.kt @@ -0,0 +1,7 @@ +package moe.fuqiuluo.shamrock.config + +object ActiveTicket: ConfigKey() { + override fun name(): String = "active_ticket" + + override fun default(): String = "" +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt index 9ca6dac..730a00d 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt @@ -1,6 +1,8 @@ package moe.fuqiuluo.shamrock.config import android.content.Intent +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader.moduleLoader import mqq.app.MobileQQ import java.util.Properties @@ -17,11 +19,22 @@ private val configKeys = setOf( ResourceGroup, RPCAddress, RPCPort, - ) internal object ShamrockConfig: Properties() { init { + if (!configFile.exists()) { + moduleLoader.getResourceAsStream("assets/config.properties")?.use { + configDir.resolve("default.prop").outputStream().use { output -> + it.copyTo(output) + } + } + moduleLoader.getResourceAsStream("assets/config.properties")?.use { + configFile.outputStream().use { output -> + it.copyTo(output) + } + } + } if (configFile.exists()) configFile.inputStream().use { load(it) } @@ -40,13 +53,34 @@ internal object ShamrockConfig: Properties() { fun updateConfig(intent: Intent? = null) { intent?.let { for (key in configKeys) { - val value = intent.getStringExtra(key.name()) - if (value != null) setProperty(key.name(), value) + when (key.default()) { + is String -> { + val value = intent.getStringExtra(key.name()) + if (value != null) setProperty(key.name(), value) + } + is Boolean -> { + val value = intent.getBooleanExtra(key.name(), key.default() as Boolean) + setProperty(key.name(), value.toString()) + } + is Int -> { + val value = intent.getIntExtra(key.name(), key.default() as Int) + setProperty(key.name(), value.toString()) + } + is Long -> { + val value = intent.getLongExtra(key.name(), key.default() as Long) + setProperty(key.name(), value.toString()) + } + } } + + if (getProperty(ActiveTicket.name()).isNullOrEmpty()) { + setProperty(ActiveTicket.name(), "") // 初始化ticket + } + setProperty(IsInit.name(), "true") } configFile.outputStream().use { - store(it, "Shamrock Config ${System.currentTimeMillis()}") + store(it, "Shamrock Config") } } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt index 7677640..6d7773d 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt @@ -4,6 +4,8 @@ package moe.fuqiuluo.shamrock.xposed.actions import android.content.Context import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import kritor.server.KritorServer import moe.fuqiuluo.shamrock.config.ActiveRPC import moe.fuqiuluo.shamrock.config.RPCPort @@ -17,10 +19,11 @@ private lateinit var server: KritorServer @XposedHook(Process.MAIN, priority = 10) internal class InitRemoteService : IAction { override fun invoke(ctx: Context) { - if (ActiveRPC.get() && !::server.isInitialized) { - server = KritorServer(RPCPort.get()) - server.start() + GlobalScope.launch { + if (ActiveRPC.get() && !::server.isInitialized) { + server = KritorServer(RPCPort.get()) + server.start() + } } - } } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/NativeLoader.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/NativeLoader.kt index 35eaf94..a2a9c68 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/NativeLoader.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/NativeLoader.kt @@ -34,8 +34,6 @@ internal object NativeLoader { XposedBridge.log("[Shamrock] 反射检测到 Android x86") true } else false - }.onFailure { - XposedBridge.log("[Shamrock] ${it.stackTraceToString()}") }.getOrElse { false } private fun getLibFilePath(name: String): String { diff --git a/xposed/src/main/java/qq/service/internals/MSFHandler.kt b/xposed/src/main/java/qq/service/internals/MSFHandler.kt index 900ecf6..a00271b 100644 --- a/xposed/src/main/java/qq/service/internals/MSFHandler.kt +++ b/xposed/src/main/java/qq/service/internals/MSFHandler.kt @@ -4,6 +4,8 @@ import com.tencent.qphone.base.remote.FromServiceMsg import com.tencent.qphone.base.remote.ToServiceMsg import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter typealias MsfPush = (FromServiceMsg) -> Unit typealias MsfResp = (ToServiceMsg, FromServiceMsg) -> Unit @@ -45,8 +47,13 @@ internal object MSFHandler { } fun onResp(toServiceMsg: ToServiceMsg, fromServiceMsg: FromServiceMsg) { - val cmd = toServiceMsg.getAttribute("respkey") as Int - val resp = mRespHandler[cmd] - resp?.invoke(toServiceMsg, fromServiceMsg) + runCatching { + val cmd = toServiceMsg.getAttribute("__respkey") as? Int? + ?: return@runCatching + val resp = mRespHandler[cmd] + resp?.invoke(toServiceMsg, fromServiceMsg) + }.onFailure { + LogCenter.log("MSF.onResp failed: ${it.message}", Level.ERROR) + } } } \ No newline at end of file From 1afc0ac6a6f7c1087448cf9e916208d6720c6d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sun, 10 Mar 2024 07:57:23 +0800 Subject: [PATCH 09/20] =?UTF-8?q?`Shamrock`:=20=E4=BF=AE=E5=A4=8D=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8TLS=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .../main/java/kritor/server/KritorServer.kt | 5 +++-- .../fuqiuluo/shamrock/config/ShamrockConfig.kt | 1 + .../xposed/actions/InitRemoteService.kt | 18 +++++++++++++++--- .../java/qq/service/internals/MSFHandler.kt | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index 7aab8e0..a568247 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -1,6 +1,8 @@ @file:OptIn(ExperimentalCoroutinesApi::class) package kritor.server +import io.grpc.Grpc +import io.grpc.InsecureServerCredentials import io.grpc.ServerBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -13,8 +15,7 @@ import kotlin.coroutines.CoroutineContext class KritorServer( private val port: Int ): CoroutineScope { - private val server = ServerBuilder - .forPort(port) + private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) .addService(Authentication) .build()!! diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt index 730a00d..ee27847 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/ShamrockConfig.kt @@ -14,6 +14,7 @@ private val configFile = configDir.resolve("config.prop") private val configKeys = setOf( ActiveRPC, + AntiJvmTrace, ForceTablet, PassiveRPC, ResourceGroup, diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt index 6d7773d..0be7f92 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt @@ -11,6 +11,8 @@ import moe.fuqiuluo.shamrock.config.ActiveRPC import moe.fuqiuluo.shamrock.config.RPCPort import moe.fuqiuluo.shamrock.config.ShamrockConfig import moe.fuqiuluo.shamrock.config.get +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.symbols.Process import moe.fuqiuluo.symbols.XposedHook @@ -20,9 +22,19 @@ private lateinit var server: KritorServer internal class InitRemoteService : IAction { override fun invoke(ctx: Context) { GlobalScope.launch { - if (ActiveRPC.get() && !::server.isInitialized) { - server = KritorServer(RPCPort.get()) - server.start() + runCatching { + if (ActiveRPC.get()) { + if (!::server.isInitialized) { + server = KritorServer(RPCPort.get()) + server.start() + } + } else { + LogCenter.log("ActiveRPC is disabled, KritorServer will not be started.") + } + + + }.onFailure { + LogCenter.log("Start RPC failed: ${it.message}", Level.ERROR) } } } diff --git a/xposed/src/main/java/qq/service/internals/MSFHandler.kt b/xposed/src/main/java/qq/service/internals/MSFHandler.kt index a00271b..7883266 100644 --- a/xposed/src/main/java/qq/service/internals/MSFHandler.kt +++ b/xposed/src/main/java/qq/service/internals/MSFHandler.kt @@ -48,7 +48,7 @@ internal object MSFHandler { fun onResp(toServiceMsg: ToServiceMsg, fromServiceMsg: FromServiceMsg) { runCatching { - val cmd = toServiceMsg.getAttribute("__respkey") as? Int? + val cmd = toServiceMsg.getAttribute("shamrock_uid") as? Int? ?: return@runCatching val resp = mRespHandler[cmd] resp?.invoke(toServiceMsg, fromServiceMsg) From bbdfb7c04e0930cbc880f78393bd593683fe21a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sun, 10 Mar 2024 16:04:52 +0800 Subject: [PATCH 10/20] =?UTF-8?q?`Shamrock`:=20=E5=AE=9E=E7=8E=B0=E9=89=B4?= =?UTF-8?q?=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- kritor | 2 +- .../main/java/kritor/auth/AuthInterceptor.kt | 64 ++++++++++++++ .../main/java/kritor/server/KritorServer.kt | 5 +- .../java/kritor/service/ContactService.kt | 47 ++++++++++ .../fuqiuluo/shamrock/helper/ContactHelper.kt | 32 ------- .../moe/fuqiuluo/shamrock/utils/AudioUtils.kt | 2 +- .../java/qq/service/contact/ContactHelper.kt | 87 +++++++++++++++++++ .../LocalCacheHelper.kt | 2 +- 8 files changed, 204 insertions(+), 37 deletions(-) create mode 100644 xposed/src/main/java/kritor/auth/AuthInterceptor.kt create mode 100644 xposed/src/main/java/kritor/service/ContactService.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/helper/ContactHelper.kt create mode 100644 xposed/src/main/java/qq/service/contact/ContactHelper.kt rename xposed/src/main/java/qq/service/{contact => internals}/LocalCacheHelper.kt (95%) diff --git a/kritor b/kritor index f4fa157..505d80b 160000 --- a/kritor +++ b/kritor @@ -1 +1 @@ -Subproject commit f4fa15754e266182b5f2c08c54c88c21c61eb065 +Subproject commit 505d80b2eec8d5f226ed28390fb15f4ca41b4c70 diff --git a/xposed/src/main/java/kritor/auth/AuthInterceptor.kt b/xposed/src/main/java/kritor/auth/AuthInterceptor.kt new file mode 100644 index 0000000..37f4186 --- /dev/null +++ b/xposed/src/main/java/kritor/auth/AuthInterceptor.kt @@ -0,0 +1,64 @@ +package kritor.auth + +import io.grpc.ForwardingServerCallListener +import io.grpc.Metadata +import io.grpc.ServerCall +import io.grpc.ServerCallHandler +import io.grpc.ServerInterceptor +import moe.fuqiuluo.shamrock.config.ActiveTicket +import moe.fuqiuluo.shamrock.config.ShamrockConfig + +object AuthInterceptor: ServerInterceptor { + /** + * Intercept [ServerCall] dispatch by the `next` [ServerCallHandler]. General + * semantics of [ServerCallHandler.startCall] apply and the returned + * [io.grpc.ServerCall.Listener] must not be `null`. + * + * + * If the implementation throws an exception, `call` will be closed with an error. + * Implementations must not throw an exception if they started processing that may use `call` on another thread. + * + * @param call object to receive response messages + * @param headers which can contain extra call metadata from [ClientCall.start], + * e.g. authentication credentials. + * @param next next processor in the interceptor chain + * @return listener for processing incoming messages for `call`, never `null`. + */ + override fun interceptCall( + call: ServerCall, + headers: Metadata?, + next: ServerCallHandler + ): ServerCall.Listener { + val methodName = call.methodDescriptor.fullMethodName + val ticket = getAllTicket() + if (ticket.isNotEmpty() && !methodName.startsWith("Auth")) { + val ticketHeader = headers?.get(Metadata.Key.of("ticket", Metadata.ASCII_STRING_MARSHALLER)) + if (ticketHeader == null || ticketHeader !in ticket) { + call.close(io.grpc.Status.UNAUTHENTICATED.withDescription("Invalid ticket"), Metadata()) + return object: ServerCall.Listener() {} + } + } + return object: ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, headers)) { + } + } + + private fun getAllTicket(): List { + val result = arrayListOf() + val activeTicketName = ActiveTicket.name() + var index = 0 + while (true) { + val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null) + if (ticket.isNullOrEmpty()) { + if (index == 0) { + return result + } else { + break + } + } else { + result.add(ticket) + } + index++ + } + return result + } +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index a568247..b3b1cb9 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -3,11 +3,10 @@ package kritor.server import io.grpc.Grpc import io.grpc.InsecureServerCredentials -import io.grpc.ServerBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.asExecutor import kritor.service.* import moe.fuqiuluo.shamrock.helper.LogCenter import kotlin.coroutines.CoroutineContext @@ -16,7 +15,9 @@ class KritorServer( private val port: Int ): CoroutineScope { private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) + .executor(Dispatchers.IO.asExecutor()) .addService(Authentication) + .addService(ContactService) .build()!! fun start(block: Boolean = false) { diff --git a/xposed/src/main/java/kritor/service/ContactService.kt b/xposed/src/main/java/kritor/service/ContactService.kt new file mode 100644 index 0000000..c237678 --- /dev/null +++ b/xposed/src/main/java/kritor/service/ContactService.kt @@ -0,0 +1,47 @@ +package kritor.service + +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.AuthCode +import io.kritor.authRsp +import io.kritor.contact.ContactServiceGrpcKt +import io.kritor.contact.ProfileCard +import io.kritor.contact.ProfileCardRequest +import io.kritor.contact.profileCard +import moe.fuqiuluo.shamrock.config.ActiveTicket +import moe.fuqiuluo.shamrock.config.ShamrockConfig +import qq.service.contact.ContactHelper + +object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() { + override suspend fun getProfileCard(request: ProfileCardRequest): ProfileCard { + val uin = if (request.hasUin()) request.uin + else ContactHelper.getUinByUidAsync(request.uid).toLong() + val contact = ContactHelper.getProfileCard(uin) + + contact.onFailure { + throw StatusRuntimeException(Status.INTERNAL + .withDescription(it.stackTraceToString()) + ) + } + + contact.onSuccess { + return profileCard { + this.uin = it.uin.toLong() + this.uid = if (request.hasUid()) request.uid + else ContactHelper.getUidByUinAsync(it.uin.toLong()) + this.name = it.strNick ?: "" + this.remark = it.strReMark ?: "" + this.level = it.iQQLevel + this.birthday = it.lBirthday + this.loginDay = it.lLoginDays.toInt() + this.voteCnt = it.lVoteCount.toInt() + this.qid = it.qid ?: "" + this.isSchoolVerified = it.schoolVerifiedFlag + } + } + + throw StatusRuntimeException(Status.INTERNAL + .withDescription("logic failed") + ) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/ContactHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/ContactHelper.kt deleted file mode 100644 index 4cc9ac0..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/ContactHelper.kt +++ /dev/null @@ -1,32 +0,0 @@ -package moe.fuqiuluo.shamrock.helper - -import kotlinx.coroutines.suspendCancellableCoroutine -import moe.fuqiuluo.shamrock.internals.NTServiceFetcher -import kotlin.coroutines.resume - -internal object ContactHelper { - suspend fun getUinByUidAsync(uid: String): String { - if (uid.isBlank() || uid == "0") { - return "0" - } - - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - - return suspendCancellableCoroutine { continuation -> - sessionService.uixConvertService.getUin(hashSetOf(uid)) { - continuation.resume(it) - } - }[uid]?.toString() ?: "0" - } - - suspend fun getUidByUinAsync(peerId: Long): String { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - return suspendCancellableCoroutine { continuation -> - sessionService.uixConvertService.getUid(hashSetOf(peerId)) { - continuation.resume(it) - } - }[peerId]!! - } -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/AudioUtils.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/AudioUtils.kt index f8783d0..0c512b6 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/AudioUtils.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/AudioUtils.kt @@ -8,7 +8,7 @@ import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFprobeKit import com.arthenica.ffmpegkit.ReturnCode import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil -import qq.service.contact.LocalCacheHelper +import qq.service.internals.LocalCacheHelper import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import java.io.File diff --git a/xposed/src/main/java/qq/service/contact/ContactHelper.kt b/xposed/src/main/java/qq/service/contact/ContactHelper.kt new file mode 100644 index 0000000..3ddc0a8 --- /dev/null +++ b/xposed/src/main/java/qq/service/contact/ContactHelper.kt @@ -0,0 +1,87 @@ +package qq.service.contact + +import com.tencent.common.app.AppInterface +import com.tencent.mobileqq.data.Card +import com.tencent.mobileqq.profilecard.api.IProfileDataService +import com.tencent.mobileqq.profilecard.api.IProfileProtocolService +import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import moe.fuqiuluo.shamrock.internals.NTServiceFetcher +import qq.service.QQInterfaces +import kotlin.coroutines.resume + +object ContactHelper: QQInterfaces() { + private val refreshCardLock by lazy { Mutex() } + + suspend fun getProfileCard(uin: Long): Result { + return getProfileCardFromCache(uin).onFailure { + return refreshAndGetProfileCard(uin) + } + } + + fun getProfileCardFromCache(uin: Long): Result { + val profileDataService = app + .getRuntimeService(IProfileDataService::class.java, "all") + val card = profileDataService.getProfileCard(uin.toString(), true) + return if (card == null || card.strNick.isNullOrEmpty()) { + Result.failure(Exception("unable to fetch profile card")) + } else { + Result.success(card) + } + } + + suspend fun refreshAndGetProfileCard(uin: Long): Result { + require(app is AppInterface) + val dataService = app + .getRuntimeService(IProfileDataService::class.java, "all") + val card = refreshCardLock.withLock { + suspendCancellableCoroutine { + app.addObserver(object: ProfileCardObserver() { + override fun onGetProfileCard(success: Boolean, obj: Any) { + app.removeObserver(this) + if (!success || obj !is Card) { + it.resume(null) + } else { + dataService.saveProfileCard(obj) + it.resume(obj) + } + } + }) + app.getRuntimeService(IProfileProtocolService::class.java, "all") + .requestProfileCard(app.currentUin, uin.toString(), 12, 0L, 0.toByte(), 0L, 0L, null, "", 0L, 10004, null, 0.toByte()) + } + } + return if (card == null || card.strNick.isNullOrEmpty()) { + Result.failure(Exception("unable to fetch profile card")) + } else { + Result.success(card) + } + } + + suspend fun getUinByUidAsync(uid: String): String { + if (uid.isBlank() || uid == "0") { + return "0" + } + + val kernelService = NTServiceFetcher.kernelService + val sessionService = kernelService.wrapperSession + + return suspendCancellableCoroutine { continuation -> + sessionService.uixConvertService.getUin(hashSetOf(uid)) { + continuation.resume(it) + } + }[uid]?.toString() ?: "0" + } + + suspend fun getUidByUinAsync(peerId: Long): String { + val kernelService = NTServiceFetcher.kernelService + val sessionService = kernelService.wrapperSession + return suspendCancellableCoroutine { continuation -> + sessionService.uixConvertService.getUid(hashSetOf(peerId)) { + continuation.resume(it) + } + }[peerId]!! + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/contact/LocalCacheHelper.kt b/xposed/src/main/java/qq/service/internals/LocalCacheHelper.kt similarity index 95% rename from xposed/src/main/java/qq/service/contact/LocalCacheHelper.kt rename to xposed/src/main/java/qq/service/internals/LocalCacheHelper.kt index 17ed79f..30fefb9 100644 --- a/xposed/src/main/java/qq/service/contact/LocalCacheHelper.kt +++ b/xposed/src/main/java/qq/service/internals/LocalCacheHelper.kt @@ -1,4 +1,4 @@ -package qq.service.contact +package qq.service.internals import moe.fuqiuluo.shamrock.utils.FileUtils import mqq.app.MobileQQ From a528030cbb9fc566234422b4ee199ee6445eb1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sun, 10 Mar 2024 16:05:20 +0800 Subject: [PATCH 11/20] =?UTF-8?q?`Shamrock`:=20=E5=AE=9E=E7=8E=B0=E9=89=B4?= =?UTF-8?q?=E6=9D=83=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- xposed/src/main/java/kritor/server/KritorServer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index b3b1cb9..376f80b 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asExecutor +import kritor.auth.AuthInterceptor import kritor.service.* import moe.fuqiuluo.shamrock.helper.LogCenter import kotlin.coroutines.CoroutineContext @@ -16,6 +17,7 @@ class KritorServer( ): CoroutineScope { private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) .executor(Dispatchers.IO.asExecutor()) + .intercept(AuthInterceptor) .addService(Authentication) .addService(ContactService) .build()!! From c16f9d543cefd78b5510829df9ad7cdf880cd99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Mon, 11 Mar 2024 12:30:42 +0800 Subject: [PATCH 12/20] =?UTF-8?q?`Shamrock`:=20=E5=AE=9E=E7=8E=B0=E8=81=94?= =?UTF-8?q?=E7=B3=BB=E4=BA=BA=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- kritor | 2 +- .../main/java/kritor/auth/AuthInterceptor.kt | 2 +- .../java/kritor/service/Authentication.kt | 17 +++ .../java/kritor/service/ContactService.kt | 119 +++++++++++++++++- .../service/contact/ProfileProtocolConst.kt | 39 ++++++ 5 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 xposed/src/main/java/qq/service/contact/ProfileProtocolConst.kt diff --git a/kritor b/kritor index 505d80b..201e91e 160000 --- a/kritor +++ b/kritor @@ -1 +1 @@ -Subproject commit 505d80b2eec8d5f226ed28390fb15f4ca41b4c70 +Subproject commit 201e91e73225ce3f1ec098c06a5cf6a717d913e5 diff --git a/xposed/src/main/java/kritor/auth/AuthInterceptor.kt b/xposed/src/main/java/kritor/auth/AuthInterceptor.kt index 37f4186..b195794 100644 --- a/xposed/src/main/java/kritor/auth/AuthInterceptor.kt +++ b/xposed/src/main/java/kritor/auth/AuthInterceptor.kt @@ -42,7 +42,7 @@ object AuthInterceptor: ServerInterceptor { } } - private fun getAllTicket(): List { + fun getAllTicket(): List { val result = arrayListOf() val activeTicketName = ActiveTicket.name() var index = 0 diff --git a/xposed/src/main/java/kritor/service/Authentication.kt b/xposed/src/main/java/kritor/service/Authentication.kt index a6c7814..d0def44 100644 --- a/xposed/src/main/java/kritor/service/Authentication.kt +++ b/xposed/src/main/java/kritor/service/Authentication.kt @@ -1,10 +1,16 @@ package kritor.service +import io.grpc.Status +import io.grpc.StatusRuntimeException import io.kritor.AuthCode import io.kritor.AuthReq import io.kritor.AuthRsp import io.kritor.AuthenticationGrpcKt +import io.kritor.GetAuthStateReq +import io.kritor.GetAuthStateRsp import io.kritor.authRsp +import io.kritor.getAuthStateRsp +import kritor.auth.AuthInterceptor import moe.fuqiuluo.shamrock.config.ActiveTicket import moe.fuqiuluo.shamrock.config.ShamrockConfig import qq.service.QQInterfaces @@ -46,4 +52,15 @@ object Authentication: AuthenticationGrpcKt.AuthenticationCoroutineImplBase() { msg = "Invalid ticket" } } + + @Grpc("Authentication", "GetAuthState") + override suspend fun getAuthState(request: GetAuthStateReq): GetAuthStateRsp { + if (request.account != QQInterfaces.app.account) { + throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account")) + } + + return getAuthStateRsp { + isRequiredAuth = AuthInterceptor.getAllTicket().isNotEmpty() + } + } } \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/ContactService.kt b/xposed/src/main/java/kritor/service/ContactService.kt index c237678..e973a65 100644 --- a/xposed/src/main/java/kritor/service/ContactService.kt +++ b/xposed/src/main/java/kritor/service/ContactService.kt @@ -1,18 +1,37 @@ package kritor.service +import android.os.Bundle +import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi +import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.* +import com.tencent.mobileqq.profilecard.api.IProfileProtocolService +import com.tencent.mobileqq.qroute.QRoute import io.grpc.Status import io.grpc.StatusRuntimeException -import io.kritor.AuthCode -import io.kritor.authRsp import io.kritor.contact.ContactServiceGrpcKt +import io.kritor.contact.GetUidRequest +import io.kritor.contact.GetUidResponse +import io.kritor.contact.GetUinByUidRequest +import io.kritor.contact.GetUinByUidResponse +import io.kritor.contact.IsBlackListUserRequest +import io.kritor.contact.IsBlackListUserResponse import io.kritor.contact.ProfileCard import io.kritor.contact.ProfileCardRequest +import io.kritor.contact.SetProfileCardRequest +import io.kritor.contact.SetProfileCardResponse +import io.kritor.contact.StrangerExt +import io.kritor.contact.StrangerInfo +import io.kritor.contact.StrangerInfoRequest import io.kritor.contact.profileCard -import moe.fuqiuluo.shamrock.config.ActiveTicket -import moe.fuqiuluo.shamrock.config.ShamrockConfig +import io.kritor.contact.strangerInfo +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import qq.service.QQInterfaces import qq.service.contact.ContactHelper +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() { + @Grpc("ContactService", "GetProfileCard") override suspend fun getProfileCard(request: ProfileCardRequest): ProfileCard { val uin = if (request.hasUin()) request.uin else ContactHelper.getUinByUidAsync(request.uid).toLong() @@ -44,4 +63,96 @@ object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() { .withDescription("logic failed") ) } + + @Grpc("ContactService", "GetStrangerInfo") + override suspend fun getStrangerInfo(request: StrangerInfoRequest): StrangerInfo { + val userId = request.uin + val info = ContactHelper.refreshAndGetProfileCard(userId).onFailure { + throw StatusRuntimeException(Status.INTERNAL + .withCause(it) + .withDescription("Unable to fetch stranger info") + ) + }.getOrThrow() + + return strangerInfo { + this.uid = ContactHelper.getUidByUinAsync(userId) + this.uin = (info.uin ?: "0").toLong() + this.name = info.strNick ?: "" + this.level = info.iQQLevel + this.loginDay = info.lLoginDays.toInt() + this.voteCnt = info.lVoteCount.toInt() + this.qid = info.qid ?: "" + this.isSchoolVerified = info.schoolVerifiedFlag + this.ext = StrangerExt.newBuilder() + .setBigVip(info.bBigClubVipOpen == 1.toByte()) + .setHollywoodVip(info.bHollywoodVipOpen == 1.toByte()) + .setQqVip(info.bQQVipOpen == 1.toByte()) + .setSuperVip(info.bSuperQQOpen == 1.toByte()) + .setVoted(info.bVoted == 1.toByte()) + .build().toByteString() + } + } + + @Grpc("ContactService", "GetUid") + override suspend fun getUid(request: GetUidRequest): GetUidResponse { + return GetUidResponse.newBuilder().apply { + request.uinList.forEach { + putUid(it, ContactHelper.getUidByUinAsync(it)) + } + }.build() + } + + @Grpc("ContactService", "GetUinByUid") + override suspend fun getUinByUid(request: GetUinByUidRequest): GetUinByUidResponse { + return GetUinByUidResponse.newBuilder().apply { + request.uidList.forEach { + putUin(it, ContactHelper.getUinByUidAsync(it).toLong()) + } + }.build() + } + + @Grpc("ContactService", "SetProfileCard") + override suspend fun setProfileCard(request: SetProfileCardRequest): SetProfileCardResponse { + val bundle = Bundle() + val service = QQInterfaces.app + .getRuntimeService(IProfileProtocolService::class.java, "all") + if (request.hasNickName()) { + bundle.putString(KEY_NICK, request.nickName) + } + if (request.hasCompany()) { + bundle.putString(KEY_COMPANY, request.company) + } + if (request.hasEmail()) { + bundle.putString(KEY_EMAIL, request.email) + } + if (request.hasCollege()) { + bundle.putString(KEY_COLLEGE, request.college) + } + if (request.hasPersonalNote()) { + bundle.putString(KEY_PERSONAL_NOTE, request.personalNote) + } + + if (request.hasBirthday()) { + bundle.putInt(KEY_BIRTHDAY, request.birthday) + } + if (request.hasAge()) { + bundle.putInt(KEY_AGE, request.age) + } + + service.setProfileDetail(bundle) + return super.setProfileCard(request) + } + + @Grpc("ContactService", "IsBlackListUser") + override suspend fun isBlackListUser(request: IsBlackListUserRequest): IsBlackListUserResponse { + val blacklistApi = QRoute.api(IProfileCardBlacklistApi::class.java) + val isBlack = withTimeoutOrNull(5000) { + suspendCancellableCoroutine { continuation -> + blacklistApi.isBlackOrBlackedUin(request.uin.toString()) { + continuation.resume(it) + } + } + } ?: false + return IsBlackListUserResponse.newBuilder().setIsBlackListUser(isBlack).build() + } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/contact/ProfileProtocolConst.kt b/xposed/src/main/java/qq/service/contact/ProfileProtocolConst.kt new file mode 100644 index 0000000..cf486c0 --- /dev/null +++ b/xposed/src/main/java/qq/service/contact/ProfileProtocolConst.kt @@ -0,0 +1,39 @@ +package qq.service.contact + +const val CMD_GET_PROFILE_DETAIL = "OidbSvc.0x480_9_IMCore" +const val CMD_SET_PROFILE_DETAIL = "OidbSvc.0x4ff_9_IMCore" +const val KET_INTERESTS = "interests" +const val KEY_AGE = "age" +const val KEY_BIRTHDAY = "birthday" +const val KEY_COLLEGE = "college" +const val KEY_COMPANY = "company" +const val KEY_CONSTELLATION = "key_constellation" +const val KEY_EMAIL = "email" +const val KEY_HOMETOWN = "hometown" +const val KEY_HOMETOWN_DESC = "hometown_desc" +const val KEY_LOCATION = "location" +const val KEY_LOCATION_DESC = "location_desc" +const val KEY_LOCATION_NAME = "location_name" +const val KEY_NICK = "nick" +const val KEY_PARSE_PROFILE_LOCATION = "parse_profile_location" +const val KEY_PERSONAL_NOTE = "personalNote" +const val KEY_PROFESSION = "profession" +const val KEY_SEX = "sex" +const val PARAM_ADD_FRIEND_SOURCE = "addFriendSource" +const val PARAM_COME_FROM_TYPE = "comeFromType" +const val PARAM_GET_CONTROL = "getControl" +const val PARAM_IS_FRIEND = "isFriend" +const val PARAM_LOGIN_SIG = "loginSig" +const val PARAM_QZONE_FEED_TIMESTAMP = "qZoneFeedTimeStamp" +const val PARAM_QZONE_SEED = "qZoneSeed" +const val PARAM_REQ_0X5EB = "req0x5ebList" +const val PARAM_REQ_EXTEND = "reqExtendFriend" +const val PARAM_REQ_MEDAL = "reqMedalWall" +const val PARAM_REQ_SERVICES = "reqServiceList" +const val PARAM_REQ_TEMPLATE = "reqTemplate" +const val PARAM_SEARCH_NAME = "searchName" +const val PARAM_SECURE_SIG = "secureSig" +const val PARAM_SELF_UIN = "selfUin" +const val PARAM_TARGET_UIN = "targetUin" +const val PARAM_TROOP_CODE = "troopCode" +const val PARAM_TROOP_UIN = "troopUin" \ No newline at end of file From cb4268edef7761ea9063604dc86d125d719d02d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Mon, 11 Mar 2024 12:48:11 +0800 Subject: [PATCH 13/20] =?UTF-8?q?`Shamrock`:=20=E5=AE=9E=E7=8E=B0Kritor?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .../main/java/kritor/server/KritorServer.kt | 1 + .../main/java/kritor/service/KritorService.kt | 118 ++++++++++++++++++ .../fuqiuluo/shamrock/utils/DownloadUtils.kt | 2 +- .../service/contact/ProfileProtocolConst.kt | 39 ------ 4 files changed, 120 insertions(+), 40 deletions(-) create mode 100644 xposed/src/main/java/kritor/service/KritorService.kt delete mode 100644 xposed/src/main/java/qq/service/contact/ProfileProtocolConst.kt diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index 376f80b..df27d30 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -20,6 +20,7 @@ class KritorServer( .intercept(AuthInterceptor) .addService(Authentication) .addService(ContactService) + .addService(KritorService) .build()!! fun start(block: Boolean = false) { diff --git a/xposed/src/main/java/kritor/service/KritorService.kt b/xposed/src/main/java/kritor/service/KritorService.kt new file mode 100644 index 0000000..e53387b --- /dev/null +++ b/xposed/src/main/java/kritor/service/KritorService.kt @@ -0,0 +1,118 @@ +package kritor.service + +import android.util.Base64 +import com.tencent.mobileqq.app.QQAppInterface +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.core.ClearCacheRequest +import io.kritor.core.ClearCacheResponse +import io.kritor.core.DownloadFileRequest +import io.kritor.core.DownloadFileResponse +import io.kritor.core.GetCurrentAccountRequest +import io.kritor.core.GetCurrentAccountResponse +import io.kritor.core.GetVersionRequest +import io.kritor.core.GetVersionResponse +import io.kritor.core.KritorServiceGrpcKt +import io.kritor.core.SwitchAccountRequest +import io.kritor.core.SwitchAccountResponse +import io.kritor.core.clearCacheResponse +import io.kritor.core.downloadFileResponse +import io.kritor.core.getCurrentAccountResponse +import io.kritor.core.getVersionResponse +import io.kritor.core.switchAccountResponse +import moe.fuqiuluo.shamrock.tools.ShamrockVersion +import moe.fuqiuluo.shamrock.utils.DownloadUtils +import moe.fuqiuluo.shamrock.utils.FileUtils +import moe.fuqiuluo.shamrock.utils.MD5 +import moe.fuqiuluo.shamrock.utils.MMKVFetcher +import mqq.app.MobileQQ +import qq.service.QQInterfaces +import qq.service.QQInterfaces.Companion.app +import qq.service.contact.ContactHelper +import java.io.File + +object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() { + @Grpc("KritorService", "GetVersion") + override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse { + return getVersionResponse { + this.version = ShamrockVersion + this.appName = "Shamrock" + } + } + + @Grpc("KritorService", "ClearCache") + override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse { + FileUtils.clearCache() + MMKVFetcher.mmkvWithId("audio2silk") + .clear() + return clearCacheResponse {} + } + + @Grpc("KritorService", "GetCurrentAccount") + override suspend fun getCurrentAccount(request: GetCurrentAccountRequest): GetCurrentAccountResponse { + return getCurrentAccountResponse { + this.accountName = if (app is QQAppInterface) app.currentNickname else "unknown" + this.accountUid = app.currentUid ?: "" + this.accountUin = (app.currentUin ?: "0").toLong() + } + } + + @Grpc("KritorService", "DownloadFile") + override suspend fun downloadFile(request: DownloadFileRequest): DownloadFileResponse { + val headerMap = mutableMapOf( + "User-Agent" to "Shamrock" + ) + if (request.hasHeaders()) { + request.headers.split("[\r\n]").forEach { + val pair = it.split("=") + if (pair.size >= 2) { + val (k, v) = pair + headerMap[k] = v + } + } + } + + var tmp = FileUtils.getTmpFile("cache") + if (request.hasBase64()) { + val bytes = Base64.decode(request.base64, Base64.DEFAULT) + tmp.writeBytes(bytes) + } else if(request.hasUrl()) { + if(!DownloadUtils.download( + urlAdr = request.url, + dest = tmp, + headers = headerMap, + threadCount = if (request.hasThreadCnt()) request.threadCnt else 3 + )) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("download failed")) + } + } + tmp = if (!request.hasFileName()) FileUtils.renameByMd5(tmp) + else tmp.parentFile!!.resolve(request.fileName).also { + tmp.renameTo(it) + } + if (request.hasRootPath()) { + tmp = File(request.rootPath).resolve(tmp.name).also { + tmp.renameTo(it) + } + } + + return downloadFileResponse { + this.fileMd5 = MD5.genFileMd5Hex(tmp.absolutePath) + this.fileAbsolutePath = tmp.absolutePath + } + } + + @Grpc("KritorService", "SwitchAccount") + override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse { + val uin = if (request.hasAccountUin()) request.accountUin.toString() + else ContactHelper.getUinByUidAsync(request.accountUid) + val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin } + ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found")) + runCatching { + app.switchAccount(account, null) + }.onFailure { + throw StatusRuntimeException(Status.INTERNAL.withCause(it).withDescription("failed to switch account")) + } + return switchAccountResponse { } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/DownloadUtils.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/DownloadUtils.kt index 1cdaf5a..73d5ed7 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/DownloadUtils.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/utils/DownloadUtils.kt @@ -33,7 +33,7 @@ object DownloadUtils { threadCount: Int = MAX_THREAD, headers: Map = mapOf() ): Boolean { - var threadCnt = if(threadCount == 0) MAX_THREAD else threadCount + var threadCnt = if(threadCount == 0 || threadCount < 0) MAX_THREAD else threadCount val url = URL(urlAdr) val connection = withContext(Dispatchers.IO) { url.openConnection() } as HttpURLConnection headers.forEach { (k, v) -> diff --git a/xposed/src/main/java/qq/service/contact/ProfileProtocolConst.kt b/xposed/src/main/java/qq/service/contact/ProfileProtocolConst.kt deleted file mode 100644 index cf486c0..0000000 --- a/xposed/src/main/java/qq/service/contact/ProfileProtocolConst.kt +++ /dev/null @@ -1,39 +0,0 @@ -package qq.service.contact - -const val CMD_GET_PROFILE_DETAIL = "OidbSvc.0x480_9_IMCore" -const val CMD_SET_PROFILE_DETAIL = "OidbSvc.0x4ff_9_IMCore" -const val KET_INTERESTS = "interests" -const val KEY_AGE = "age" -const val KEY_BIRTHDAY = "birthday" -const val KEY_COLLEGE = "college" -const val KEY_COMPANY = "company" -const val KEY_CONSTELLATION = "key_constellation" -const val KEY_EMAIL = "email" -const val KEY_HOMETOWN = "hometown" -const val KEY_HOMETOWN_DESC = "hometown_desc" -const val KEY_LOCATION = "location" -const val KEY_LOCATION_DESC = "location_desc" -const val KEY_LOCATION_NAME = "location_name" -const val KEY_NICK = "nick" -const val KEY_PARSE_PROFILE_LOCATION = "parse_profile_location" -const val KEY_PERSONAL_NOTE = "personalNote" -const val KEY_PROFESSION = "profession" -const val KEY_SEX = "sex" -const val PARAM_ADD_FRIEND_SOURCE = "addFriendSource" -const val PARAM_COME_FROM_TYPE = "comeFromType" -const val PARAM_GET_CONTROL = "getControl" -const val PARAM_IS_FRIEND = "isFriend" -const val PARAM_LOGIN_SIG = "loginSig" -const val PARAM_QZONE_FEED_TIMESTAMP = "qZoneFeedTimeStamp" -const val PARAM_QZONE_SEED = "qZoneSeed" -const val PARAM_REQ_0X5EB = "req0x5ebList" -const val PARAM_REQ_EXTEND = "reqExtendFriend" -const val PARAM_REQ_MEDAL = "reqMedalWall" -const val PARAM_REQ_SERVICES = "reqServiceList" -const val PARAM_REQ_TEMPLATE = "reqTemplate" -const val PARAM_SEARCH_NAME = "searchName" -const val PARAM_SECURE_SIG = "secureSig" -const val PARAM_SELF_UIN = "selfUin" -const val PARAM_TARGET_UIN = "targetUin" -const val PARAM_TROOP_CODE = "troopCode" -const val PARAM_TROOP_UIN = "troopUin" \ No newline at end of file From ca47f9dbdfa5c82744c89c268c730c028c7e8aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Tue, 12 Mar 2024 18:46:05 +0800 Subject: [PATCH 14/20] =?UTF-8?q?`Shamrock`:=20=E3=82=B0=E3=83=AB=E3=83=BC?= =?UTF-8?q?=E3=83=97=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=B5=E3=83=BC?= =?UTF-8?q?=E3=83=93=E3=82=B9=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- kritor | 2 +- .../main/java/kritor/server/KritorServer.kt | 3 + .../java/kritor/service/ContactService.kt | 31 +- .../main/java/kritor/service/FriendService.kt | 41 ++ .../java/kritor/service/GroupFileService.kt | 138 ++++ .../main/java/kritor/service/GroupService.kt | 406 ++++++++++++ .../main/java/kritor/service/KritorService.kt | 7 +- .../moe/fuqiuluo/shamrock/tools/Kotlinx.kt | 4 +- .../src/main/java/qq/service/QQInterfaces.kt | 120 +++- .../java/qq/service/contact/ContactHelper.kt | 95 ++- .../java/qq/service/file/GroupFileHelper.kt | 162 +++++ .../java/qq/service/friend/FriendHelper.kt | 40 ++ .../main/java/qq/service/group/GroupHelper.kt | 617 ++++++++++++++++++ .../qq/service/group/NotJoinedGroupInfo.kt | 17 + .../qq/service/group/ProhibitedMemberInfo.kt | 10 + .../java/qq/service/internals/MSFHandler.kt | 14 +- 16 files changed, 1693 insertions(+), 14 deletions(-) create mode 100644 xposed/src/main/java/kritor/service/FriendService.kt create mode 100644 xposed/src/main/java/kritor/service/GroupFileService.kt create mode 100644 xposed/src/main/java/kritor/service/GroupService.kt create mode 100644 xposed/src/main/java/qq/service/file/GroupFileHelper.kt create mode 100644 xposed/src/main/java/qq/service/friend/FriendHelper.kt create mode 100644 xposed/src/main/java/qq/service/group/GroupHelper.kt create mode 100644 xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt create mode 100644 xposed/src/main/java/qq/service/group/ProhibitedMemberInfo.kt diff --git a/kritor b/kritor index 201e91e..f007631 160000 --- a/kritor +++ b/kritor @@ -1 +1 @@ -Subproject commit 201e91e73225ce3f1ec098c06a5cf6a717d913e5 +Subproject commit f007631c7e17fe6220055a404fdaaa6e7a24a7ef diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index df27d30..f7fa4c1 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -21,6 +21,9 @@ class KritorServer( .addService(Authentication) .addService(ContactService) .addService(KritorService) + .addService(FriendService) + .addService(GroupService) + .addService(GroupFileService) .build()!! fun start(block: Boolean = false) { diff --git a/xposed/src/main/java/kritor/service/ContactService.kt b/xposed/src/main/java/kritor/service/ContactService.kt index e973a65..267fd49 100644 --- a/xposed/src/main/java/kritor/service/ContactService.kt +++ b/xposed/src/main/java/kritor/service/ContactService.kt @@ -21,8 +21,11 @@ import io.kritor.contact.SetProfileCardResponse import io.kritor.contact.StrangerExt import io.kritor.contact.StrangerInfo import io.kritor.contact.StrangerInfoRequest +import io.kritor.contact.VoteUserRequest +import io.kritor.contact.VoteUserResponse import io.kritor.contact.profileCard import io.kritor.contact.strangerInfo +import io.kritor.contact.voteUserResponse import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import qq.service.QQInterfaces @@ -31,10 +34,32 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() { + @Grpc("ContactService", "VoteUser") + override suspend fun voteUser(request: VoteUserRequest): VoteUserResponse { + ContactHelper.voteUser(when(request.accountCase!!) { + VoteUserRequest.AccountCase.ACCOUNT_UIN -> request.accountUin + VoteUserRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid).toLong() + VoteUserRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("account not set") + ) + }, request.voteCount).onFailure { + throw StatusRuntimeException(Status.INTERNAL + .withDescription(it.stackTraceToString()) + ) + } + return voteUserResponse { } + } + @Grpc("ContactService", "GetProfileCard") override suspend fun getProfileCard(request: ProfileCardRequest): ProfileCard { - val uin = if (request.hasUin()) request.uin - else ContactHelper.getUinByUidAsync(request.uid).toLong() + val uin = when (request.accountCase!!) { + ProfileCardRequest.AccountCase.ACCOUNT_UIN -> request.accountUin + ProfileCardRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid).toLong() + ProfileCardRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("account not set") + ) + } + val contact = ContactHelper.getProfileCard(uin) contact.onFailure { @@ -46,7 +71,7 @@ object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() { contact.onSuccess { return profileCard { this.uin = it.uin.toLong() - this.uid = if (request.hasUid()) request.uid + this.uid = if (request.hasAccountUid()) request.accountUid else ContactHelper.getUidByUinAsync(it.uin.toLong()) this.name = it.strNick ?: "" this.remark = it.strReMark ?: "" diff --git a/xposed/src/main/java/kritor/service/FriendService.kt b/xposed/src/main/java/kritor/service/FriendService.kt new file mode 100644 index 0000000..6a2c862 --- /dev/null +++ b/xposed/src/main/java/kritor/service/FriendService.kt @@ -0,0 +1,41 @@ +package kritor.service + +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.friend.FriendServiceGrpcKt +import io.kritor.friend.GetFriendListRequest +import io.kritor.friend.GetFriendListResponse +import io.kritor.friend.friendData +import io.kritor.friend.friendExt +import io.kritor.friend.getFriendListResponse +import qq.service.contact.ContactHelper +import qq.service.friend.FriendHelper + +object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() { + @Grpc("FriendService", "GetFriendList") + override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse { + val friendList = FriendHelper.getFriendList(if(request.hasRefresh()) request.refresh else false).onFailure { + throw StatusRuntimeException(Status.INTERNAL + .withDescription(it.stackTraceToString()) + ) + }.getOrThrow() + + return getFriendListResponse { + friendList.forEach { + this.friendList.add(friendData { + uin = it.uin.toLong() + uid = ContactHelper.getUidByUinAsync(uin) + qid = "" + nick = it.name ?: "" + remark = it.remark ?: "" + age = it.age + level = 0 + gender = it.gender.toInt() + groupId = it.groupid + ext = friendExt {}.toByteString() + }) + } + } + } + +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/GroupFileService.kt b/xposed/src/main/java/kritor/service/GroupFileService.kt new file mode 100644 index 0000000..3dc2121 --- /dev/null +++ b/xposed/src/main/java/kritor/service/GroupFileService.kt @@ -0,0 +1,138 @@ +package kritor.service + +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.file.* +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.symbols.decodeProtobuf +import protobuf.auto.toByteArray +import protobuf.oidb.cmd0x6d7.CreateFolderReq +import protobuf.oidb.cmd0x6d7.DeleteFolderReq +import protobuf.oidb.cmd0x6d7.Oidb0x6d7ReqBody +import protobuf.oidb.cmd0x6d7.Oidb0x6d7RespBody +import protobuf.oidb.cmd0x6d7.RenameFolderReq +import qq.service.QQInterfaces +import qq.service.file.GroupFileHelper +import qq.service.file.GroupFileHelper.getGroupFileSystemInfo +import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 +import tencent.im.oidb.cmd0x6d8.oidb_0x6d8 +import tencent.im.oidb.oidb_sso + +internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() { + @Grpc("GroupFileService", "CreateFolder") + override suspend fun createFolder(request: CreateFolderRequest): CreateFolderResponse { + val data = Oidb0x6d7ReqBody( + createFolder = CreateFolderReq( + groupCode = request.groupId.toULong(), + appId = 3u, + parentFolderId = "/", + folderName = request.name + ) + ).toByteArray() + val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data) + ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val oidbPkg = oidb_sso.OIDBSSOPkg() + oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidbPkg.bytes_bodybuffer.get() + .toByteArray() + .decodeProtobuf() + if (rsp.createFolder?.retCode != 0) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to create folder: ${rsp.createFolder?.retCode}")) + } + return createFolderResponse { + this.id = rsp.createFolder?.folderInfo?.folderId ?: "" + this.usedSpace = 0 + } + } + + @Grpc("GroupFileService", "DeleteFolder") + override suspend fun deleteFolder(request: DeleteFolderRequest): DeleteFolderResponse { + val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody( + deleteFolder = DeleteFolderReq( + groupCode = request.groupId.toULong(), + appId = 3u, + folderId = request.folderId + ) + ).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val oidbPkg = oidb_sso.OIDBSSOPkg() + oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf() + if (rsp.deleteFolder?.retCode != 0) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete folder: ${rsp.deleteFolder?.retCode}")) + } + return deleteFolderResponse { } + } + + @Grpc("GroupFileService", "DeleteFile") + override suspend fun deleteFile(request: DeleteFileRequest): DeleteFileResponse { + val oidb0x6d6ReqBody = oidb_0x6d6.ReqBody().apply { + delete_file_req.set(oidb_0x6d6.DeleteFileReqBody().apply { + uint64_group_code.set(request.groupId) + uint32_app_id.set(3) + uint32_bus_id.set(request.busId) + str_parent_folder_id.set("/") + str_file_id.set(request.fileId) + }) + } + val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray()) + ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val oidbPkg = oidb_sso.OIDBSSOPkg() + oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidb_0x6d6.RspBody().apply { + mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray()) + } + if (rsp.delete_file_rsp.int32_ret_code.get() != 0) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete file: ${rsp.delete_file_rsp.int32_ret_code.get()}")) + } + return deleteFileResponse { } + } + + @Grpc("GroupFileService", "RenameFolder") + override suspend fun renameFolder(request: RenameFolderRequest): RenameFolderResponse { + val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody( + renameFolder = RenameFolderReq( + groupCode = request.groupId.toULong(), + appId = 3u, + folderId = request.folderId, + folderName = request.name + ) + ).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val oidbPkg = oidb_sso.OIDBSSOPkg() + oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf() + if (rsp.renameFolder?.retCode != 0) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to rename folder: ${rsp.renameFolder?.retCode}")) + } + return renameFolderResponse { } + } + + override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse { + return getGroupFileSystemInfo(request.groupId) + } + + @Grpc("GroupFileService", "GetRootFiles") + override suspend fun getRootFiles(request: GetRootFilesRequest): GetRootFilesResponse { + return getRootFilesResponse { + val response = GroupFileHelper.getGroupFiles(request.groupId) + this.files.addAll(response.filesList) + this.folders.addAll(response.foldersList) + } + } + + @Grpc("GroupFileService", "GetFiles") + override suspend fun getFiles(request: GetFilesRequest): GetFilesResponse { + return GroupFileHelper.getGroupFiles(request.groupId, request.folderId) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/GroupService.kt b/xposed/src/main/java/kritor/service/GroupService.kt new file mode 100644 index 0000000..8e0d5b8 --- /dev/null +++ b/xposed/src/main/java/kritor/service/GroupService.kt @@ -0,0 +1,406 @@ +package kritor.service + +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.group.BanMemberRequest +import io.kritor.group.BanMemberResponse +import io.kritor.group.GetGroupHonorRequest +import io.kritor.group.GetGroupHonorResponse +import io.kritor.group.GetGroupInfoRequest +import io.kritor.group.GetGroupInfoResponse +import io.kritor.group.GetGroupListRequest +import io.kritor.group.GetGroupListResponse +import io.kritor.group.GetGroupMemberInfoRequest +import io.kritor.group.GetGroupMemberInfoResponse +import io.kritor.group.GetGroupMemberListRequest +import io.kritor.group.GetGroupMemberListResponse +import io.kritor.group.GetNotJoinedGroupInfoRequest +import io.kritor.group.GetNotJoinedGroupInfoResponse +import io.kritor.group.GetProhibitedUserListRequest +import io.kritor.group.GetProhibitedUserListResponse +import io.kritor.group.GetRemainCountAtAllRequest +import io.kritor.group.GetRemainCountAtAllResponse +import io.kritor.group.GroupServiceGrpcKt +import io.kritor.group.KickMemberRequest +import io.kritor.group.KickMemberResponse +import io.kritor.group.LeaveGroupRequest +import io.kritor.group.LeaveGroupResponse +import io.kritor.group.ModifyGroupNameRequest +import io.kritor.group.ModifyGroupNameResponse +import io.kritor.group.ModifyGroupRemarkRequest +import io.kritor.group.ModifyGroupRemarkResponse +import io.kritor.group.ModifyMemberCardRequest +import io.kritor.group.ModifyMemberCardResponse +import io.kritor.group.PokeMemberRequest +import io.kritor.group.PokeMemberResponse +import io.kritor.group.SetGroupAdminRequest +import io.kritor.group.SetGroupAdminResponse +import io.kritor.group.SetGroupUniqueTitleRequest +import io.kritor.group.SetGroupUniqueTitleResponse +import io.kritor.group.SetGroupWholeBanRequest +import io.kritor.group.SetGroupWholeBanResponse +import io.kritor.group.banMemberResponse +import io.kritor.group.getGroupHonorResponse +import io.kritor.group.getGroupInfoResponse +import io.kritor.group.getGroupListResponse +import io.kritor.group.getGroupMemberInfoResponse +import io.kritor.group.getGroupMemberListResponse +import io.kritor.group.getNotJoinedGroupInfoResponse +import io.kritor.group.getProhibitedUserListResponse +import io.kritor.group.getRemainCountAtAllResponse +import io.kritor.group.groupHonorInfo +import io.kritor.group.groupMemberInfo +import io.kritor.group.kickMemberResponse +import io.kritor.group.leaveGroupResponse +import io.kritor.group.modifyGroupNameResponse +import io.kritor.group.modifyGroupRemarkResponse +import io.kritor.group.modifyMemberCardResponse +import io.kritor.group.notJoinedGroupInfo +import io.kritor.group.pokeMemberResponse +import io.kritor.group.prohibitedUserInfo +import io.kritor.group.setGroupAdminResponse +import io.kritor.group.setGroupUniqueTitleResponse +import io.kritor.group.setGroupWholeBanResponse +import moe.fuqiuluo.shamrock.helper.TroopHonorHelper +import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import qq.service.contact.ContactHelper +import qq.service.group.GroupHelper + +internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase() { + @Grpc("GroupService", "BanMember") + override suspend fun banMember(request: BanMemberRequest): BanMemberResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.banMember(request.groupId, when(request.targetCase!!) { + BanMemberRequest.TargetCase.TARGET_UIN -> request.targetUin + BanMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }, request.duration) + + return banMemberResponse { + groupId = request.groupId + } + } + + @Grpc("GroupService", "PokeMember") + override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse { + GroupHelper.pokeMember(request.groupId, when(request.targetCase!!) { + PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin + PokeMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }) + return pokeMemberResponse { } + } + + @Grpc("GroupService", "KickMember") + override suspend fun kickMember(request: KickMemberRequest): KickMemberResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + GroupHelper.kickMember(request.groupId, request.rejectAddRequest, if (request.hasKickReason()) request.kickReason else "", when(request.targetCase!!) { + KickMemberRequest.TargetCase.TARGET_UIN -> request.targetUin + KickMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }) + return kickMemberResponse { } + } + + @Grpc("GroupService", "LeaveGroup") + override suspend fun leaveGroup(request: LeaveGroupRequest): LeaveGroupResponse { + GroupHelper.resignTroop(request.groupId.toString()) + return leaveGroupResponse { } + } + + @Grpc("GroupService", "ModifyMemberCard") + override suspend fun modifyMemberCard(request: ModifyMemberCardRequest): ModifyMemberCardResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + GroupHelper.modifyGroupMemberCard(request.groupId, when(request.targetCase!!) { + ModifyMemberCardRequest.TargetCase.TARGET_UIN -> request.targetUin + ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }, request.card) + return modifyMemberCardResponse { } + } + + @Grpc("GroupService", "ModifyGroupName") + override suspend fun modifyGroupName(request: ModifyGroupNameRequest): ModifyGroupNameResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.modifyTroopName(request.groupId.toString(), request.groupName) + + return modifyGroupNameResponse { } + } + + @Grpc("GroupService", "ModifyGroupRemark") + override suspend fun modifyGroupRemark(request: ModifyGroupRemarkRequest): ModifyGroupRemarkResponse { + GroupHelper.modifyGroupRemark(request.groupId, request.remark) + + return modifyGroupRemarkResponse { } + } + + @Grpc("GroupService", "SetGroupAdmin") + override suspend fun setGroupAdmin(request: SetGroupAdminRequest): SetGroupAdminResponse { + if (!GroupHelper.isOwner(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.setGroupAdmin(request.groupId, when(request.targetCase!!) { + SetGroupAdminRequest.TargetCase.TARGET_UIN -> request.targetUin + SetGroupAdminRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }, request.isAdmin) + + return setGroupAdminResponse { } + } + + @Grpc("GroupService", "SetGroupUniqueTitle") + override suspend fun setGroupUniqueTitle(request: SetGroupUniqueTitleRequest): SetGroupUniqueTitleResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.setGroupUniqueTitle(request.groupId, when(request.targetCase!!) { + SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin + SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }, request.uniqueTitle) + + return setGroupUniqueTitleResponse { } + } + + @Grpc("GroupService", "SetGroupWholeBan") + override suspend fun setGroupWholeBan(request: SetGroupWholeBanRequest): SetGroupWholeBanResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.setGroupWholeBan(request.groupId, request.isBan) + return setGroupWholeBanResponse { } + } + + @Grpc("GroupService", "GetGroupInfo") + override suspend fun getGroupInfo(request: GetGroupInfoRequest): GetGroupInfoResponse { + val groupInfo = GroupHelper.getGroupInfo(request.groupId.toString(), true).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group info").withCause(it)) + }.getOrThrow() + return getGroupInfoResponse { + this.groupInfo = io.kritor.group.groupInfo { + groupId = groupInfo.troopcode.toLong() + groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: "" + groupRemark = groupInfo.troopRemark ?: "" + owner = groupInfo.troopowneruin?.toLong() ?: 0 + admins.addAll(GroupHelper.getAdminList(groupId)) + maxMemberCount = groupInfo.wMemberMax + memberCount = groupInfo.wMemberNum + groupUin = groupInfo.troopuin?.toLong() ?: 0 + } + } + } + + @Grpc("GroupService", "GetGroupList") + override suspend fun getGroupList(request: GetGroupListRequest): GetGroupListResponse { + val groupList = GroupHelper.getGroupList(if (request.hasRefresh()) request.refresh else false).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group list").withCause(it)) + }.getOrThrow() + return getGroupListResponse { + groupList.forEach { groupInfo -> + this.groupInfo.add(io.kritor.group.groupInfo { + groupId = groupInfo.troopcode.toLong() + groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: "" + groupRemark = groupInfo.troopRemark ?: "" + owner = groupInfo.troopowneruin?.toLong() ?: 0 + admins.addAll(GroupHelper.getAdminList(groupId)) + maxMemberCount = groupInfo.wMemberMax + memberCount = groupInfo.wMemberNum + groupUin = groupInfo.troopuin?.toLong() ?: 0 + }) + } + } + } + + @Grpc("GroupService", "GetGroupMemberInfo") + override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse { + val memberInfo = GroupHelper.getTroopMemberInfoByUin(request.groupId, when(request.targetCase!!) { + GetGroupMemberInfoRequest.TargetCase.UIN -> request.uin + GetGroupMemberInfoRequest.TargetCase.UID -> ContactHelper.getUinByUidAsync(request.uid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member info").withCause(it)) + }.getOrThrow() + return getGroupMemberInfoResponse { + groupMemberInfo = groupMemberInfo { + uid = if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.UID) request.uid else ContactHelper.getUidByUinAsync(request.uin) + uin = memberInfo.memberuin?.toLong() ?: 0 + nick = memberInfo.troopnick + .ifNullOrEmpty { memberInfo.hwName } + .ifNullOrEmpty { memberInfo.troopColorNick } + .ifNullOrEmpty { memberInfo.friendnick } ?: "" + age = memberInfo.age.toInt() + uniqueTitle = memberInfo.mUniqueTitle ?: "" + uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire + card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: "" + joinTime = memberInfo.join_time + lastActiveTime = memberInfo.last_active_time + level = memberInfo.level + shutUpTimestamp = memberInfo.gagTimeStamp + + distance = memberInfo.distance + honor.addAll((memberInfo.honorList ?: "") + .split("|") + .filter { it.isNotBlank() } + .map { it.toInt() }) + unfriendly = false + cardChangeable = GroupHelper.isAdmin(request.groupId.toString()) + } + } + } + + @Grpc("GroupService", "GetGroupMemberList") + override suspend fun getGroupMemberList(request: GetGroupMemberListRequest): GetGroupMemberListResponse { + val memberList = GroupHelper.getGroupMemberList(request.groupId.toString(), if (request.hasRefresh()) request.refresh else false).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it)) + }.getOrThrow() + return getGroupMemberListResponse { + memberList.forEach { memberInfo -> + this.groupMemberInfo.add(groupMemberInfo { + uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0) + uin = memberInfo.memberuin?.toLong() ?: 0 + nick = memberInfo.troopnick + .ifNullOrEmpty { memberInfo.hwName } + .ifNullOrEmpty { memberInfo.troopColorNick } + .ifNullOrEmpty { memberInfo.friendnick } ?: "" + age = memberInfo.age.toInt() + uniqueTitle = memberInfo.mUniqueTitle ?: "" + uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire + card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: "" + joinTime = memberInfo.join_time + lastActiveTime = memberInfo.last_active_time + level = memberInfo.level + shutUpTimestamp = memberInfo.gagTimeStamp + + distance = memberInfo.distance + honor.addAll((memberInfo.honorList ?: "") + .split("|") + .filter { it.isNotBlank() } + .map { it.toInt() }) + unfriendly = false + cardChangeable = GroupHelper.isAdmin(request.groupId.toString()) + }) + } + } + } + + @Grpc("GroupService", "GetProhibitedUserList") + override suspend fun getProhibitedUserList(request: GetProhibitedUserListRequest): GetProhibitedUserListResponse { + val prohibitedList = GroupHelper.getProhibitedMemberList(request.groupId).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get prohibited user list").withCause(it)) + }.getOrThrow() + return getProhibitedUserListResponse { + prohibitedList.forEach { + this.prohibitedUserInfo.add(prohibitedUserInfo { + uid = ContactHelper.getUidByUinAsync(it.memberUin) + uin = it.memberUin + prohibitedTime = it.shutuptimestap + }) + } + } + } + + @Grpc("GroupService", "GetRemainCountAtAll") + override suspend fun getRemainCountAtAll(request: GetRemainCountAtAllRequest): GetRemainCountAtAllResponse { + val remainAtAllRsp = GroupHelper.getGroupRemainAtAllRemain(request.groupId).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get remain count").withCause(it)) + }.getOrThrow() + return getRemainCountAtAllResponse { + accessAtAll = remainAtAllRsp.bool_can_at_all.get() + remainCountForGroup = remainAtAllRsp.uint32_remain_at_all_count_for_group.get() + remainCountForSelf = remainAtAllRsp.uint32_remain_at_all_count_for_uin.get() + } + } + + @Grpc("GroupService", "GetNotJoinedGroupInfo") + override suspend fun getNotJoinedGroupInfo(request: GetNotJoinedGroupInfoRequest): GetNotJoinedGroupInfoResponse { + val groupInfo = GroupHelper.getNotJoinedGroupInfo(request.groupId).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get not joined group info").withCause(it)) + }.getOrThrow() + return getNotJoinedGroupInfoResponse { + this.groupInfo = notJoinedGroupInfo { + groupId = groupInfo.groupId + groupName = groupInfo.groupName + owner = groupInfo.owner + maxMemberCount = groupInfo.maxMember + memberCount = groupInfo.memberCount + groupDesc = groupInfo.groupDesc + createTime = groupInfo.createTime.toInt() + groupFlag = groupInfo.groupFlag + groupFlagExt = groupInfo.groupFlagExt + } + } + } + + @Grpc("GroupService", "GetGroupHonor") + override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse { + return getGroupHonorResponse { + GroupHelper.getGroupMemberList(request.groupId.toString(), true).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it)) + }.onSuccess { memberList -> + memberList.forEach { member -> + (member.honorList ?: "").split("|") + .filter { it.isNotBlank() } + .map { it.toInt() }.forEach { + val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag) + if (honor != null) { + groupHonorInfo.add(groupHonorInfo { + uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong()) + uin = member.memberuin.toLong() + nick = member.troopnick + .ifNullOrEmpty { member.hwName } + .ifNullOrEmpty { member.troopColorNick } + .ifNullOrEmpty { member.friendnick } ?: "" + honorName = honor.honorName + avatar = honor.honorIconUrl + id = honor.honorId + description = honor.honorUrl + }) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/KritorService.kt b/xposed/src/main/java/kritor/service/KritorService.kt index e53387b..45eb251 100644 --- a/xposed/src/main/java/kritor/service/KritorService.kt +++ b/xposed/src/main/java/kritor/service/KritorService.kt @@ -104,8 +104,11 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() { @Grpc("KritorService", "SwitchAccount") override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse { - val uin = if (request.hasAccountUin()) request.accountUin.toString() - else ContactHelper.getUinByUidAsync(request.accountUid) + val uin = when(request.accountCase!!) { + SwitchAccountRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid) + SwitchAccountRequest.AccountCase.ACCOUNT_UIN -> request.accountUin.toString() + SwitchAccountRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("account not found")) + } val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found")) runCatching { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Kotlinx.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Kotlinx.kt index cb40366..2f5c3ce 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Kotlinx.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Kotlinx.kt @@ -47,8 +47,8 @@ fun ByteArray.slice(off: Int, length: Int = size - off): ByteArray { .let { s -> if (uppercase) s.lowercase(Locale.getDefault()) else s } } ?: "null" -fun String?.ifNullOrEmpty(defaultValue: String?): String? { - return if (this.isNullOrEmpty()) defaultValue else this +fun String?.ifNullOrEmpty(defaultValue: () -> String?): String? { + return if (this.isNullOrEmpty()) defaultValue() else this } @JvmOverloads fun String.hex2ByteArray(replace: Boolean = false): ByteArray { diff --git a/xposed/src/main/java/qq/service/QQInterfaces.kt b/xposed/src/main/java/qq/service/QQInterfaces.kt index b37a104..2e6b047 100644 --- a/xposed/src/main/java/qq/service/QQInterfaces.kt +++ b/xposed/src/main/java/qq/service/QQInterfaces.kt @@ -1,15 +1,129 @@ package qq.service +import android.os.Bundle +import com.tencent.common.app.AppInterface +import com.tencent.mobileqq.pb.ByteStringMicro +import com.tencent.qphone.base.remote.FromServiceMsg +import com.tencent.qphone.base.remote.ToServiceMsg +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.shamrock.utils.PlatformUtils import mqq.app.MobileQQ +import protobuf.auto.toByteArray +import protobuf.oidb.TrpcOidb +import qq.service.internals.MSFHandler +import tencent.im.oidb.oidb_sso +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds abstract class QQInterfaces { - companion object { - val app = if (PlatformUtils.isMqqPackage()) + val app = (if (PlatformUtils.isMqqPackage()) MobileQQ.getMobileQQ().waitAppRuntime() else - MobileQQ.getMobileQQ().waitAppRuntime(null) + MobileQQ.getMobileQQ().waitAppRuntime(null)) as AppInterface + + fun sendToServiceMsg(to: ToServiceMsg) { + app.sendToService(to) + } + + suspend fun sendToServiceMsgAW( + to: ToServiceMsg, + timeout: Duration = 30.seconds + ): FromServiceMsg? { + val seq = MSFHandler.nextSeq() + to.addAttribute("shamrock_uid", seq) + val resp: Pair? = withTimeoutOrNull(timeout) { + suspendCancellableCoroutine { continuation -> + GlobalScope.launch { + MSFHandler.registerResp(seq, continuation) + sendToServiceMsg(to) + } + } + } + if (resp == null) { + MSFHandler.unregisterResp(seq) + } + return resp?.second + } + + fun sendExtra(cmd: String, builder: (Bundle) -> Unit) { + val toServiceMsg = createToServiceMsg(cmd) + builder(toServiceMsg.extraData) + app.sendToService(toServiceMsg) + } + + fun createToServiceMsg(cmd: String): ToServiceMsg { + return ToServiceMsg("mobileqq.service", app.currentAccountUin, cmd) + } + + fun sendOidb(cmd: String, command: Int, service: Int, data: ByteArray, trpc: Boolean = false) { + val to = createToServiceMsg(cmd) + if (trpc) { + val oidb = TrpcOidb( + cmd = command, + service = service, + buffer = data, + flag = 1 + ) + to.putWupBuffer(oidb.toByteArray()) + } else { + val oidb = oidb_sso.OIDBSSOPkg() + oidb.uint32_command.set(command) + oidb.uint32_service_type.set(service) + oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(data)) + oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext())) + to.putWupBuffer(oidb.toByteArray()) + } + to.addAttribute("req_pb_protocol_flag", true) + app.sendToService(to) + } + + @DelicateCoroutinesApi + suspend fun sendBufferAW( + cmd: String, + isProto: Boolean, + data: ByteArray, + timeout: Duration = 30.seconds + ): FromServiceMsg? { + val toServiceMsg = createToServiceMsg(cmd) + toServiceMsg.putWupBuffer(data) + toServiceMsg.addAttribute("req_pb_protocol_flag", isProto) + return sendToServiceMsgAW(toServiceMsg, timeout) + } + + @DelicateCoroutinesApi + suspend fun sendOidbAW( + cmd: String, + command: Int, + service: Int, + data: ByteArray, + trpc: Boolean = false, + timeout: Duration = 30.seconds + ): FromServiceMsg? { + val to = createToServiceMsg(cmd) + if (trpc) { + val oidb = TrpcOidb( + cmd = command, + service = service, + buffer = data, + flag = 1 + ) + to.putWupBuffer(oidb.toByteArray()) + } else { + val oidb = oidb_sso.OIDBSSOPkg() + oidb.uint32_command.set(command) + oidb.uint32_service_type.set(service) + oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(data)) + oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext())) + to.putWupBuffer(oidb.toByteArray()) + } + to.addAttribute("req_pb_protocol_flag", true) + return sendToServiceMsgAW(to, timeout) + } } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/contact/ContactHelper.kt b/xposed/src/main/java/qq/service/contact/ContactHelper.kt index 3ddc0a8..30240eb 100644 --- a/xposed/src/main/java/qq/service/contact/ContactHelper.kt +++ b/xposed/src/main/java/qq/service/contact/ContactHelper.kt @@ -3,6 +3,8 @@ package qq.service.contact import com.tencent.common.app.AppInterface import com.tencent.mobileqq.data.Card import com.tencent.mobileqq.profilecard.api.IProfileDataService +import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_SELF_UIN +import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_TARGET_UIN import com.tencent.mobileqq.profilecard.api.IProfileProtocolService import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver import kotlinx.coroutines.suspendCancellableCoroutine @@ -12,9 +14,100 @@ import moe.fuqiuluo.shamrock.internals.NTServiceFetcher import qq.service.QQInterfaces import kotlin.coroutines.resume -object ContactHelper: QQInterfaces() { +internal object ContactHelper: QQInterfaces() { + const val FROM_C2C_AIO = 2 + const val FROM_CONDITION_SEARCH = 9 + const val FROM_CONTACTS_TAB = 5 + const val FROM_FACE_2_FACE_ADD_FRIEND = 11 + const val FROM_MAYKNOW_FRIEND = 3 + const val FROM_QCIRCLE = 4 + const val FROM_QQ_TROOP = 1 + const val FROM_QZONE = 7 + const val FROM_SCAN = 6 + const val FROM_SEARCH = 8 + const val FROM_SETTING_ME = 12 + const val FROM_SHARE_CARD = 10 + + const val PROFILE_CARD_IS_BLACK = 2 + const val PROFILE_CARD_IS_BLACKED = 1 + const val PROFILE_CARD_NOT_BLACK = 3 + + const val SUB_FROM_C2C_AIO = 21 + const val SUB_FROM_C2C_INTERACTIVE_LOGO = 25 + const val SUB_FROM_C2C_LEFT_SLIDE = 23 + const val SUB_FROM_C2C_OTHER = 24 + const val SUB_FROM_C2C_SETTING = 22 + const val SUB_FROM_C2C_TOFU = 26 + const val SUB_FROM_CONDITION_SEARCH_OTHER = 99 + const val SUB_FROM_CONDITION_SEARCH_RESULT = 91 + const val SUB_FROM_CONTACTS_FRIEND_TAB = 51 + const val SUB_FROM_CONTACTS_TAB = 55 + const val SUB_FROM_FACE_2_FACE_ADD_FRIEND_RESULT_AVATAR = 111 + const val SUB_FROM_FACE_2_FACE_OTHER = 119 + const val SUB_FROM_FRIEND_APPLY = 56 + const val SUB_FROM_FRIEND_NOTIFY_MORE = 57 + const val SUB_FROM_FRIEND_NOTIFY_TAB = 54 + const val SUB_FROM_GROUPING_TAB = 52 + const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB = 31 + const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB_MORE = 37 + const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE = 34 + const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_MORE = 39 + const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_SEARCH = 36 + const val SUB_FROM_MAYKNOW_FRIEND_NEW_FRIEND_PAGE = 32 + const val SUB_FROM_MAYKNOW_FRIEND_OTHER = 35 + const val SUB_FROM_MAYKNOW_FRIEND_SEARCH = 33 + const val SUB_FROM_MAYKNOW_FRIEND_SEARCH_MORE = 38 + const val SUB_FROM_PHONE_LIST_TAB = 53 + const val SUB_FROM_QCIRCLE_OTHER = 42 + const val SUB_FROM_QCIRCLE_PROFILE = 41 + const val SUB_FROM_QQ_TROOP_ACTIVE_MEMBER = 15 + const val SUB_FROM_QQ_TROOP_ADMIN = 16 + const val SUB_FROM_QQ_TROOP_AIO = 11 + const val SUB_FROM_QQ_TROOP_MEMBER = 12 + const val SUB_FROM_QQ_TROOP_OTHER = 14 + const val SUB_FROM_QQ_TROOP_SETTING_MEMBER_LIST = 17 + const val SUB_FROM_QQ_TROOP_TEMP_SESSION = 13 + const val SUB_FROM_QRCODE_SCAN_DRAWER = 64 + const val SUB_FROM_QRCODE_SCAN_NEW = 61 + const val SUB_FROM_QRCODE_SCAN_OLD = 62 + const val SUB_FROM_QRCODE_SCAN_OTHER = 69 + const val SUB_FROM_QRCODE_SCAN_PROFILE = 63 + const val SUB_FROM_QZONE_HOME = 71 + const val SUB_FROM_QZONE_OTHER = 79 + const val SUB_FROM_SEARCH_CONTACT_TAB_MORE_FIND_PROFILE = 83 + const val SUB_FROM_SEARCH_FIND_PROFILE_TAB = 82 + const val SUB_FROM_SEARCH_MESSAGE_TAB_MORE_FIND_PROFILE = 84 + const val SUB_FROM_SEARCH_NEW_FRIEND_MORE_FIND_PROFILE = 85 + const val SUB_FROM_SEARCH_OTHER = 89 + const val SUB_FROM_SEARCH_TAB = 81 + const val SUB_FROM_SETTING_ME_AVATAR = 121 + const val SUB_FROM_SETTING_ME_OTHER = 129 + const val SUB_FROM_SHARE_CARD_C2C = 101 + const val SUB_FROM_SHARE_CARD_OTHER = 109 + const val SUB_FROM_SHARE_CARD_TROOP = 102 + const val SUB_FROM_TYPE_DEFAULT = 0 + private val refreshCardLock by lazy { Mutex() } + suspend fun voteUser(target: Long, count: Int): Result { + if(count !in 1 .. 20) { + return Result.failure(IllegalArgumentException("vote count must be in 1 .. 20")) + } + val card = getProfileCard(target).onFailure { + return Result.failure(RuntimeException("unable to fetch contact info")) + }.getOrThrow() + sendExtra("VisitorSvc.ReqFavorite") { + it.putLong(PARAM_SELF_UIN, app.longAccountUin) + it.putLong(PARAM_TARGET_UIN, target) + it.putByteArray("vCookies", card.vCookies) + it.putBoolean("nearby_people", true) + it.putInt("favoriteSource", FROM_CONTACTS_TAB) + it.putInt("iCount", count) + it.putInt("from", FROM_CONTACTS_TAB) + } + return Result.success(Unit) + } + suspend fun getProfileCard(uin: Long): Result { return getProfileCardFromCache(uin).onFailure { return refreshAndGetProfileCard(uin) diff --git a/xposed/src/main/java/qq/service/file/GroupFileHelper.kt b/xposed/src/main/java/qq/service/file/GroupFileHelper.kt new file mode 100644 index 0000000..bf1131b --- /dev/null +++ b/xposed/src/main/java/qq/service/file/GroupFileHelper.kt @@ -0,0 +1,162 @@ +@file:OptIn(ExperimentalStdlibApi::class) + +package qq.service.file + +import com.tencent.mobileqq.pb.ByteStringMicro +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.file.File +import io.kritor.file.Folder +import io.kritor.file.GetFileSystemInfoResponse +import io.kritor.file.GetFilesRequest +import io.kritor.file.GetFilesResponse +import io.kritor.file.folder +import io.kritor.file.getFileSystemInfoResponse +import io.kritor.file.getFilesRequest +import io.kritor.file.getFilesResponse +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.shamrock.utils.DeflateTools +import qq.service.QQInterfaces +import tencent.im.oidb.cmd0x6d8.oidb_0x6d8 +import tencent.im.oidb.oidb_sso +import kotlin.time.Duration.Companion.seconds + +internal object GroupFileHelper: QQInterfaces() { + suspend fun getGroupFileSystemInfo(groupId: Long): GetFileSystemInfoResponse { + val fromServiceMsg = sendOidbAW("OidbSvc.0x6d8_1", 1752, 2, oidb_0x6d8.ReqBody().also { + it.group_file_cnt_req.set(oidb_0x6d8.GetFileCountReqBody().also { + it.uint64_group_code.set(groupId) + it.uint32_app_id.set(3) + it.uint32_bus_id.set(0) + }) + }.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val fileCnt: Int + val limitCnt: Int + if (fromServiceMsg.wupBuffer != null) { + oidb_0x6d8.RspBody().mergeFrom( + oidb_sso.OIDBSSOPkg() + .mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + .bytes_bodybuffer.get() + .toByteArray() + ).group_file_cnt_rsp.apply { + fileCnt = uint32_all_file_count.get() + limitCnt = uint32_limit_count.get() + } + } else { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response")) + } + + val fromServiceMsg2 = sendOidbAW("OidbSvc.0x6d8_1", 1752, 3, oidb_0x6d8.ReqBody().also { + it.group_space_req.set(oidb_0x6d8.GetSpaceReqBody().apply { + uint64_group_code.set(groupId) + uint32_app_id.set(3) + }) + }.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + val totalSpace: Long + val usedSpace: Long + if (fromServiceMsg2.isSuccess && fromServiceMsg2.wupBuffer != null) { + oidb_0x6d8.RspBody().mergeFrom( + oidb_sso.OIDBSSOPkg() + .mergeFrom(fromServiceMsg2.wupBuffer.slice(4)) + .bytes_bodybuffer.get() + .toByteArray()).group_space_rsp.apply { + totalSpace = uint64_total_space.get() + usedSpace = uint64_used_space.get() + } + } else { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response x2")) + } + + return getFileSystemInfoResponse { + this.fileCount = fileCnt + this.totalCount = limitCnt + this.totalSpace = totalSpace.toInt() + this.usedSpace = usedSpace.toInt() + } + } + + suspend fun getGroupFiles(groupId: Long, folderId: String = "/"): GetFilesResponse { + val fileSystemInfo = getGroupFileSystemInfo(groupId) + val fromServiceMsg = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also { + it.file_list_info_req.set(oidb_0x6d8.GetFileListReqBody().apply { + uint64_group_code.set(groupId) + uint32_app_id.set(3) + str_folder_id.set(folderId) + + uint32_file_count.set(fileSystemInfo.fileCount) + uint32_all_file_count.set(0) + uint32_req_from.set(3) + uint32_sort_by.set(oidb_0x6d8.GetFileListReqBody.SORT_BY_TIMESTAMP) + + uint32_filter_code.set(0) + uint64_uin.set(0) + + uint32_start_index.set(0) + + bytes_context.set(ByteStringMicro.copyFrom(EMPTY_BYTE_ARRAY)) + + uint32_show_onlinedoc_folder.set(0) + }) + }.toByteArray(), timeout = 15.seconds) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val files = arrayListOf() + val dirs = arrayListOf() + if (fromServiceMsg.wupBuffer != null) { + val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(fromServiceMsg.wupBuffer.slice(4).let { + if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it + }) + + oidb_0x6d8.RspBody().mergeFrom(oidb.bytes_bodybuffer.get().toByteArray()) + .file_list_info_rsp.apply { + rpt_item_list.get().forEach { file -> + if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) { + val fileInfo = file.file_info + files.add(io.kritor.file.file { + this.fileId = fileInfo.str_file_id.get() + this.fileName = fileInfo.str_file_name.get() + this.fileSize = fileInfo.uint64_file_size.get() + this.busId = fileInfo.uint32_bus_id.get() + this.uploadTime = fileInfo.uint32_upload_time.get() + this.deadTime = fileInfo.uint32_dead_time.get() + this.modifyTime = fileInfo.uint32_modify_time.get() + this.downloadTimes = fileInfo.uint32_download_times.get() + this.uploader = fileInfo.uint64_uploader_uin.get() + this.uploaderName = fileInfo.str_uploader_name.get() + this.sha = fileInfo.bytes_sha.get().toByteArray().toHexString() + this.sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString() + this.md5 = fileInfo.bytes_md5.get().toByteArray().toHexString() + }) + } + else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) { + val folderInfo = file.folder_info + dirs.add(folder { + this.folderId = folderInfo.str_folder_id.get() + this.folderName = folderInfo.str_folder_name.get() + this.totalFileCount = folderInfo.uint32_total_file_count.get() + this.createTime = folderInfo.uint32_create_time.get() + this.creator = folderInfo.uint64_create_uin.get() + this.creatorName = folderInfo.str_creator_name.get() + }) + } else { + LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN) + } + } + } + } else { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response")) + } + + return getFilesResponse { + this.files.addAll(files) + this.folders.addAll(folders) + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/friend/FriendHelper.kt b/xposed/src/main/java/qq/service/friend/FriendHelper.kt new file mode 100644 index 0000000..d93921e --- /dev/null +++ b/xposed/src/main/java/qq/service/friend/FriendHelper.kt @@ -0,0 +1,40 @@ +package qq.service.friend + +import com.tencent.mobileqq.data.Friends +import com.tencent.mobileqq.friend.api.IFriendDataService +import com.tencent.mobileqq.friend.api.IFriendHandlerService +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import qq.service.QQInterfaces +import kotlin.coroutines.resume + +internal object FriendHelper: QQInterfaces() { + suspend fun getFriendList(refresh: Boolean): Result> { + val service = app.getRuntimeService(IFriendDataService::class.java, "all") + if(refresh || !service.isInitFinished) { + if(!requestFriendList(service)) { + return Result.failure(Exception("获取好友列表失败")) + } + } + return Result.success(service.allFriends) + } + + private suspend fun requestFriendList(dataService: IFriendDataService): Boolean { + val service = app.getRuntimeService(IFriendHandlerService::class.java, "all") + service.requestFriendList(true, 0) + return suspendCancellableCoroutine { continuation -> + val waiter = GlobalScope.launch { + while (!dataService.isInitFinished) { + delay(200) + } + continuation.resume(true) + } + continuation.invokeOnCancellation { + waiter.cancel() + continuation.resume(false) + } + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/group/GroupHelper.kt b/xposed/src/main/java/qq/service/group/GroupHelper.kt new file mode 100644 index 0000000..ca57424 --- /dev/null +++ b/xposed/src/main/java/qq/service/group/GroupHelper.kt @@ -0,0 +1,617 @@ +package qq.service.group + +import KQQ.RespBatchProcess +import com.qq.jce.wup.UniPacket +import com.tencent.mobileqq.app.BusinessHandlerFactory +import com.tencent.mobileqq.data.troop.TroopInfo +import com.tencent.mobileqq.data.troop.TroopMemberInfo +import com.tencent.mobileqq.pb.ByteStringMicro +import com.tencent.mobileqq.troop.api.ITroopInfoService +import com.tencent.mobileqq.troop.api.ITroopMemberInfoService +import com.tencent.qqnt.kernel.nativeinterface.MemberInfo +import friendlist.stUinInfo +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.internals.NTServiceFetcher +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import moe.fuqiuluo.shamrock.tools.putBuf32Long +import moe.fuqiuluo.shamrock.tools.slice +import protobuf.auto.toByteArray +import protobuf.oidb.cmd0xf16.Oidb0xf16 +import protobuf.oidb.cmd0xf16.SetGroupRemarkReq +import qq.service.QQInterfaces +import tencent.im.group.group_member_info +import tencent.im.oidb.cmd0x88d.oidb_0x88d +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.cmd0xed3.oidb_cmd0xed3 +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 +import kotlin.coroutines.resume + +internal object GroupHelper: QQInterfaces() { + private val RefreshTroopMemberInfoLock by lazy { Mutex() } + private val RefreshTroopMemberListLock by lazy { Mutex() } + + private lateinit var METHOD_REQ_MEMBER_INFO: Method + private lateinit var METHOD_REQ_MEMBER_INFO_V2: Method + private lateinit var METHOD_REQ_TROOP_LIST: Method + private lateinit var METHOD_REQ_TROOP_MEM_LIST: Method + private lateinit var METHOD_REQ_MODIFY_GROUP_NAME: Method + + fun getGroupInfo(groupId: String): TroopInfo { + val service = app + .getRuntimeService(ITroopInfoService::class.java, "all") + + return service.getTroopInfo(groupId) + } + + fun isAdmin(groupId: String): Boolean { + val groupInfo = getGroupInfo(groupId) + + return groupInfo.isAdmin || groupInfo.troopowneruin == app.account + } + + fun isOwner(groupId: String): Boolean { + val groupInfo = getGroupInfo(groupId) + return groupInfo.troopowneruin == app.account + } + + fun getAdminList( + groupId: Long, + withOwner: Boolean = false + ): List { + val groupInfo = getGroupInfo(groupId.toString()) + return (groupInfo.Administrator ?: "") + .split("|", ",") + .also { + if (withOwner && it is ArrayList) { + it.add(0, groupInfo.troopowneruin) + } + }.mapNotNull { it.ifNullOrEmpty { null }?.toLong() } + } + + suspend fun getGroupList(refresh: Boolean): Result> { + val service = app.getRuntimeService(ITroopInfoService::class.java, "all") + + var troopList = service.allTroopList + if(refresh || !service.isTroopCacheInited || troopList == null) { + if(!requestGroupInfo(service)) { + return Result.failure(Exception("获取群列表失败")) + } else { + troopList = service.allTroopList + } + } + return Result.success(troopList) + } + + private suspend fun requestGroupInfo( + service: ITroopInfoService + ): Boolean { + refreshTroopList() + + return suspendCancellableCoroutine { continuation -> + val waiter = GlobalScope.launch { + do { + delay(1000) + } while ( + !service.isTroopCacheInited + ) + continuation.resume(true) + } + continuation.invokeOnCancellation { + waiter.cancel() + continuation.resume(false) + } + } + } + + fun banMember(groupId: Long, memberUin: Long, time: Int) { + val buffer = ByteBuffer.allocate(1 * 8 + 7) + buffer.putBuf32Long(groupId) + buffer.put(32.toByte()) + buffer.putShort(1) + buffer.putBuf32Long(memberUin) + buffer.putInt(time) + val array = buffer.array() + sendOidb("OidbSvc.0x570_8", 1392, 8, array) + } + + fun pokeMember(groupId: Long, memberUin: Long) { + val req = oidb_cmd0xed3.ReqBody().apply { + uint64_group_code.set(groupId) + uint64_to_uin.set(memberUin) + uint32_msg_seq.set(0) + } + sendOidb("OidbSvc.0xed3", 3795, 1, req.toByteArray()) + } + + fun kickMember(groupId: Long, rejectAddRequest: Boolean, kickMsg: String, vararg memberUin: Long) { + val reqBody = oidb_0x8a0.ReqBody() + reqBody.opt_uint64_group_code.set(groupId) + memberUin.forEach { + val memberInfo = oidb_0x8a0.KickMemberInfo() + memberInfo.opt_uint32_operate.set(5) + memberInfo.opt_uint64_member_uin.set(it) + memberInfo.opt_uint32_flag.set(if (rejectAddRequest) 1 else 0) + reqBody.rpt_msg_kick_list.add(memberInfo) + } + if (kickMsg.isNotEmpty()) { + reqBody.bytes_kick_msg.set(ByteStringMicro.copyFrom(kickMsg.toByteArray())) + } + sendOidb("OidbSvc.0x8a0_0", 2208, 0, reqBody.toByteArray()) + } + + fun resignTroop(groupId: String) { + sendExtra("ProfileService.GroupMngReq") { + it.putInt("groupreqtype", 2) + it.putString("troop_uin", groupId) + it.putString("uin", app.currentUin) + } + } + + fun modifyGroupMemberCard(groupId: Long, userId: Long, name: String): Boolean { + val createToServiceMsg = createToServiceMsg("friendlist.ModifyGroupCardReq") + createToServiceMsg.extraData.putLong("dwZero", 0L) + createToServiceMsg.extraData.putLong("dwGroupCode", groupId) + val info = stUinInfo() + info.cGender = -1 + info.dwuin = userId + info.sEmail = "" + info.sName = name + info.sPhone = "" + info.sRemark = "" + info.dwFlag = 1 + createToServiceMsg.extraData.putSerializable("vecUinInfo", arrayListOf(info)) + createToServiceMsg.extraData.putLong("dwNewSeq", 0L) + sendToServiceMsg(createToServiceMsg) + return true + } + + fun modifyTroopName(groupId: String, name: String) { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MODIFY_HANDLER) + + if (!::METHOD_REQ_MODIFY_GROUP_NAME.isInitialized) { + METHOD_REQ_MODIFY_GROUP_NAME = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 3 + && it.parameterTypes[0] == String::class.java + && it.parameterTypes[1] == String::class.java + && it.parameterTypes[2] == Boolean::class.java + && !Modifier.isPrivate(it.modifiers) + } + } + + METHOD_REQ_MODIFY_GROUP_NAME.invoke(businessHandler, groupId, name, false) + } + + fun modifyGroupRemark(groupId: Long, remark: String): Boolean { + sendOidb("OidbSvc.0xf16_1", 3862, 1, Oidb0xf16( + setGroupRemarkReq = SetGroupRemarkReq( + groupCode = groupId.toULong(), + groupUin = groupCode2GroupUin(groupId).toULong(), + groupRemark = remark + ) + ).toByteArray()) + return true + } + + fun setGroupAdmin(groupId: Long, userId: Long, enable: Boolean) { + val buffer = ByteBuffer.allocate(9) + buffer.putBuf32Long(groupId) + buffer.putBuf32Long(userId) + buffer.put(if (enable) 1 else 0) + val array = buffer.array() + sendOidb("OidbSvc.0x55c_1", 1372, 1, array) + } + + suspend fun setGroupUniqueTitle(groupId: Long, userId: Long, title: String) { + val localMemberInfo = getTroopMemberInfoByUin(groupId, userId, true).getOrThrow() + val req = Oidb_0x8fc.ReqBody() + req.uint64_group_code.set(groupId) + val memberInfo = Oidb_0x8fc.MemberInfo() + memberInfo.uint64_uin.set(userId) + memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty { + localMemberInfo.troopremark.ifNullOrEmpty { "" } + })) + memberInfo.bytes_special_title.set(ByteStringMicro.copyFromUtf8(title)) + memberInfo.uint32_special_title_expire_time.set(-1) + req.rpt_mem_level_info.add(memberInfo) + sendOidb("OidbSvc.0x8fc_2", 2300, 2, req.toByteArray()) + } + + fun setGroupWholeBan(groupId: Long, enable: Boolean) { + val reqBody = oidb_0x89a.ReqBody() + reqBody.uint64_group_code.set(groupId) + reqBody.st_group_info.set(oidb_0x89a.groupinfo().apply { + uint32_shutup_time.set(if (enable) 268435455 else 0) + }) + sendOidb("OidbSvc.0x89a_0", 2202, 0, reqBody.toByteArray()) + } + + suspend fun getGroupMemberList(groupId: String, refresh: Boolean): Result> { + val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") + var memberList = service.getAllTroopMembers(groupId) + if (refresh || memberList == null) { + memberList = requestTroopMemberInfo(service, groupId).onFailure { + return Result.failure(Exception("获取群成员列表失败")) + }.getOrThrow() + } + + getGroupInfo(groupId, true).onSuccess { + if(it.wMemberNum > memberList.size) { + return getGroupMemberList(groupId, true) + } + } + + return Result.success(memberList) + } + + suspend fun getProhibitedMemberList(groupId: Long): Result> { + val fromServiceMsg = sendOidbAW("OidbSvc.0x899_0", 2201, 0, oidb_0x899.ReqBody().apply { + uint64_group_code.set(groupId) + uint64_start_uin.set(0) + uint32_identify_flag.set(6) + memberlist_opt.set(oidb_0x899.memberlist().apply { + uint64_member_uin.set(0) + uint32_shutup_timestap.set(0) + }) + }.toByteArray()) ?: return Result.failure(RuntimeException("[oidb] timeout")) + if (!fromServiceMsg.isSuccess) { + return Result.failure(RuntimeException("[oidb] failed")) + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + if(body.uint32_result.get() != 0) { + return Result.failure(RuntimeException(body.str_error_msg.get())) + } + val resp = oidb_0x899.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + return Result.success(resp.rpt_memberlist.get().map { + ProhibitedMemberInfo(it.uint64_member_uin.get(), it.uint32_shutup_timestap.get()) + }) + } + + suspend fun getGroupRemainAtAllRemain (groupId: Long): Result { + val fromServiceMsg = 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(app.longAccountUin) + uint64_group_code.set(groupId) + }.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout")) + if (!fromServiceMsg.isSuccess) { + return Result.failure(RuntimeException("[oidb] failed")) + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.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(resp) + } + + suspend fun getNotJoinedGroupInfo(groupId: Long): Result { + return withTimeoutOrNull(5000) timeout@{ + val toServiceMsg = createToServiceMsg("ProfileService.ReqBatchProcess") + toServiceMsg.extraData.putLong("troop_code", groupId) + toServiceMsg.extraData.putBoolean("is_admin", false) + toServiceMsg.extraData.putInt("from", 0) + val fromServiceMsg = sendToServiceMsgAW(toServiceMsg) ?: return@timeout Result.failure(Exception("获取群信息超时")) + if (!fromServiceMsg.isSuccess) { + return@timeout Result.failure(Exception("获取群信息失败")) + } + val uniPacket = UniPacket(true) + uniPacket.encodeName = "utf-8" + uniPacket.decode(fromServiceMsg.wupBuffer) + val respBatchProcess = uniPacket.getByClass("RespBatchProcess", RespBatchProcess()) + val batchRespInfo = oidb_0x88d.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg() + .mergeFrom(respBatchProcess.batch_response_list.first().buffer) + .bytes_bodybuffer.get().toByteArray()).stzrspgroupinfo.get().firstOrNull() + ?: return@timeout Result.failure(Exception("获取群信息失败")) + val info = batchRespInfo.stgroupinfo + Result.success(NotJoinedGroupInfo( + groupId = batchRespInfo.uint64_group_code.get(), + maxMember = info.uint32_group_member_max_num.get(), + memberCount = info.uint32_group_member_num.get(), + groupName = info.string_group_name.get().toStringUtf8(), + groupDesc = info.string_group_finger_memo.get().toStringUtf8(), + owner = info.uint64_group_owner.get(), + createTime = info.uint32_group_create_time.get().toLong(), + groupFlag = info.uint32_group_flag.get(), + groupFlagExt = info.uint32_group_flag_ext.get() + )) + } ?: Result.failure(Exception("获取群信息超时")) + } + + suspend fun getGroupInfo(groupId: String, refresh: Boolean): Result { + val service = app + .getRuntimeService(ITroopInfoService::class.java, "all") + + val groupInfo = getGroupInfo(groupId) + + return if(refresh || !service.isTroopCacheInited || groupInfo.troopuin.isNullOrBlank()) { + requestGroupInfo(service, groupId) + } else { + Result.success(groupInfo) + } + } + + private suspend fun requestGroupInfo(dataService: ITroopInfoService, uin: String): Result { + val info = withTimeoutOrNull(1000) { + var troopInfo: TroopInfo? + do { + troopInfo = dataService.getTroopInfo(uin) + delay(100) + } while (troopInfo == null || troopInfo.troopuin.isNullOrBlank()) + return@withTimeoutOrNull troopInfo + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群列表失败")) + } + } + + suspend fun getTroopMemberInfoByUin( + groupId: Long, + uin: Long, + refresh: Boolean = false + ): Result { + val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") + var info = service.getTroopMember(groupId.toString(), uin.toString()) + if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) { + info = requestTroopMemberInfo(service, groupId, uin).getOrNull() + } + if (info == null) { + info = getTroopMemberInfoByUinViaNt(groupId, uin).getOrNull()?.let { + TroopMemberInfo().apply { + troopnick = it.cardName + friendnick = it.nick + } + } + } + try { + if (info != null && (info.alias == null || info.alias.isBlank())) { + val req = group_member_info.ReqBody() + req.uint64_group_code.set(groupId) + req.uint64_uin.set(uin) + req.bool_new_client.set(true) + req.uint32_client_type.set(1) + req.uint32_rich_card_name_ver.set(1) + val fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray()) + if (fromServiceMsg != null && fromServiceMsg.isSuccess) { + val rsp = group_member_info.RspBody() + rsp.mergeFrom(fromServiceMsg.wupBuffer.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("getTroopMemberInfoByUin: " + err.stackTraceToString(), Level.WARN) + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群成员信息失败")) + } + } + + suspend fun getTroopMemberInfoByUinViaNt( + groupId: Long, + qq: Long, + timeout: Long = 5000L + ): Result { + return runCatching { + val kernelService = NTServiceFetcher.kernelService + val sessionService = kernelService.wrapperSession + val groupService = sessionService.groupService + val info = withTimeoutOrNull(timeout) { + suspendCancellableCoroutine { + groupService.getTransferableMemberInfo(groupId) { code, _, data -> + if (code != 0) { + it.resume(null) + return@getTransferableMemberInfo + } + data.forEach { (_, info) -> + if (info.uin == qq) { + it.resume(info) + return@forEach + } + } + it.resume(null) + } + } + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群成员信息失败")) + } + } + } + + private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result { + val info = RefreshTroopMemberInfoLock.withLock { + val groupIdStr = groupId.toString() + val memberUinStr = memberUin.toString() + + service.deleteTroopMember(groupIdStr, memberUinStr) + + requestMemberInfoV2(groupId, memberUin) + requestMemberInfo(groupId, memberUin) + + withTimeoutOrNull(timeout) { + while (!service.isMemberInCache(groupIdStr, memberUinStr)) { + delay(200) + } + return@withTimeoutOrNull service.getTroopMember(groupIdStr, memberUinStr) + } + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群成员信息失败")) + } + } + + private fun requestMemberInfo(groupId: Long, memberUin: Long) { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER) + + if (!::METHOD_REQ_MEMBER_INFO.isInitialized) { + METHOD_REQ_MEMBER_INFO = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 2 && + it.parameterTypes[0] == Long::class.java && + it.parameterTypes[1] == Long::class.java && + !Modifier.isPrivate(it.modifiers) + } + } + + METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId, memberUin) + } + + private fun requestMemberInfoV2(groupId: Long, memberUin: Long) { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER) + + if (!::METHOD_REQ_MEMBER_INFO_V2.isInitialized) { + METHOD_REQ_MEMBER_INFO_V2 = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 3 && + it.parameterTypes[0] == String::class.java && + it.parameterTypes[1] == String::class.java && + !Modifier.isPrivate(it.modifiers) + } + } + + METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler, groupId.toString(), groupUin2GroupCode(groupId).toString(), arrayListOf(memberUin.toString())) + } + + private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String): Result> { + val info = RefreshTroopMemberListLock.withLock { + service.deleteTroopMembers(groupId) + refreshTroopMemberList(groupId) + + withTimeoutOrNull(10000) { + var memberList: List? + do { + delay(100) + memberList = service.getAllTroopMembers(groupId) + } while (memberList.isNullOrEmpty()) + return@withTimeoutOrNull memberList + } + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群成员信息失败")) + } + } + + private fun refreshTroopMemberList(groupId: String) { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_LIST_HANDLER) + + // void C(boolean forceRefresh, String groupId, String troopcode, int reqType); // RequestedTroopList/refreshMemberListFromServer + if (!::METHOD_REQ_TROOP_MEM_LIST.isInitialized) { + METHOD_REQ_TROOP_MEM_LIST = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 4 + && it.parameterTypes[0] == Boolean::class.java + && it.parameterTypes[1] == String::class.java + && it.parameterTypes[2] == String::class.java + && it.parameterTypes[3] == Int::class.java + && !Modifier.isPrivate(it.modifiers) + } + } + + METHOD_REQ_TROOP_MEM_LIST.invoke(businessHandler, true, groupId, groupUin2GroupCode(groupId.toLong()).toString(), 5) + } + + private fun refreshTroopList() { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_LIST_HANDLER) + + if (!::METHOD_REQ_TROOP_LIST.isInitialized) { + METHOD_REQ_TROOP_LIST = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 0 && !Modifier.isPrivate(it.modifiers) && it.returnType == Void.TYPE + } + } + + METHOD_REQ_TROOP_LIST.invoke(businessHandler) + } + + fun groupUin2GroupCode(groupuin: Long): Long { + var calc = groupuin / 1000000L + while (true) { + calc -= if (calc >= 0 + 202 && calc + 202 <= 10) { + (202 - 0).toLong() + } else if (calc >= 11 + 480 && calc <= 19 + 480) { + (480 - 11).toLong() + } else if (calc >= 20 + 2100 && calc <= 66 + 2100) { + (2100 - 20).toLong() + } else if (calc >= 67 + 2010 && calc <= 156 + 2010) { + (2010 - 67).toLong() + } else if (calc >= 157 + 2147 && calc <= 209 + 2147) { + (2147 - 157).toLong() + } else if (calc >= 210 + 4100 && calc <= 309 + 4100) { + (4100 - 210).toLong() + } else if (calc >= 310 + 3800 && calc <= 499 + 3800) { + (3800 - 310).toLong() + } else { + break + } + } + return calc * 1000000L + groupuin % 1000000L + } + + fun groupCode2GroupUin(groupcode: Long): Long { + var calc = groupcode / 1000000L + loop@ while (true) calc += when (calc) { + in 0..10 -> { + (202 - 0).toLong() + } + in 11..19 -> { + (480 - 11).toLong() + } + in 20..66 -> { + (2100 - 20).toLong() + } + in 67..156 -> { + (2010 - 67).toLong() + } + in 157..209 -> { + (2147 - 157).toLong() + } + in 210..309 -> { + (4100 - 210).toLong() + } + in 310..499 -> { + (3800 - 310).toLong() + } + else -> { + break@loop + } + } + return calc * 1000000L + groupcode % 1000000L + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt b/xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt new file mode 100644 index 0000000..9959e8f --- /dev/null +++ b/xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt @@ -0,0 +1,17 @@ +package qq.service.group + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class NotJoinedGroupInfo( + @SerialName("group_id") val groupId: Long, + @SerialName("max_member_cnt") val maxMember: Int, + @SerialName("member_count") val memberCount: Int, + @SerialName("group_name") val groupName: String, + @SerialName("group_desc") val groupDesc: String, + @SerialName("owner") val owner: Long, + @SerialName("create_time") val createTime: Long, + @SerialName("group_flag") val groupFlag: Int, + @SerialName("group_flag_ext") val groupFlagExt: Int, +) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/group/ProhibitedMemberInfo.kt b/xposed/src/main/java/qq/service/group/ProhibitedMemberInfo.kt new file mode 100644 index 0000000..6611dd7 --- /dev/null +++ b/xposed/src/main/java/qq/service/group/ProhibitedMemberInfo.kt @@ -0,0 +1,10 @@ +package qq.service.group + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ProhibitedMemberInfo( + @SerialName("user_id") val memberUin: Long, + @SerialName("time") val shutuptimestap: Int +) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/internals/MSFHandler.kt b/xposed/src/main/java/qq/service/internals/MSFHandler.kt index 7883266..2317332 100644 --- a/xposed/src/main/java/qq/service/internals/MSFHandler.kt +++ b/xposed/src/main/java/qq/service/internals/MSFHandler.kt @@ -2,13 +2,16 @@ package qq.service.internals import com.tencent.qphone.base.remote.FromServiceMsg import com.tencent.qphone.base.remote.ToServiceMsg +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter +import kotlin.coroutines.resume typealias MsfPush = (FromServiceMsg) -> Unit -typealias MsfResp = (ToServiceMsg, FromServiceMsg) -> Unit +typealias MsfResp = CancellableContinuation> internal object MSFHandler { private val mPushHandlers = hashMapOf() @@ -16,6 +19,13 @@ internal object MSFHandler { private val mPushLock = Mutex() private val mRespLock = Mutex() + private val seq = atomic(0) + + fun nextSeq(): Int { + seq.compareAndSet(0xFFFFFFF, 0) + return seq.incrementAndGet() + } + suspend fun registerPush(cmd: String, push: MsfPush) { mPushLock.withLock { mPushHandlers[cmd] = push @@ -51,7 +61,7 @@ internal object MSFHandler { val cmd = toServiceMsg.getAttribute("shamrock_uid") as? Int? ?: return@runCatching val resp = mRespHandler[cmd] - resp?.invoke(toServiceMsg, fromServiceMsg) + resp?.resume(toServiceMsg to fromServiceMsg) }.onFailure { LogCenter.log("MSF.onResp failed: ${it.message}", Level.ERROR) } From 7bacea328834f13b0194600e4ff0c18d03933256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Tue, 12 Mar 2024 21:02:36 +0800 Subject: [PATCH 15/20] =?UTF-8?q?`Shamrock`:=20=E3=82=A4=E3=83=99=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=83=97=E3=83=83=E3=82=B7=E3=83=A5=E3=81=AE=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- kritor | 2 +- .../main/java/kritor/server/KritorServer.kt | 2 + .../main/java/kritor/service/EventService.kt | 29 ++ .../java/kritor/service/MessageService.kt | 7 + .../internals/GlobalEventTransmitter.kt | 473 ++++++++++++++++++ .../shamrock/xposed/actions/FetchService.kt | 2 +- .../java/qq/service/contact/ContactHelper.kt | 2 +- .../main/java/qq/service/group/GroupHelper.kt | 2 +- .../java/qq/service/internals/AioListener.kt | 65 ++- .../service}/internals/NTServiceFetcher.kt | 4 +- .../main/java/qq/service/msg/MessageHelper.kt | 31 ++ 11 files changed, 611 insertions(+), 8 deletions(-) create mode 100644 xposed/src/main/java/kritor/service/EventService.kt create mode 100644 xposed/src/main/java/kritor/service/MessageService.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt rename xposed/src/main/java/{moe/fuqiuluo/shamrock => qq/service}/internals/NTServiceFetcher.kt (96%) create mode 100644 xposed/src/main/java/qq/service/msg/MessageHelper.kt diff --git a/kritor b/kritor index f007631..e4aac65 160000 --- a/kritor +++ b/kritor @@ -1 +1 @@ -Subproject commit f007631c7e17fe6220055a404fdaaa6e7a24a7ef +Subproject commit e4aac653e14249cbb6f27567ffd463165f6deebe diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index f7fa4c1..9496286 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -24,6 +24,8 @@ class KritorServer( .addService(FriendService) .addService(GroupService) .addService(GroupFileService) + .addService(MessageService) + .addService(EventService) .build()!! fun start(block: Boolean = false) { diff --git a/xposed/src/main/java/kritor/service/EventService.kt b/xposed/src/main/java/kritor/service/EventService.kt new file mode 100644 index 0000000..58c1235 --- /dev/null +++ b/xposed/src/main/java/kritor/service/EventService.kt @@ -0,0 +1,29 @@ +package kritor.service + +import io.kritor.event.EventRequest +import io.kritor.event.EventServiceGrpcKt +import io.kritor.event.EventStructure +import io.kritor.event.EventType +import io.kritor.event.eventStructure +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter + +object EventService: EventServiceGrpcKt.EventServiceCoroutineImplBase() { + override fun registerActiveListener(request: EventRequest): Flow { + return channelFlow { + when(request.type!!) { + EventType.CORE_EVENT -> TODO() + EventType.MESSAGE -> GlobalEventTransmitter.onMessageEvent { + send(eventStructure { + this.type = EventType.MESSAGE + this.message = it.second + }) + } + EventType.NOTICE -> TODO() + EventType.REQUEST -> TODO() + EventType.UNRECOGNIZED -> TODO() + } + } + } +} diff --git a/xposed/src/main/java/kritor/service/MessageService.kt b/xposed/src/main/java/kritor/service/MessageService.kt new file mode 100644 index 0000000..3776912 --- /dev/null +++ b/xposed/src/main/java/kritor/service/MessageService.kt @@ -0,0 +1,7 @@ +package kritor.service + +import io.kritor.message.MessageServiceGrpcKt + +internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImplBase() { + +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt new file mode 100644 index 0000000..cc72ed2 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt @@ -0,0 +1,473 @@ +package moe.fuqiuluo.shamrock.internals + +import com.tencent.qqnt.kernel.nativeinterface.MsgElement +import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import io.kritor.Scene +import io.kritor.contact +import io.kritor.event.MessageEvent +import io.kritor.event.messageEvent +import io.kritor.sender +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.io.core.BytePacketBuilder +import qq.service.QQInterfaces + +internal object GlobalEventTransmitter: QQInterfaces() { + private val messageEventFlow by lazy { + MutableSharedFlow>() + } + //private val noticeEventFlow by lazy { + // MutableSharedFlow() + //} + //private val requestEventFlow by lazy { + // MutableSharedFlow() + //} + + //private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent) + + //private suspend fun pushRequest(requestEvent: RequestEvent) = requestEventFlow.emit(requestEvent) + + private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message) + + object MessageTransmitter { + suspend fun transGroupMessage( + record: MsgRecord, + elements: ArrayList, + ): Boolean { + transMessageEvent(record, messageEvent { + this.time = record.msgTime.toInt() + this.scene = Scene.GROUP + this.messageId = record.msgId + this.messageSeq = record.msgSeq + this.contact = contact { + this.scene = scene + this.peer = record.peerUin.toString() + this.subPeer = record.peerUid + } + this.sender = sender { + this.uin = record.senderUin + this.uid = record.senderUid + this.nick = record.sendNickName + } + }) + return true + } + + suspend fun transPrivateMessage( + record: MsgRecord, + elements: ArrayList, + ): Boolean { + val botUin = app.longAccountUin + var nickName = record.sendNickName + + return true + } + + suspend fun transTempMessage( + record: MsgRecord, + elements: ArrayList, + groupCode: Long, + fromNick: String, + ): Boolean { + val botUin = app.longAccountUin + var nickName = record.sendNickName + + return true + } + + suspend fun transGuildMessage( + record: MsgRecord, + elements: ArrayList, + ): Boolean { + val botUin = app.longAccountUin + var nickName = record.sendNickName + + return true + } + } + + /* + /** + * 文件通知 通知器 + */ + object FileNoticeTransmitter { + /** + * 推送私聊文件事件 + */ + suspend fun transPrivateFileEvent( + msgTime: Long, + userId: Long, + fileId: String, + fileSubId: String, + fileName: String, + fileSize: Long, + expireTime: Long, + url: String + ): Boolean { + pushNotice(NoticeEvent( + time = msgTime, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.PrivateUpload, + operatorId = userId, + userId = userId, + senderId = userId, + privateFile = PrivateFileMsg( + id = fileId, + name = fileName, + size = fileSize, + url = url, + subId = fileSubId, + expire = expireTime + ) + )) + return true + } + + /** + * 推送私聊文件事件 + */ + suspend fun transGroupFileEvent( + msgTime: Long, + userId: Long, + groupId: Long, + uuid: String, + fileName: String, + fileSize: Long, + bizId: Int, + url: String + ): Boolean { + pushNotice(NoticeEvent( + time = msgTime, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.GroupUpload, + operatorId = userId, + userId = userId, + groupId = groupId, + file = GroupFileMsg( + id = uuid, + name = fileName, + size = fileSize, + busid = bizId.toLong(), + url = url + ) + )) + return true + } + } + + /** + * 群聊通知 通知器 + */ + 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, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.Notify, + subType = NoticeSubType.Poke, + operatorId = operation, + userId = operation, + groupId = groupCode, + target = target, + pokeDetail = PokeDetail( + action = action, + suffix = suffix, + actionImg = actionImg + ) + )) + return true + } + + suspend fun transGroupMemberNumChanged( + time: Long, + target: Long, + targetUid: String, + groupCode: Long, + operator: Long, + operatorUid: String, + noticeType: NoticeType, + noticeSubType: NoticeSubType + ): Boolean { + pushNotice(NoticeEvent( + time = time, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = noticeType, + subType = noticeSubType, + operatorId = operator, + userId = target, + senderId = operator, + target = target, + groupId = groupCode, + targetUid = targetUid, + operatorUid = operatorUid, + userUid = targetUid + )) + return true + } + + suspend fun transGroupAdminChanged( + msgTime: Long, + target: Long, + targetUid: String, + groupCode: Long, + setAdmin: Boolean + ): Boolean { + pushNotice(NoticeEvent( + time = msgTime, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.GroupAdminChange, + subType = if (setAdmin) NoticeSubType.Set else NoticeSubType.UnSet, + operatorId = 0, + userId = target, + userUid = targetUid, + target = target, + targetUid = targetUid, + groupId = groupCode + )) + return true + } + + suspend fun transGroupBan( + msgTime: Long, + subType: NoticeSubType, + operator: Long, + operatorUid: String, + target: Long, + targetUid: String, + groupCode: Long, + duration: Int + ): Boolean { + pushNotice(NoticeEvent( + time = msgTime, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.GroupBan, + subType = subType, + operatorId = operator, + userId = target, + senderId = operator, + target = target, + groupId = groupCode, + duration = duration, + operatorUid = operatorUid, + targetUid = targetUid + )) + return true + } + + suspend fun transGroupMsgRecall( + time: Long, + operator: Long, + target: Long, + groupCode: Long, + msgHash: Int, + tipText: String + ): Boolean { + pushNotice(NoticeEvent( + time = time, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.GroupRecall, + operatorId = operator, + userId = target, + msgId = msgHash, + tip = tipText, + groupId = groupCode + )) + return true + } + + suspend fun transCardChange( + time: Long, + targetId: Long, + oldCard: String, + newCard: String, + groupId: Long + ): Boolean { + pushNotice(NoticeEvent( + time = time, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.GroupCard, + userId = targetId, + cardNew = newCard, + cardOld = oldCard, + groupId = groupId + )) + return true + } + + suspend fun transTitleChange( + time: Long, + targetId: Long, + title: String, + groupId: Long + ): Boolean { + pushNotice(NoticeEvent( + time = time, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.Notify, + userId = targetId, + groupId = groupId, + title = title, + subType = NoticeSubType.Title + )) + return true + } + + suspend fun transEssenceChange( + time: Long, + senderUin: Long, + operatorUin: Long, + msgId: Int, + groupId: Long, + subType: NoticeSubType + ): Boolean { + pushNotice(NoticeEvent( + time = time, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.Essence, + senderId = senderUin, + groupId = groupId, + operatorId = operatorUin, + msgId = msgId, + subType = subType + )) + return true + } + } + + /** + * 私聊通知 通知器 + */ + object PrivateNoticeTransmitter { + suspend fun transPrivatePoke(msgTime: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?): Boolean { + pushNotice(NoticeEvent( + time = msgTime, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.Notify, + subType = NoticeSubType.Poke, + operatorId = operation, + userId = operation, + senderId = operation, + target = target, + pokeDetail = PokeDetail( + actionImg = actionImg, + action = action, + suffix = suffix + ) + )) + return true + } + + suspend fun transPrivateRecall(time: Long, operation: Long, msgHashId: Int, tipText: String): Boolean { + pushNotice(NoticeEvent( + time = time, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.FriendRecall, + operatorId = operation, + userId = operation, + msgId = msgHashId, + tip = tipText + )) + return true + } + + } + + /** + * 请求 通知器 + */ + object RequestTransmitter { + suspend fun transFriendApp(time: Long, operation: Long, tipText: String, flag: String): Boolean { + pushRequest(RequestEvent( + time = time, + selfId = app.longAccountUin, + postType = PostType.Request, + type = RequestType.Friend, + userId = operation, + comment = tipText, + flag = flag + )) + return true + } + + suspend fun transGroupApply( + time: Long, + applier: Long, + applierUid: String, + reason: String, + groupCode: Long, + flag: String, + subType: RequestSubType + ): Boolean { + pushRequest(RequestEvent( + time = time, + selfId = app.longAccountUin, + postType = PostType.Request, + type = RequestType.Group, + userId = applier, + userUid = applierUid, + comment = reason, + groupId = groupCode, + subType = subType, + flag = flag + )) + return true + } + }*/ + + suspend inline fun onMessageEvent(collector: FlowCollector>) { + messageEventFlow.collect { + GlobalScope.launch { + collector.emit(it) + } + } + } + + /* + suspend inline fun onNoticeEvent(collector: FlowCollector) { + noticeEventFlow.collect { + GlobalScope.launch { + collector.emit(it) + } + } + } + + suspend inline fun onRequestEvent(collector: FlowCollector) { + requestEventFlow.collect { + GlobalScope.launch { + collector.emit(it) + } + } + }*/ +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FetchService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FetchService.kt index c8cf092..d84ee96 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FetchService.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/FetchService.kt @@ -11,7 +11,7 @@ import moe.fuqiuluo.shamrock.tools.hookMethod import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.internals.NTServiceFetcher +import qq.service.internals.NTServiceFetcher import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader import moe.fuqiuluo.symbols.XposedHook diff --git a/xposed/src/main/java/qq/service/contact/ContactHelper.kt b/xposed/src/main/java/qq/service/contact/ContactHelper.kt index 30240eb..a516495 100644 --- a/xposed/src/main/java/qq/service/contact/ContactHelper.kt +++ b/xposed/src/main/java/qq/service/contact/ContactHelper.kt @@ -10,7 +10,7 @@ import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import moe.fuqiuluo.shamrock.internals.NTServiceFetcher +import qq.service.internals.NTServiceFetcher import qq.service.QQInterfaces import kotlin.coroutines.resume diff --git a/xposed/src/main/java/qq/service/group/GroupHelper.kt b/xposed/src/main/java/qq/service/group/GroupHelper.kt index ca57424..e6dc318 100644 --- a/xposed/src/main/java/qq/service/group/GroupHelper.kt +++ b/xposed/src/main/java/qq/service/group/GroupHelper.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.internals.NTServiceFetcher +import qq.service.internals.NTServiceFetcher import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty import moe.fuqiuluo.shamrock.tools.putBuf32Long import moe.fuqiuluo.shamrock.tools.slice diff --git a/xposed/src/main/java/qq/service/internals/AioListener.kt b/xposed/src/main/java/qq/service/internals/AioListener.kt index c14fe19..86e7a27 100644 --- a/xposed/src/main/java/qq/service/internals/AioListener.kt +++ b/xposed/src/main/java/qq/service/internals/AioListener.kt @@ -24,6 +24,7 @@ import com.tencent.qqnt.kernel.nativeinterface.ImportOldDbMsgNotifyInfo import com.tencent.qqnt.kernel.nativeinterface.InputStatusInfo import com.tencent.qqnt.kernel.nativeinterface.KickedInfo import com.tencent.qqnt.kernel.nativeinterface.MsgAbstract +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.MsgSetting @@ -33,12 +34,74 @@ import com.tencent.qqnt.kernel.nativeinterface.SearchGroupFileResult import com.tencent.qqnt.kernel.nativeinterface.TabStatusInfo import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo import com.tencent.qqnt.kernel.nativeinterface.UnreadCntInfo +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter +import qq.service.msg.MessageHelper object AioListener: IKernelMsgListener { - override fun onRecvMsg(arrayList: ArrayList?) { + override fun onRecvMsg(msgs: ArrayList) { + msgs.forEach { + GlobalScope.launch { + try { + onMsg(it) + } catch (e: Exception) { + LogCenter.log("OnMessage: " + e.stackTraceToString(), Level.WARN) + } + } + } + } + private suspend fun onMsg(record: MsgRecord) { + when (record.chatType) { + MsgConstant.KCHATTYPEGROUP -> { + LogCenter.log("群消息(group = ${record.peerName}(${record.peerUin}), uin = ${record.senderUin}, id = ${record.msgId})") + + if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(record, record.elements)) { + LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN) + } + } + + MsgConstant.KCHATTYPEC2C -> { + LogCenter.log("私聊消息(private = ${record.senderUin}, id = [${record.msgId} | ${record.msgSeq}])") + + if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage( + record, record.elements + ) + ) { + LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN) + } + } + + MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> { + var groupCode = 0L + var fromNick = "" + MessageHelper.getTempChatInfo(record.chatType, record.senderUid).onSuccess { + groupCode = it.groupCode.toLong() + fromNick = it.fromNick + } + + LogCenter.log("私聊临时消息(private = ${record.senderUin}, groupId=$groupCode)") + + if (!GlobalEventTransmitter.MessageTransmitter.transTempMessage(record, record.elements, groupCode, fromNick) + ) { + LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN) + } + } + + MsgConstant.KCHATTYPEGUILD -> { + LogCenter.log("频道消息(guildId = ${record.guildId}, sender = ${record.senderUid}, id = [${record.msgId}])") + if (!GlobalEventTransmitter.MessageTransmitter + .transGuildMessage(record, record.elements) + ) { + LogCenter.log("频道消息推送失败 -> MessageTransmitter", Level.WARN) + } + } + + else -> LogCenter.log("不支持PUSH事件: ${record.chatType}") + } } override fun onMsgRecall(chatType: Int, peerId: String, msgId: Long) { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt b/xposed/src/main/java/qq/service/internals/NTServiceFetcher.kt similarity index 96% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt rename to xposed/src/main/java/qq/service/internals/NTServiceFetcher.kt index 8f03741..5c6f8c8 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/NTServiceFetcher.kt +++ b/xposed/src/main/java/qq/service/internals/NTServiceFetcher.kt @@ -1,4 +1,4 @@ -package moe.fuqiuluo.shamrock.internals +package qq.service.internals import com.tencent.qqnt.kernel.api.IKernelService import com.tencent.qqnt.kernel.api.impl.MsgService @@ -10,8 +10,6 @@ import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.tools.hookMethod import moe.fuqiuluo.shamrock.utils.PlatformUtils -import qq.service.internals.AioListener -import qq.service.internals.msgService internal object NTServiceFetcher { private lateinit var iKernelService: IKernelService diff --git a/xposed/src/main/java/qq/service/msg/MessageHelper.kt b/xposed/src/main/java/qq/service/msg/MessageHelper.kt new file mode 100644 index 0000000..29c5c28 --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/MessageHelper.kt @@ -0,0 +1,31 @@ +package qq.service.msg + +import com.tencent.qqnt.kernel.api.IKernelService +import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import qq.service.QQInterfaces +import qq.service.internals.msgService +import kotlin.coroutines.resume + +internal object MessageHelper: QQInterfaces() { + suspend fun getTempChatInfo(chatType: Int, uid: String): Result { + val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService + ?: return Result.failure(Exception("获取消息服务失败")) + val info: TempChatInfo = withTimeoutOrNull(5000) { + suspendCancellableCoroutine { + msgService.getTempChatInfo(chatType, uid) { code, msg, tempChatInfo -> + if (code == 0) { + it.resume(tempChatInfo) + } else { + LogCenter.log("获取临时会话信息失败: $code:$msg", Level.ERROR) + it.resume(null) + } + } + } + } ?: return Result.failure(Exception("获取临时会话信息失败")) + return Result.success(info) + } +} \ No newline at end of file From a95d8d85e89370696ae7c9509a1ce056b3468bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Thu, 14 Mar 2024 20:33:30 +0800 Subject: [PATCH 16/20] =?UTF-8?q?`Shamrock`:=20=E5=AE=8C=E6=88=90=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- app/src/main/cpp/CMakeLists.txt | 1 - app/src/main/cpp/cqcode.cpp | 138 ----- app/src/main/cpp/group_honor.cpp | 87 --- app/src/main/cpp/interface/cqcode.h | 20 - app/src/main/cpp/message.cpp | 120 +---- .../oidb/cmd0x11c5/NtV2RichMediaRsp.kt | 2 +- .../qqnt/kernel/api/impl/MsgService.java | 5 + .../java/kritor/service/GroupFileService.kt | 8 +- .../internals/GlobalEventTransmitter.kt | 71 ++- .../shamrock/xposed/actions/AntiDetection.kt | 14 + .../main/java/qq/service/bdh/FileTransfer.kt | 190 +++++++ .../java/qq/service/bdh/NtV2RichMediaSvc.kt | 495 ++++++++++++++++++ .../main/java/qq/service/bdh/RichProtoSvc.kt | 429 +++++++++++++++ .../main/java/qq/service/bdh/TryUpPicData.kt | 13 + .../java/qq/service/file/GroupFileHelper.kt | 4 +- .../main/java/qq/service/group/GroupHelper.kt | 8 +- .../java/qq/service/internals/AioListener.kt | 313 +---------- .../service/kernel/SimpleKernelMsgListener.kt | 316 +++++++++++ .../main/java/qq/service/msg/MessageHelper.kt | 24 + .../main/java/qq/service/msg/MsgConvertor.kt | 405 ++++++++++++++ 20 files changed, 1969 insertions(+), 694 deletions(-) delete mode 100644 app/src/main/cpp/cqcode.cpp delete mode 100644 app/src/main/cpp/group_honor.cpp delete mode 100644 app/src/main/cpp/interface/cqcode.h create mode 100644 xposed/src/main/java/qq/service/bdh/FileTransfer.kt create mode 100644 xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt create mode 100644 xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt create mode 100644 xposed/src/main/java/qq/service/bdh/TryUpPicData.kt create mode 100644 xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt create mode 100644 xposed/src/main/java/qq/service/msg/MsgConvertor.kt diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index a69e8ff..ea53a89 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -37,7 +37,6 @@ add_library(${CMAKE_PROJECT_NAME} SHARED # List C/C++ source files with relative paths to this CMakeLists.txt. ${SRC_DIR} md5.cpp - cqcode.cpp silk.cpp message.cpp shamrock.cpp) diff --git a/app/src/main/cpp/cqcode.cpp b/app/src/main/cpp/cqcode.cpp deleted file mode 100644 index 60a5682..0000000 --- a/app/src/main/cpp/cqcode.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include -#include "cqcode.h" - -inline void replace_string(std::string& str, const std::string& from, const std::string& to) { - size_t startPos = 0; - while ((startPos = str.find(from, startPos)) != std::string::npos) { - str.replace(startPos, from.length(), to); - startPos += to.length(); - } -} - -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>& dest) { - std::string cache; - bool is_start = false; - std::string key_tmp; - std::unordered_map kv; - 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 kv; - replace_string(cache, "[", "["); - replace_string(cache, "]", "]"); - replace_string(cache, "&", "&"); - kv.emplace("_type", "text"); - kv.emplace("text", cache); - dest.push_back(kv); - cache.clear(); - } - std::string_view cq_flag(&code[i],4); - if(cq_flag == "[CQ:"){ - is_start = true; - i += 3; - }else{ - cache += c; - } - } - } - else if (c == "=") { - if (is_start) { - if (cache.empty()) { - throw illegal_code(); - } else { - if (key_tmp.empty()) { - key_tmp.append(cache); - cache.clear(); - } else { - cache += c; - } - } - } else { - cache += 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, "[", "["); - replace_string(cache, "]", "]"); - replace_string(cache, ",", ","); - replace_string(cache, "&", "&"); - kv.emplace(key_tmp, cache); - cache.clear(); - key_tmp.clear(); - } - } - } else { - cache += c; - } - } - else if (c == "]") { - if (is_start) { - if (!cache.empty()) { - if (!key_tmp.empty()) { - replace_string(cache, "[", "["); - replace_string(cache, "]", "]"); - replace_string(cache, ",", ","); - replace_string(cache, "&", "&"); - kv.emplace(key_tmp, cache); - } else { - kv.emplace("_type", cache); - } - dest.push_back(kv); - kv.clear(); - key_tmp.clear(); - cache.clear(); - is_start = false; - } - } else { - cache += c; - } - } - else { - cache += c; - i += (utf8_char_len - 1); - } - } - if (!cache.empty()) { - std::unordered_map kv; - replace_string(cache, "[", "["); - replace_string(cache, "]", "]"); - replace_string(cache, "&", "&"); - kv.emplace("_type", "text"); - kv.emplace("text", cache); - dest.push_back(kv); - } -} diff --git a/app/src/main/cpp/group_honor.cpp b/app/src/main/cpp/group_honor.cpp deleted file mode 100644 index ebae11f..0000000 --- a/app/src/main/cpp/group_honor.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "jni.h" -#include -#include -#include - -struct Honor { - int id; - std::string name; - std::string icon_url; - int priority; -}; - -int calc_honor_flag(int honor_id, char honor_flag); - -jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor); - - -extern "C" -JNIEXPORT jobject JNICALL -Java_moe_fuqiuluo_shamrock_remote_action_handlers_GetTroopHonor_nativeDecodeHonor(JNIEnv *env, jobject thiz, - jstring user_id, - jint honor_id, - jbyte honor_flag) { - static std::vector honor_list = { - Honor{1, "龙王", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150116_n4PxCiurbm.png", 1}, - Honor{2, "群聊之火", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190136_92JEGFKC5k.png", 3}, - Honor{3, "群聊炽焰", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190204_zgCTeSrMq1.png", 4}, - Honor{5, "冒尖小春笋", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150335_tUJCAtoKVP.png", 5}, - Honor{6, "快乐源泉", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150434_3tDmsJExCP.png", 7}, - Honor{7, "学术新星", "https://sola.gtimg.cn/aoi/sola/20200515140645_j0X6gbuHNP.png", 8}, - Honor{8, "顶尖学霸", "https://sola.gtimg.cn/aoi/sola/20200515140639_0CtWOpfVzK.png", 9}, - Honor{9, "至尊学神", "https://sola.gtimg.cn/aoi/sola/20200515140628_P8UEYBjMBT.png", 10}, - Honor{10, "一笔当先", "https://sola.gtimg.cn/aoi/sola/20200515140654_4r94tSCdaB.png", 11}, - Honor{11, "奋进小翠竹", "https://sola.gtimg.cn/aoi/sola/20200812151819_wbj6z2NGoB.png", 6}, - Honor{12, "氛围魔杖", "https://sola.gtimg.cn/aoi/sola/20200812151831_4ZJgQCaD1H.png", 2}, - Honor{13, "壕礼皇冠", "https://sola.gtimg.cn/aoi/sola/20200930154050_juZOAMg7pt.png", 12}, - }; - int flag = calc_honor_flag(honor_id, honor_flag); - if ((honor_id != 1 && honor_id != 2 && honor_id != 3) || flag != 1) { - auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) { - return honor.id == honor_id; - }); - return make_honor_object(env, user_id, honor); - } else { - auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) { - return honor.id == honor_id; - }); - std::string url = "https://static-res.qq.com/static-res/groupInteract/vas/a/" + std::to_string(honor_id) + "_1.png"; - honor = Honor{honor_id, honor.name, url, honor.priority}; - return make_honor_object(env, user_id, honor); - } -} - -int calc_honor_flag(int honor_id, char honor_flag) { - int flag; - if (honor_flag == 0) { - return 0; - } - if (honor_id == 1) { - flag = honor_flag; - } else if (honor_id == 2 || honor_id == 3) { - flag = honor_flag >> 2; - } else if (honor_id != 4) { - return 0; - } else { - flag = honor_flag >> 4; - } - return flag & 3; -} - -jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor) { - jclass GroupMemberHonor = env->FindClass("moe/fuqiuluo/shamrock/remote/service/data/GroupMemberHonor"); - jmethodID GroupMemberHonor_init = env->GetMethodID(GroupMemberHonor, "", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V"); - auto user_id_str = (jstring) user_id; - jstring honor_desc = env->NewStringUTF(honor.name.c_str()); - jstring uin_name = env->NewStringUTF(""); - jstring honor_icon_url = env->NewStringUTF(honor.icon_url.c_str()); - jobject ret = env->NewObject(GroupMemberHonor, GroupMemberHonor_init, user_id_str, uin_name, honor_icon_url, 0, honor.id, honor_desc); - - env->DeleteLocalRef(GroupMemberHonor); - env->DeleteLocalRef(user_id_str); - env->DeleteLocalRef(honor_desc); - env->DeleteLocalRef(honor_icon_url); - - return ret; -} diff --git a/app/src/main/cpp/interface/cqcode.h b/app/src/main/cpp/interface/cqcode.h deleted file mode 100644 index aed24f9..0000000 --- a/app/src/main/cpp/interface/cqcode.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef UNTITLED_CQCODE_H -#define UNTITLED_CQCODE_H - -#include -#include -#include -#include - -class illegal_code: std::exception { -public: - [[nodiscard]] const char * what() const noexcept override { - return "Error cq code."; - } -}; - -void decode_cqcode(const std::string& code, std::vector>& dest); - -void encode_cqcode(const std::vector>& segment, std::string& dest); - -#endif //UNTITLED_CQCODE_H diff --git a/app/src/main/cpp/message.cpp b/app/src/main/cpp/message.cpp index 97fec4a..09284d4 100644 --- a/app/src/main/cpp/message.cpp +++ b/app/src/main/cpp/message.cpp @@ -1,5 +1,4 @@ #include "jni.h" -#include "cqcode.h" #include inline void replace_string(std::string& str, const std::string& from, const std::string& to) { @@ -12,7 +11,7 @@ inline void replace_string(std::string& str, const std::string& from, const std: extern "C" JNIEXPORT jlong JNICALL -Java_moe_fuqiuluo_shamrock_helper_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz, +Java_qq_service_msg_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz, jint chat_type, jlong time) { static std::random_device rd; @@ -32,123 +31,6 @@ Java_moe_fuqiuluo_shamrock_helper_MessageHelper_getChatType(JNIEnv *env, jobject return (int32_t) ((int64_t) msg_id & 0xffL); } -extern "C" -JNIEXPORT jobject JNICALL -Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeDecodeCQCode(JNIEnv *env, jobject thiz, - jstring code) { - jclass ArrayList = env->FindClass("java/util/ArrayList"); - jmethodID NewArrayList = env->GetMethodID(ArrayList, "", "()V"); - jmethodID ArrayListAdd = env->GetMethodID(ArrayList, "add", "(Ljava/lang/Object;)Z"); - jobject arrayList = env->NewObject(ArrayList, NewArrayList); - - const char* cCode = env->GetStringUTFChars(code, nullptr); - std::string cppCode = cCode; - std::vector> dest; - try { - decode_cqcode(cppCode, dest); - } catch (illegal_code& code) { - return arrayList; - } - - jclass HashMap = env->FindClass("java/util/HashMap"); - jmethodID NewHashMap = env->GetMethodID(HashMap, "", "()V"); - jclass String = env->FindClass("java/lang/String"); - jmethodID NewString = env->GetMethodID(String, "", "([BLjava/lang/String;)V"); - jstring charset = env->NewStringUTF("UTF-8"); - jmethodID put = env->GetMethodID(HashMap, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto& map : dest) { - jobject hashMap = env->NewObject(HashMap, NewHashMap); - for (const auto& pair : map) { - jbyteArray keyArray = env->NewByteArray((int) pair.first.size()); - jbyteArray valueArray = env->NewByteArray((int) pair.second.size()); - env->SetByteArrayRegion(keyArray, 0, (int) pair.first.size(), (jbyte*)pair.first.c_str()); - env->SetByteArrayRegion(valueArray, 0, (int) pair.second.size(), (jbyte*)pair.second.c_str()); - auto key = (jstring) env->NewObject(String, NewString, keyArray, charset); - auto value = (jstring) env->NewObject(String, NewString, valueArray, charset); - env->CallObjectMethod(hashMap, put, key, value); - } - env->CallBooleanMethod(arrayList, ArrayListAdd, hashMap); - } - - env->DeleteLocalRef(ArrayList); - env->DeleteLocalRef(HashMap); - env->DeleteLocalRef(String); - env->DeleteLocalRef(charset); - env->ReleaseStringUTFChars(code, cCode); - - return arrayList; -} - -extern "C" -JNIEXPORT jstring JNICALL -Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeEncodeCQCode(JNIEnv *env, jobject thiz, - jobject segment_list) { - jclass List = env->FindClass("java/util/List"); - jmethodID ListSize = env->GetMethodID(List, "size", "()I"); - jmethodID ListGet = env->GetMethodID(List, "get", "(I)Ljava/lang/Object;"); - jclass Map = env->FindClass("java/util/Map"); - jmethodID MapGet = env->GetMethodID(Map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); - jmethodID entrySetMethod = env->GetMethodID(Map, "entrySet", "()Ljava/util/Set;"); - jclass setClass = env->FindClass("java/util/Set"); - jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;"); - jclass entryClass = env->FindClass("java/util/Map$Entry"); - jmethodID getKeyMethod = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;"); - jmethodID getValueMethod = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;"); - - std::string result; - jint size = env->CallIntMethod(segment_list, ListSize); - for (int i = 0; i < size; i++ ) { - jobject segment = env->CallObjectMethod(segment_list, ListGet, i); - jobject entrySet = env->CallObjectMethod(segment, entrySetMethod); - jobject iterator = env->CallObjectMethod(entrySet, iteratorMethod); - auto type = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("_type")); - auto typeString = env->GetStringUTFChars(type, nullptr); - if (strcmp(typeString, "text") == 0) { - auto text = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("text")); - auto textString = env->GetStringUTFChars(text, nullptr); - std::string tmpValue = textString; - replace_string(tmpValue, "&", "&"); - replace_string(tmpValue, "[", "["); - replace_string(tmpValue, "]", "]"); - replace_string(tmpValue, ",", ","); - result.append(tmpValue); - env->ReleaseStringUTFChars(text, textString); - } else { - result.append("[CQ:"); - result.append(typeString); - while (env->CallBooleanMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "hasNext", "()Z"))) { - jobject entry = env->CallObjectMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "next", "()Ljava/lang/Object;")); - auto key = (jstring) env->CallObjectMethod(entry, getKeyMethod); - auto value = (jstring) env->CallObjectMethod(entry, getValueMethod); - auto keyString = env->GetStringUTFChars(key, nullptr); - auto valueString = env->GetStringUTFChars(value, nullptr); - if (strcmp(keyString, "_type") != 0) { - std::string tmpValue = valueString; - replace_string(tmpValue, "&", "&"); - replace_string(tmpValue, "[", "["); - replace_string(tmpValue, "]", "]"); - replace_string(tmpValue, ",", ","); - result.append(",").append(keyString).append("=").append(tmpValue); - } - env->ReleaseStringUTFChars(key, keyString); - env->ReleaseStringUTFChars(value, valueString); - env->DeleteLocalRef(entry); - env->DeleteLocalRef(key); - env->DeleteLocalRef(value); - } - result.append("]"); - } - env->ReleaseStringUTFChars(type, typeString); - } - - env->DeleteLocalRef(List); - env->DeleteLocalRef(Map); - env->DeleteLocalRef(setClass); - env->DeleteLocalRef(entryClass); - return env->NewStringUTF(result.c_str()); -} - - extern "C" JNIEXPORT jlong JNICALL Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz, diff --git a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt index 96b8f28..e12c009 100644 --- a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt +++ b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt @@ -9,7 +9,7 @@ import moe.fuqiuluo.symbols.Protobuf @Serializable data class NtV2RichMediaRsp( - @ProtoNumber(1) val head: RspHead, + @ProtoNumber(1) val head: RspHead?, @ProtoNumber(2) val upload: UploadRsp?, @ProtoNumber(3) val download: DownloadRsp?, @ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?, diff --git a/qqinterface/src/main/java/com/tencent/qqnt/kernel/api/impl/MsgService.java b/qqinterface/src/main/java/com/tencent/qqnt/kernel/api/impl/MsgService.java index 3fc02e1..4fa64c0 100644 --- a/qqinterface/src/main/java/com/tencent/qqnt/kernel/api/impl/MsgService.java +++ b/qqinterface/src/main/java/com/tencent/qqnt/kernel/api/impl/MsgService.java @@ -18,6 +18,11 @@ public class MsgService { public void addMsgListener(IKernelMsgListener listener) { } + public void removeMsgListener(@NotNull IKernelMsgListener iKernelMsgListener) { + + } + + public String getRichMediaFilePathForGuild(@NotNull RichMediaFilePathInfo richMediaFilePathInfo) { return null; } diff --git a/xposed/src/main/java/kritor/service/GroupFileService.kt b/xposed/src/main/java/kritor/service/GroupFileService.kt index 3dc2121..1daaa63 100644 --- a/xposed/src/main/java/kritor/service/GroupFileService.kt +++ b/xposed/src/main/java/kritor/service/GroupFileService.kt @@ -31,7 +31,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti ).toByteArray() val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) - if (!fromServiceMsg.isSuccess) { + if (fromServiceMsg.wupBuffer == null) { throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) } val oidbPkg = oidb_sso.OIDBSSOPkg() @@ -57,7 +57,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti folderId = request.folderId ) ).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) - if (!fromServiceMsg.isSuccess) { + if (fromServiceMsg.wupBuffer == null) { throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) } val oidbPkg = oidb_sso.OIDBSSOPkg() @@ -82,7 +82,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti } val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) - if (!fromServiceMsg.isSuccess) { + if (fromServiceMsg.wupBuffer == null) { throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) } val oidbPkg = oidb_sso.OIDBSSOPkg() @@ -106,7 +106,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti folderName = request.name ) ).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) - if (!fromServiceMsg.isSuccess) { + if (fromServiceMsg.wupBuffer == null) { throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) } val oidbPkg = oidb_sso.OIDBSSOPkg() diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt index cc72ed2..eb2de00 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt @@ -1,18 +1,22 @@ +@file:OptIn(DelicateCoroutinesApi::class) + package moe.fuqiuluo.shamrock.internals import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgRecord -import io.kritor.Scene -import io.kritor.contact import io.kritor.event.MessageEvent +import io.kritor.event.Scene +import io.kritor.event.contact import io.kritor.event.messageEvent -import io.kritor.sender +import io.kritor.event.sender +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.io.core.BytePacketBuilder import qq.service.QQInterfaces +import qq.service.msg.toKritorMessages internal object GlobalEventTransmitter: QQInterfaces() { private val messageEventFlow by lazy { @@ -51,6 +55,7 @@ internal object GlobalEventTransmitter: QQInterfaces() { this.uid = record.senderUid this.nick = record.sendNickName } + this.elements.addAll(elements.toKritorMessages(record)) }) return true } @@ -59,9 +64,23 @@ internal object GlobalEventTransmitter: QQInterfaces() { record: MsgRecord, elements: ArrayList, ): Boolean { - val botUin = app.longAccountUin - var nickName = record.sendNickName - + transMessageEvent(record, messageEvent { + this.time = record.msgTime.toInt() + this.scene = Scene.FRIEND + this.messageId = record.msgId + this.messageSeq = record.msgSeq + this.contact = contact { + this.scene = scene + this.peer = record.senderUin.toString() + this.subPeer = record.senderUid + } + this.sender = sender { + this.uin = record.senderUin + this.uid = record.senderUid + this.nick = record.sendNickName + } + this.elements.addAll(elements.toKritorMessages(record)) + }) return true } @@ -71,9 +90,23 @@ internal object GlobalEventTransmitter: QQInterfaces() { groupCode: Long, fromNick: String, ): Boolean { - val botUin = app.longAccountUin - var nickName = record.sendNickName - + transMessageEvent(record, messageEvent { + this.time = record.msgTime.toInt() + this.scene = Scene.FRIEND + this.messageId = record.msgId + this.messageSeq = record.msgSeq + this.contact = contact { + this.scene = scene + this.peer = record.senderUin.toString() + this.subPeer = groupCode.toString() + } + this.sender = sender { + this.uin = record.senderUin + this.uid = record.senderUid + this.nick = record.sendNickName + } + this.elements.addAll(elements.toKritorMessages(record)) + }) return true } @@ -81,9 +114,23 @@ internal object GlobalEventTransmitter: QQInterfaces() { record: MsgRecord, elements: ArrayList, ): Boolean { - val botUin = app.longAccountUin - var nickName = record.sendNickName - + transMessageEvent(record, messageEvent { + this.time = record.msgTime.toInt() + this.scene = Scene.GUILD + this.messageId = record.msgId + this.messageSeq = record.msgSeq + this.contact = contact { + this.scene = scene + this.peer = record.channelId.toString() + this.subPeer = record.guildId + } + this.sender = sender { + this.uin = record.senderUin + this.uid = record.senderUid + this.nick = record.sendNickName + } + this.elements.addAll(elements.toKritorMessages(record)) + }) return true } } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt index 878cc10..e402c6f 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt @@ -7,6 +7,7 @@ import android.content.pm.PackageManager import android.content.pm.VersionedPackage import android.os.Build import android.os.Looper +import com.tencent.qphone.base.remote.ToServiceMsg import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XSharedPreferences import de.robv.android.xposed.XposedHelpers @@ -21,6 +22,8 @@ import moe.fuqiuluo.shamrock.xposed.XposedEntry import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader import moe.fuqiuluo.symbols.XposedHook +import mqq.app.MobileQQ +import qq.service.QQInterfaces @XposedHook(priority = 0) class AntiDetection: IAction { @@ -36,6 +39,17 @@ class AntiDetection: IAction { if (ShamrockConfig[AntiJvmTrace]) antiTrace() antiMemoryWalking() + antiO3Report() + } + + private fun antiO3Report() { + QQInterfaces.app.javaClass.hookMethod("sendToService").before { + val toServiceMsg = it.args[0] as ToServiceMsg? + if (toServiceMsg != null && toServiceMsg.serviceCmd.startsWith("trpc.o3")) { + LogCenter.log("拦截trpc.o3环境上报包", Level.WARN) + it.result = null + } + } } private fun antiGetPackageGidsDetection(ctx: Context) { diff --git a/xposed/src/main/java/qq/service/bdh/FileTransfer.kt b/xposed/src/main/java/qq/service/bdh/FileTransfer.kt new file mode 100644 index 0000000..4e44bcb --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/FileTransfer.kt @@ -0,0 +1,190 @@ +@file:OptIn(DelicateCoroutinesApi::class) + +package qq.service.bdh + +import com.tencent.mobileqq.transfile.BaseTransProcessor +import com.tencent.mobileqq.transfile.FileMsg +import com.tencent.mobileqq.transfile.TransferRequest +import com.tencent.mobileqq.transfile.api.ITransFileController +import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.utils.MD5 +import mqq.app.AppRuntime +import qq.service.QQInterfaces +import java.io.File +import java.lang.Math.abs +import kotlin.coroutines.resume +import kotlin.random.Random + +internal abstract class FileTransfer { + suspend fun transC2CResource( + peerId: String, + file: File, + fileType: Int, busiType: Int, + wait: Boolean = true, + builder: (TransferRequest) -> Unit + ): Boolean { + val runtime = QQInterfaces.app + val transferRequest = TransferRequest() + transferRequest.needSendMsg = false + transferRequest.mSelfUin = runtime.account + transferRequest.mPeerUin = peerId + transferRequest.mSecondId = runtime.currentAccountUin + transferRequest.mUinType = FileMsg.UIN_BUDDY + transferRequest.mFileType = fileType + transferRequest.mUniseq = createMessageUniseq() + transferRequest.mIsUp = true + builder(transferRequest) + transferRequest.mBusiType = busiType + transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath) + transferRequest.mLocalPath = file.absolutePath + return transAndWait(runtime, transferRequest, wait) + } + + suspend fun transTroopResource( + groupId: String, + file: File, + fileType: Int, busiType: Int, + wait: Boolean = true, + builder: (TransferRequest) -> Unit + ): Boolean { + val runtime = QQInterfaces.app + val transferRequest = TransferRequest() + transferRequest.needSendMsg = false + transferRequest.mSelfUin = runtime.account + transferRequest.mPeerUin = groupId + transferRequest.mSecondId = runtime.currentAccountUin + transferRequest.mUinType = FileMsg.UIN_TROOP + transferRequest.mFileType = fileType + transferRequest.mUniseq = createMessageUniseq() + transferRequest.mIsUp = true + builder(transferRequest) + transferRequest.mBusiType = busiType + transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath) + transferRequest.mLocalPath = file.absolutePath + return transAndWait(runtime, transferRequest, wait) + } + + private suspend fun transAndWait( + runtime: AppRuntime, + transferRequest: TransferRequest, + wait: Boolean + ): Boolean { + return withTimeoutOrNull(60_000) { + val service = runtime.getRuntimeService(ITransFileController::class.java, "all") + if(service.transferAsync(transferRequest)) { + if (!wait) { // 如果无需等待直接返回 + return@withTimeoutOrNull true + } + suspendCancellableCoroutine { continuation -> + GlobalScope.launch { + lateinit var processor: IHttpCommunicatorListener + while ( + //service.findProcessor( + // transferRequest.keyForTransfer // uin + uniseq + //) != null + service.containsProcessor(runtime.currentAccountUin, transferRequest.mUniseq) + // 如果上传处理器依旧存在,说明没有上传成功 + && service.isWorking.get() + ) { + processor = service.findProcessor(runtime.currentAccountUin, transferRequest.mUniseq) + delay(100) + } + if (processor is BaseTransProcessor && processor.file != null) { + val fileMsg = processor.file + LogCenter.log("[OldBDH] 资源上传结束(fileId = ${fileMsg.fileID}, fileKey = ${fileMsg.fileKey}, path = ${fileMsg.filePath})") + } + continuation.resume(true) + } + // 实现取消上传器 + // 目前没什么用 + continuation.invokeOnCancellation { + continuation.resume(false) + } + } + } else true + } ?: false + } + + companion object { + const val SEND_MSG_BUSINESS_TYPE_AIO_ALBUM_PIC = 1031 + const val SEND_MSG_BUSINESS_TYPE_AIO_KEY_WORD_PIC = 1046 + const val SEND_MSG_BUSINESS_TYPE_AIO_QZONE_PIC = 1045 + const val SEND_MSG_BUSINESS_TYPE_ALBUM_PIC = 1007 + const val SEND_MSG_BUSINESS_TYPE_BLESS = 1056 + const val SEND_MSG_BUSINESS_TYPE_CAPTURE_PIC = 1008 + const val SEND_MSG_BUSINESS_TYPE_COMMEN_FALSH_PIC = 1040 + const val SEND_MSG_BUSINESS_TYPE_CUSTOM = 1006 + const val SEND_MSG_BUSINESS_TYPE_DOUTU_PIC = 1044 + const val SEND_MSG_BUSINESS_TYPE_FALSH_PIC = 1039 + const val SEND_MSG_BUSINESS_TYPE_FAST_IMAGE = 1037 + const val SEND_MSG_BUSINESS_TYPE_FORWARD_EDIT = 1048 + const val SEND_MSG_BUSINESS_TYPE_FORWARD_PIC = 1009 + const val SEND_MSG_BUSINESS_TYPE_FULL_SCREEN_ESSENCE = 1057 + const val SEND_MSG_BUSINESS_TYPE_GALEERY_PIC = 1041 + const val SEND_MSG_BUSINESS_TYPE_GAME_CENTER_STRATEGY = 1058 + const val SEND_MSG_BUSINESS_TYPE_HOT_PIC = 1042 + const val SEND_MSG_BUSINESS_TYPE_MIXED_PICS = 1043 + const val SEND_MSG_BUSINESS_TYPE_PIC_AIO_ALBUM = 1052 + const val SEND_MSG_BUSINESS_TYPE_PIC_CAMERA = 1050 + const val SEND_MSG_BUSINESS_TYPE_PIC_FAV = 1053 + const val SEND_MSG_BUSINESS_TYPE_PIC_SCREEN = 1027 + const val SEND_MSG_BUSINESS_TYPE_PIC_SHARE = 1030 + const val SEND_MSG_BUSINESS_TYPE_PIC_TAB_CAMERA = 1051 + const val SEND_MSG_BUSINESS_TYPE_QQPINYIN_SEND_PIC = 1038 + const val SEND_MSG_BUSINESS_TYPE_RECOMMENDED_STICKER = 1047 + const val SEND_MSG_BUSINESS_TYPE_RELATED_EMOTION = 1054 + const val SEND_MSG_BUSINESS_TYPE_SHOWLOVE = 1036 + const val SEND_MSG_BUSINESS_TYPE_SOGOU_SEND_PIC = 1034 + const val SEND_MSG_BUSINESS_TYPE_TROOP_BAR = 1035 + const val SEND_MSG_BUSINESS_TYPE_WLAN_RECV_NOTIFY = 1055 + const val SEND_MSG_BUSINESS_TYPE_ZHITU_PIC = 1049 + const val SEND_MSG_BUSINESS_TYPE_ZPLAN_EMOTICON_GIF = 1060 + const val SEND_MSG_BUSINESS_TYPE_ZPLAN_PIC = 1059 + + const val VIDEO_FORMAT_AFS = 7 + const val VIDEO_FORMAT_AVI = 1 + const val VIDEO_FORMAT_MKV = 4 + const val VIDEO_FORMAT_MOD = 9 + const val VIDEO_FORMAT_MOV = 8 + const val VIDEO_FORMAT_MP4 = 2 + const val VIDEO_FORMAT_MTS = 11 + const val VIDEO_FORMAT_RM = 6 + const val VIDEO_FORMAT_RMVB = 5 + const val VIDEO_FORMAT_TS = 10 + const val VIDEO_FORMAT_WMV = 3 + + const val BUSI_TYPE_GUILD_VIDEO = 4601 + const val BUSI_TYPE_MULTI_FORWARD_VIDEO = 1010 + const val BUSI_TYPE_PUBACCOUNT_PERM_VIDEO = 1009 + const val BUSI_TYPE_PUBACCOUNT_TEMP_VIDEO = 1007 + const val BUSI_TYPE_SHORT_VIDEO = 1 + const val BUSI_TYPE_SHORT_VIDEO_PTV = 2 + const val BUSI_TYPE_VIDEO = 0 + const val BUSI_TYPE_VIDEO_EMOTICON_PIC = 1022 + const val BUSI_TYPE_VIDEO_EMOTICON_VIDEO = 1021 + + const val TRANSFILE_TYPE_PIC = 1 + const val TRANSFILE_TYPE_PIC_EMO = 65538 + const val TRANSFILE_TYPE_PIC_THUMB = 65537 + const val TRANSFILE_TYPE_PISMA = 49 + const val TRANSFILE_TYPE_RAWPIC = 131075 + + const val TRANSFILE_TYPE_PROFILE_COVER = 35 + const val TRANSFILE_TYPE_PTT = 2 + const val TRANSFILE_TYPE_PTT_SLICE_TO_TEXT = 327696 + const val TRANSFILE_TYPE_QQHEAD_PIC = 131074 + + internal fun createMessageUniseq(time: Long = System.currentTimeMillis()): Long { + var uniseq = (time / 1000).toInt().toLong() + uniseq = uniseq shl 32 or kotlin.math.abs(Random.nextInt()).toLong() + return uniseq + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt new file mode 100644 index 0000000..233a55e --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt @@ -0,0 +1,495 @@ +package qq.service.bdh + +import android.graphics.BitmapFactory +import androidx.exifinterface.media.ExifInterface +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qqnt.aio.adapter.api.IAIOPttApi +import com.tencent.qqnt.kernel.nativeinterface.CommonFileInfo +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgElement +import com.tencent.qqnt.kernel.nativeinterface.PicElement +import com.tencent.qqnt.kernel.nativeinterface.PttElement +import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil +import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo +import com.tencent.qqnt.kernel.nativeinterface.VideoElement +import com.tencent.qqnt.msg.api.IMsgService +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.config.ResourceGroup +import moe.fuqiuluo.shamrock.config.ShamrockConfig +import moe.fuqiuluo.shamrock.tools.hex2ByteArray +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.shamrock.utils.AudioUtils +import moe.fuqiuluo.shamrock.utils.FileUtils +import moe.fuqiuluo.shamrock.utils.MediaType +import moe.fuqiuluo.symbols.decodeProtobuf +import protobuf.auto.toByteArray +import protobuf.oidb.TrpcOidb +import protobuf.oidb.cmd0x11c5.ClientMeta +import protobuf.oidb.cmd0x11c5.CodecConfigReq +import protobuf.oidb.cmd0x11c5.CommonHead +import protobuf.oidb.cmd0x11c5.DownloadExt +import protobuf.oidb.cmd0x11c5.DownloadReq +import protobuf.oidb.cmd0x11c5.FileInfo +import protobuf.oidb.cmd0x11c5.FileType +import protobuf.oidb.cmd0x11c5.IndexNode +import protobuf.oidb.cmd0x11c5.MultiMediaReqHead +import protobuf.oidb.cmd0x11c5.NtV2RichMediaReq +import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp +import protobuf.oidb.cmd0x11c5.SceneInfo +import protobuf.oidb.cmd0x11c5.UploadInfo +import protobuf.oidb.cmd0x11c5.UploadReq +import protobuf.oidb.cmd0x11c5.UploadRsp +import protobuf.oidb.cmd0x11c5.VideoDownloadExt +import protobuf.oidb.cmd0x388.Cmd0x388ReqBody +import protobuf.oidb.cmd0x388.Cmd0x388RspBody +import protobuf.oidb.cmd0x388.TryUpImgReq +import qq.service.QQInterfaces +import qq.service.internals.NTServiceFetcher +import qq.service.internals.msgService +import qq.service.kernel.SimpleKernelMsgListener +import qq.service.msg.MessageHelper +import java.io.File +import kotlin.coroutines.resume +import kotlin.math.roundToInt +import kotlin.random.Random +import kotlin.random.nextUInt +import kotlin.random.nextULong +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +internal object NtV2RichMediaSvc: QQInterfaces() { + private val requestIdSeq = atomic(1L) + + private fun fetchGroupResUploadTo(): String { + return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!! + } + + suspend fun tryUploadResourceByNt( + chatType: Int, + elementType: Int, + resources: ArrayList, + timeout: Duration, + retryCnt: Int = 5 + ): Result> { + return internalTryUploadResourceByNt(chatType, elementType, resources, timeout).onFailure { + if (retryCnt > 0) { + return tryUploadResourceByNt(chatType, elementType, resources, timeout, retryCnt - 1) + } + } + } + + /** + * 批量上传图片 + */ + private suspend fun internalTryUploadResourceByNt( + chatType: Int, + elementType: Int, + resources: ArrayList, + timeout: Duration + ): Result> { + require(resources.size in 1 .. 10) { "imageFiles.size() must be in 1 .. 10" } + + val messages = ArrayList(resources.map { file -> + val elem = MsgElement() + elem.elementType = elementType + when(elementType) { + MsgConstant.KELEMTYPEPIC -> { + val pic = PicElement() + pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) + val msgService = NTServiceFetcher.kernelService.msgService!! + val originalPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + 2, 0, pic.md5HexStr, file.name, 1, 0, null, "", true + ) + ) + if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( + originalPath + ) != file.length() + ) { + val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + 2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true + ) + ) + QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) + QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath) + } + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(file.absolutePath, options) + val exifInterface = ExifInterface(file.absolutePath) + val orientation = exifInterface.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED + ) + if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) { + pic.picWidth = options.outWidth + pic.picHeight = options.outHeight + } else { + pic.picWidth = options.outHeight + pic.picHeight = options.outWidth + } + pic.sourcePath = file.absolutePath + pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath) + pic.original = true + pic.picType = FileUtils.getPicType(file) + elem.picElement = pic + } + MsgConstant.KELEMTYPEPTT -> { + require(resources.size == 1) // 语音只能单个上传 + var pttFile = file + val ptt = PttElement() + when (AudioUtils.getMediaType(pttFile)) { + MediaType.Silk -> { + ptt.formatType = MsgConstant.KPTTFORMATTYPESILK + ptt.duration = QRoute.api(IAIOPttApi::class.java) + .getPttFileDuration(pttFile.absolutePath) + } + MediaType.Amr -> { + ptt.duration = AudioUtils.getDurationSec(pttFile) + ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR + } + MediaType.Pcm -> { + val result = AudioUtils.pcmToSilk(pttFile) + ptt.duration = (result.second * 0.001).roundToInt() + pttFile = result.first + ptt.formatType = MsgConstant.KPTTFORMATTYPESILK + } + + else -> { + val result = AudioUtils.audioToSilk(pttFile) + ptt.duration = runCatching { + QRoute.api(IAIOPttApi::class.java) + .getPttFileDuration(result.second.absolutePath) + }.getOrElse { + result.first + } + pttFile = result.second + ptt.formatType = MsgConstant.KPTTFORMATTYPESILK + } + } + ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(pttFile.absolutePath) + val msgService = NTServiceFetcher.kernelService.msgService!! + val originalPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + MsgConstant.KELEMTYPEPTT, 0, ptt.md5HexStr, file.name, 1, 0, null, "", true + ) + ) + if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != pttFile.length()) { + QQNTWrapperUtil.CppProxy.copyFile(pttFile.absolutePath, originalPath) + } + if (originalPath != null) { + ptt.filePath = originalPath + } else { + ptt.filePath = pttFile.absolutePath + } + ptt.canConvert2Text = true + ptt.fileId = 0 + ptt.fileUuid = "" + ptt.text = "" + ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD + ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE + elem.pttElement = ptt + } + MsgConstant.KELEMTYPEVIDEO -> { + require(resources.size == 1) // 视频只能单个上传 + val video = VideoElement() + video.videoMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) + val msgService = NTServiceFetcher.kernelService.msgService!! + val originalPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + 5, 2, video.videoMd5, file.name, 1, 0, null, "", true + ) + ) + val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + 5, 1, video.videoMd5, file.name, 2, 0, null, "", true + ) + ) + if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( + originalPath + ) != file.length() + ) { + QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) + AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!) + } + video.fileTime = AudioUtils.getVideoTime(file) + video.fileSize = file.length() + video.fileName = file.name + video.fileFormat = FileTransfer.VIDEO_FORMAT_MP4 + video.thumbSize = QQNTWrapperUtil.CppProxy.getFileSize(thumbPath).toInt() + val options = BitmapFactory.Options() + BitmapFactory.decodeFile(thumbPath, options) + video.thumbWidth = options.outWidth + video.thumbHeight = options.outHeight + video.thumbMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(thumbPath) + video.thumbPath = hashMapOf(0 to thumbPath) + elem.videoElement = video + } + else -> throw IllegalArgumentException("unsupported elementType: $elementType") + } + return@map elem + }) + if (messages.isEmpty()) { + return Result.failure(Exception("no valid image files")) + } + val contact = when(chatType) { + MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, app.currentAccountUin) + else -> Contact(chatType, fetchGroupResUploadTo(), null) + } + val result = mutableListOf() + val msgService = NTServiceFetcher.kernelService.msgService + ?: return Result.failure(Exception("kernelService.msgService is null")) + withTimeoutOrNull(timeout) { + val uniseq = MessageHelper.generateMsgId(chatType) + suspendCancellableCoroutine { + val listener = object: SimpleKernelMsgListener() { + override fun onRichMediaUploadComplete(fileTransNotifyInfo: FileTransNotifyInfo) { + if (fileTransNotifyInfo.msgId == uniseq) { + result.add(fileTransNotifyInfo.commonFileInfo) + } + if (result.size == resources.size) { + msgService.removeMsgListener(this) + it.resume(true) + } + } + } + msgService.addMsgListener(listener) + + QRoute.api(IMsgService::class.java).sendMsg(contact, uniseq, messages) { _, _ -> + if (contact.chatType == MsgConstant.KCHATTYPEGROUP && contact.peerUid == "100000000") { + val kernelService = NTServiceFetcher.kernelService + val sessionService = kernelService.wrapperSession + val service = sessionService.msgService + service.deleteMsg(contact, arrayListOf(uniseq), null) + } + } + + it.invokeOnCancellation { + msgService.removeMsgListener(listener) + } + } + } + + if (result.isEmpty()) { + return Result.failure(Exception("upload failed")) + } + + return Result.success(result) + } + + /** + * 获取NT图片的RKEY + */ + suspend fun getNtPicRKey( + fileId: String, + md5: String, + sha: String, + fileSize: ULong, + width: UInt, + height: UInt, + sceneBuilder: suspend SceneInfo.() -> Unit + ): Result { + runCatching { + val req = NtV2RichMediaReq( + head = MultiMediaReqHead( + commonHead = CommonHead( + requestId = requestIdSeq.incrementAndGet().toULong(), + cmd = 200u + ), + sceneInfo = SceneInfo( + requestType = 2u, + businessType = 1u, + ).apply { + sceneBuilder() + }, + clientMeta = ClientMeta(2u) + ), + download = DownloadReq( + IndexNode( + FileInfo( + fileSize = fileSize, + md5 = md5.lowercase(), + sha1 = sha.lowercase(), + name = "${md5}.jpg", + fileType = FileType( + fileType = 1u, + picFormat = 1000u, + videoFormat = 0u, + voiceFormat = 0u + ), + width = width, + height = height, + time = 0u, + original = 1u + ), + fileUuid = fileId, + storeId = 1u, + uploadTime = 0u, + ttl = 0u, + subType = 0u, + storeAppId = 0u + ), + DownloadExt( + video = VideoDownloadExt( + busiType = 0u, + subBusiType = 0u, + msgCodecConfig = CodecConfigReq( + platformChipinfo = "", + osVer = "", + deviceName = "" + ), + flag = 1u + ) + ) + ) + ).toByteArray() + val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 4549, 200, req, true) + if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { + return Result.failure(Exception("unable to get multimedia pic info: ${fromServiceMsg?.wupBuffer}")) + } + fromServiceMsg.wupBuffer.slice(4).decodeProtobuf().buffer.decodeProtobuf().download?.rkeyParam?.let { + return Result.success(it) + } + }.onFailure { + return Result.failure(it) + } + return Result.failure(Exception("unable to get c2c nt pic")) + } + + suspend fun requestUploadNtPic( + file: File, + md5: String, + sha: String, + name: String, + width: UInt, + height: UInt, + retryCnt: Int, + sceneBuilder: suspend SceneInfo.() -> Unit + ): Result { + return runCatching { + requestUploadNtPic(file, md5, sha, name, width, height, sceneBuilder).getOrThrow() + }.onFailure { + if (retryCnt > 0) { + return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, sceneBuilder) + } + } + } + + private suspend fun requestUploadNtPic( + file: File, + md5: String, + sha: String, + name: String, + width: UInt, + height: UInt, + sceneBuilder: suspend SceneInfo.() -> Unit + ): Result { + val req = NtV2RichMediaReq( + head = MultiMediaReqHead( + commonHead = CommonHead( + requestId = requestIdSeq.incrementAndGet().toULong(), + cmd = 100u + ), + sceneInfo = SceneInfo( + requestType = 2u, + businessType = 1u, + ).apply { + sceneBuilder() + }, + clientMeta = ClientMeta(2u) + ), + upload = UploadReq( + listOf(UploadInfo( + FileInfo( + fileSize = file.length().toULong(), + md5 = md5, + sha1 = sha, + name = name, + fileType = FileType( + fileType = 1u, + picFormat = 1000u, + videoFormat = 0u, + voiceFormat = 0u + ), + width = width, + height = height, + time = 0u, + original = 1u + ), + subFileType = 0u + )), + tryFastUploadCompleted = true, + srvSendMsg = false, + clientRandomId = Random.nextULong(), + compatQMsgSceneType = 1u, + clientSeq = Random.nextUInt(), + noNeedCompatMsg = false + ) + ).toByteArray() + val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3.seconds) + if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { + return Result.failure(Exception("unable to request upload nt pic")) + } + val rspBuffer = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf().buffer + val rsp = rspBuffer.decodeProtobuf() + if (rsp.upload == null) { + return Result.failure(Exception("unable to request upload nt pic: ${rsp.head}")) + } + return Result.success(rsp.upload!!) + } + + /** + * 使用OldBDH获取图片上传状态以及图片上传服务器 + */ + suspend fun requestUploadGroupPic( + groupId: ULong, + md5: String, + fileSize: ULong, + width: UInt, + height: UInt, + ): Result { + return runCatching { + val fromServiceMsg = sendBufferAW("ImgStore.GroupPicUp", true, Cmd0x388ReqBody( + netType = 3, + subCmd = 1, + msgTryUpImg = arrayListOf( + TryUpImgReq( + groupCode = groupId.toLong(), + srcUin = app.longAccountUin, + fileMd5 = md5.hex2ByteArray(), + fileSize = fileSize.toLong(), + fileName = "$md5.jpg", + srcTerm = 2, + platformType = 9, + buType = 212, + picWidth = width.toInt(), + picHeight = height.toInt(), + picType = 1000, + buildVer = "1.0.0", + originalPic = 1, + fileIndex = byteArrayOf(), + srvUpload = 0 + ) + ), + ).toByteArray()) + if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { + return Result.failure(Exception("unable to request upload group pic")) + } + val rsp = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf() + .msgTryUpImgRsp!!.first() + TryUpPicData( + uKey = rsp.ukey, + exist = rsp.fileExist, + fileId = rsp.fileId.toULong(), + upIp = rsp.upIp, + upPort = rsp.upPort + ) + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt b/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt new file mode 100644 index 0000000..f8ac3a2 --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt @@ -0,0 +1,429 @@ +@file:OptIn(ExperimentalSerializationApi::class) +package qq.service.bdh + +import com.tencent.mobileqq.pb.ByteStringMicro +import com.tencent.mobileqq.transfile.FileMsg +import com.tencent.mobileqq.transfile.api.IProtoReqManager +import com.tencent.mobileqq.transfile.protohandler.RichProto +import com.tencent.mobileqq.transfile.protohandler.RichProtoProc +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.serialization.ExperimentalSerializationApi +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.shamrock.tools.toHexString +import moe.fuqiuluo.shamrock.utils.PlatformUtils +import moe.fuqiuluo.symbols.decodeProtobuf +import mqq.app.MobileQQ +import protobuf.auto.toByteArray +import protobuf.oidb.cmd0x11c5.C2CUserInfo +import protobuf.oidb.cmd0x11c5.ChannelUserInfo +import protobuf.oidb.cmd0x11c5.GroupUserInfo +import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo +import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq +import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody +import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody +import qq.service.QQInterfaces +import qq.service.contact.ContactHelper +import tencent.im.cs.cmd0x346.cmd0x346 +import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 +import tencent.im.oidb.cmd0xe37.cmd0xe37 +import tencent.im.oidb.oidb_sso +import kotlin.coroutines.resume + +private const val GPRO_PIC = "gchat.qpic.cn" +private const val MULTIMEDIA_DOMAIN = "multimedia.nt.qq.com.cn" +private const val C2C_PIC = "c2cpicdw.qpic.cn" + +internal object RichProtoSvc: QQInterfaces() { + suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String { + val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, Oidb0xfc2ReqBody( + msgCmd = 1200, + msgBusType = 4202, + msgChannelInfo = Oidb0xfc2ChannelInfo( + guildId = peerId.toULong(), + channelId = channelId.toULong() + ), + msgTerminalType = 2, + msgApplyDownloadReq = Oidb0xfc2MsgApplyDownloadReq( + fieldId = fileId, + supportEncrypt = 0 + ) + ).toByteArray()) + if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { + return "" + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + body.bytes_bodybuffer + .get().toByteArray() + .decodeProtobuf() + .msgApplyDownloadRsp?.let { + it.msgDownloadInfo?.let { + return "https://${it.downloadDomain}${it.downloadUrl}&fname=$fileId&isthumb=0" + } + } + return "" + } + + suspend fun getGroupFileDownUrl( + peerId: Long, + fileId: String, + bizId: Int = 102 + ): String { + val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x6d6_2", 1750, 2, oidb_0x6d6.ReqBody().apply { + download_file_req.set(oidb_0x6d6.DownloadFileReqBody().apply { + uint64_group_code.set(peerId) + uint32_app_id.set(3) + uint32_bus_id.set(bizId) + str_file_id.set(fileId) + }) + }.toByteArray()) + if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { + return "" + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val result = oidb_0x6d6.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + if (body.uint32_result.get() != 0 + || result.download_file_rsp.int32_ret_code.get() != 0) { + return "" + } + + val domain = if (!result.download_file_rsp.str_download_dns.has()) + ("https://" + result.download_file_rsp.str_download_ip.get()) + else ("http://" + result.download_file_rsp.str_download_dns.get().toByteArray().decodeToString()) + val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString() + val appId = MobileQQ.getMobileQQ().appId + val version = PlatformUtils.getQQVersion(MobileQQ.getContext()) + + return "$domain/ftn_handler/$downloadUrl/?fname=$fileId&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk" + } + + suspend fun getC2CFileDownUrl( + fileId: String, + subId: String, + retryCnt: Int = 0 + ): String { + val fromServiceMsg = sendOidbAW("OidbSvc.0xe37_1200", 3639, 1200, cmd0xe37.Req0xe37().apply { + bytes_cmd_0x346_req_body.set(ByteStringMicro.copyFrom(cmd0x346.ReqBody().apply { + uint32_cmd.set(1200) + uint32_seq.set(1) + msg_apply_download_req.set(cmd0x346.ApplyDownloadReq().apply { + uint64_uin.set(app.longAccountUin) + bytes_uuid.set(ByteStringMicro.copyFrom(fileId.toByteArray())) + uint32_owner_type.set(2) + str_fileidcrc.set(subId) + + }) + uint32_business_id.set(3) + uint32_client_type.set(104) + uint32_flag_support_mediaplatform.set(1) + msg_extension_req.set(cmd0x346.ExtensionReq().apply { + uint32_download_url_type.set(1) + }) + }.toByteArray())) + }.toByteArray()) + if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { + if (retryCnt < 5) { + return getC2CFileDownUrl(fileId, subId, retryCnt + 1) + } + return "" + } else { + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val result = cmd0x346.RspBody().mergeFrom(cmd0xe37.Resp0xe37().mergeFrom( + body.bytes_bodybuffer.get().toByteArray() + ).bytes_cmd_0x346_rsp_body.get().toByteArray()) + if (body.uint32_result.get() != 0 || + result.msg_apply_download_rsp.int32_ret_code.has() && result.msg_apply_download_rsp.int32_ret_code.get() != 0) { + return "" + } + + val oldData = result.msg_apply_download_rsp.msg_download_info + //val newData = result[14, 40] NTQQ 文件信息 + + val domain = if (oldData.str_download_dns.has()) ("https://" + oldData.str_download_dns.get()) else ("http://" + oldData.rpt_str_downloadip_list.get().first()) + val params = oldData.str_download_url.get() + val appId = MobileQQ.getMobileQQ().appId + val version = PlatformUtils.getQQVersion(MobileQQ.getContext()) + + return "$domain$params&isthumb=0&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk" + } + } + + suspend fun getGroupPicDownUrl( + originalUrl: String, + md5: String, + peer: String = "", + fileId: String = "", + sha: String = "", + fileSize: ULong = 0uL, + width: UInt = 0u, + height: UInt = 0u + ): String { + val isNtServer = originalUrl.startsWith("/download") + val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC + if (originalUrl.isNotEmpty()) { + if (isNtServer && !originalUrl.contains("rkey=")) { + NtV2RichMediaSvc.getNtPicRKey( + fileId = fileId, + md5 = md5, + sha = sha, + fileSize = fileSize, + width = width, + height = height + ) { + sceneType = 2u + grp = GroupUserInfo(peer.toULong()) + }.onSuccess { + return "https://$domain$originalUrl$it" + }.onFailure { + LogCenter.log("getGroupPicDownUrl: ${it.stackTraceToString()}", Level.WARN) + } + } + return "https://$domain$originalUrl" + } + return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2" + } + + suspend fun getC2CPicDownUrl( + originalUrl: String, + md5: String, + peer: String = "", + fileId: String = "", + sha: String = "", + fileSize: ULong = 0uL, + width: UInt = 0u, + height: UInt = 0u, + storeId: Int = 0 + ): String { + val isNtServer = storeId == 1 || originalUrl.startsWith("/download") + val domain = if (isNtServer) MULTIMEDIA_DOMAIN else C2C_PIC + if (originalUrl.isNotEmpty()) { + if (fileId.isNotEmpty()) NtV2RichMediaSvc.getNtPicRKey( + fileId = fileId, + md5 = md5, + sha = sha, + fileSize = fileSize, + width = width, + height = height + ) { + sceneType = 1u + c2c = C2CUserInfo( + accountType = 2u, + uid = ContactHelper.getUidByUinAsync(peer.toLong()) + ) + }.onSuccess { + if (isNtServer && !originalUrl.contains("rkey=")) { + return "https://$domain$originalUrl$it" + } + }.onFailure { + LogCenter.log("getC2CPicDownUrl: ${it.stackTraceToString()}", Level.WARN) + } + if (isNtServer && !originalUrl.contains("rkey=")) { + return "https://$domain$originalUrl&rkey=" + } + return "https://$domain$originalUrl" + } + return "https://$domain/offpic_new/0/0-0-${md5}/0?term=2" + } + + suspend fun getGuildPicDownUrl( + originalUrl: String, + md5: String, + peer: String = "", + subPeer: String = "", + fileId: String = "", + sha: String = "", + fileSize: ULong = 0uL, + width: UInt = 0u, + height: UInt = 0u + ): String { + val isNtServer = originalUrl.startsWith("/download") + val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC + if (originalUrl.isNotEmpty()) { + if (isNtServer && !originalUrl.contains("rkey=")) { + NtV2RichMediaSvc.getNtPicRKey( + fileId = fileId, + md5 = md5, + sha = sha, + fileSize = fileSize, + width = width, + height = height + ) { + sceneType = 3u + channel = ChannelUserInfo(peer.toULong(), subPeer.toULong(), 1u) + }.onSuccess { + return "https://$domain$originalUrl$it" + }.onFailure { + LogCenter.log("getGuildPicDownUrl: ${it.stackTraceToString()}", Level.WARN) + } + return "https://$domain$originalUrl&rkey=" + } + return "https://$domain$originalUrl" + } + return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2" + } + + suspend fun getC2CVideoDownUrl( + peerId: String, + md5: ByteArray, + fileUUId: String + ): String { + return suspendCancellableCoroutine { + val richProtoReq = RichProto.RichProtoReq() + val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq() + downReq.selfUin = app.currentAccountUin + downReq.peerUin = peerId + downReq.secondUin = peerId + downReq.uinType = FileMsg.UIN_BUDDY + downReq.agentType = 0 + downReq.chatType = 1 + downReq.troopUin = peerId + downReq.clientType = 2 + downReq.fileId = fileUUId + downReq.md5 = md5 + downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO + downReq.subBusiType = 0 + downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4 + downReq.downType = 1 + downReq.sceneType = 1 + richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> + if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) { + LogCenter.log("requestDownPrivateVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN) + it.resume("") + } else { + val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp + val url = StringBuilder() + url.append(videoDownResp.mIpList.random().getServerUrl("http://")) + url.append(videoDownResp.mUrl) + it.resume(url.toString()) + } + } + richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW + richProtoReq.reqs.add(downReq) + richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all") + RichProtoProc.procRichProtoReq(richProtoReq) + } + } + + suspend fun getGroupVideoDownUrl( + peerId: String, + md5: ByteArray, + fileUUId: String + ): String { + return suspendCancellableCoroutine { + val richProtoReq = RichProto.RichProtoReq() + val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq() + downReq.selfUin = app.currentAccountUin + downReq.peerUin = peerId + downReq.secondUin = peerId + downReq.uinType = FileMsg.UIN_TROOP + downReq.agentType = 0 + downReq.chatType = 1 + downReq.troopUin = peerId + downReq.clientType = 2 + downReq.fileId = fileUUId + downReq.md5 = md5 + downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO + downReq.subBusiType = 0 + downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4 + downReq.downType = 1 + downReq.sceneType = 1 + richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> + if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) { + LogCenter.log("requestDownGroupVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN) + it.resume("") + } else { + val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp + val url = StringBuilder() + url.append(videoDownResp.mIpList.random().getServerUrl("http://")) + url.append(videoDownResp.mUrl) + it.resume(url.toString()) + } + } + richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW + richProtoReq.reqs.add(downReq) + richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all") + RichProtoProc.procRichProtoReq(richProtoReq) + } + } + + suspend fun getC2CPttDownUrl( + peerId: String, + fileUUId: String + ): String { + return suspendCancellableCoroutine { + val richProtoReq = RichProto.RichProtoReq() + val pttDownReq: RichProto.RichProtoReq.C2CPttDownReq = RichProto.RichProtoReq.C2CPttDownReq() + pttDownReq.selfUin = app.currentAccountUin + pttDownReq.peerUin = peerId + pttDownReq.secondUin = peerId + pttDownReq.uinType = FileMsg.UIN_BUDDY + pttDownReq.busiType = 1002 + pttDownReq.uuid = fileUUId + pttDownReq.storageSource = "pttcenter" + pttDownReq.isSelfSend = false + + pttDownReq.voiceType = 1 + pttDownReq.downType = 1 + richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> + if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) { + LogCenter.log("requestDownPrivateVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN) + it.resume("") + } else { + val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.C2CPttDownResp + val url = StringBuilder() + url.append(pttDownResp.downloadUrl) + url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${PlatformUtils.getQQVersion(MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk") + it.resume(url.toString()) + } + } + richProtoReq.protoKey = RichProtoProc.C2C_PTT_DW + richProtoReq.reqs.add(pttDownReq) + richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all") + RichProtoProc.procRichProtoReq(richProtoReq) + } + } + + suspend fun getGroupPttDownUrl( + peerId: String, + md5: ByteArray, + groupFileKey: String + ): String { + return suspendCancellableCoroutine { + val richProtoReq = RichProto.RichProtoReq() + val groupPttDownReq: RichProto.RichProtoReq.GroupPttDownReq = RichProto.RichProtoReq.GroupPttDownReq() + groupPttDownReq.selfUin = app.currentAccountUin + groupPttDownReq.peerUin = peerId + groupPttDownReq.secondUin = peerId + groupPttDownReq.uinType = FileMsg.UIN_TROOP + groupPttDownReq.groupFileID = 0 + groupPttDownReq.groupFileKey = groupFileKey + groupPttDownReq.md5 = md5 + groupPttDownReq.voiceType = 1 + groupPttDownReq.downType = 1 + richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> + if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) { + LogCenter.log("requestDownGroupVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN) + it.resume("") + } else { + val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.GroupPttDownResp + val url = StringBuilder() + url.append("http://") + url.append(pttDownResp.domainV4V6) + url.append(pttDownResp.urlPath) + url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${ + PlatformUtils.getQQVersion( + MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk") + it.resume(url.toString()) + } + } + richProtoReq.protoKey = RichProtoProc.GRP_PTT_DW + richProtoReq.reqs.add(groupPttDownReq) + richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all") + RichProtoProc.procRichProtoReq(richProtoReq) + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/bdh/TryUpPicData.kt b/xposed/src/main/java/qq/service/bdh/TryUpPicData.kt new file mode 100644 index 0000000..aa6a2c4 --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/TryUpPicData.kt @@ -0,0 +1,13 @@ +package qq.service.bdh + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class TryUpPicData( + @SerialName("ukey") val uKey: ByteArray, + @SerialName("exist") val exist: Boolean, + @SerialName("file_id") val fileId: ULong, + @SerialName("up_ip") var upIp: ArrayList? = null, + @SerialName("up_port") var upPort: ArrayList? = null, +) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/file/GroupFileHelper.kt b/xposed/src/main/java/qq/service/file/GroupFileHelper.kt index bf1131b..61076ce 100644 --- a/xposed/src/main/java/qq/service/file/GroupFileHelper.kt +++ b/xposed/src/main/java/qq/service/file/GroupFileHelper.kt @@ -33,7 +33,7 @@ internal object GroupFileHelper: QQInterfaces() { it.uint32_bus_id.set(0) }) }.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) - if (!fromServiceMsg.isSuccess) { + if (fromServiceMsg.wupBuffer == null) { throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) } val fileCnt: Int @@ -104,7 +104,7 @@ internal object GroupFileHelper: QQInterfaces() { uint32_show_onlinedoc_folder.set(0) }) }.toByteArray(), timeout = 15.seconds) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) - if (!fromServiceMsg.isSuccess) { + if (fromServiceMsg.wupBuffer == null) { throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) } val files = arrayListOf() diff --git a/xposed/src/main/java/qq/service/group/GroupHelper.kt b/xposed/src/main/java/qq/service/group/GroupHelper.kt index e6dc318..0189399 100644 --- a/xposed/src/main/java/qq/service/group/GroupHelper.kt +++ b/xposed/src/main/java/qq/service/group/GroupHelper.kt @@ -269,7 +269,7 @@ internal object GroupHelper: QQInterfaces() { uint32_shutup_timestap.set(0) }) }.toByteArray()) ?: return Result.failure(RuntimeException("[oidb] timeout")) - if (!fromServiceMsg.isSuccess) { + if (fromServiceMsg.wupBuffer == null) { return Result.failure(RuntimeException("[oidb] failed")) } val body = oidb_sso.OIDBSSOPkg() @@ -291,7 +291,7 @@ internal object GroupHelper: QQInterfaces() { uint64_uin.set(app.longAccountUin) uint64_group_code.set(groupId) }.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout")) - if (!fromServiceMsg.isSuccess) { + if (fromServiceMsg.wupBuffer == null) { return Result.failure(RuntimeException("[oidb] failed")) } val body = oidb_sso.OIDBSSOPkg() @@ -311,7 +311,7 @@ internal object GroupHelper: QQInterfaces() { toServiceMsg.extraData.putBoolean("is_admin", false) toServiceMsg.extraData.putInt("from", 0) val fromServiceMsg = sendToServiceMsgAW(toServiceMsg) ?: return@timeout Result.failure(Exception("获取群信息超时")) - if (!fromServiceMsg.isSuccess) { + if (fromServiceMsg.wupBuffer == null) { return@timeout Result.failure(Exception("获取群信息失败")) } val uniPacket = UniPacket(true) @@ -393,7 +393,7 @@ internal object GroupHelper: QQInterfaces() { req.uint32_client_type.set(1) req.uint32_rich_card_name_ver.set(1) val fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray()) - if (fromServiceMsg != null && fromServiceMsg.isSuccess) { + if (fromServiceMsg != null && fromServiceMsg.wupBuffer != null) { val rsp = group_member_info.RspBody() rsp.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) if (rsp.msg_meminfo.str_location.has()) { diff --git a/xposed/src/main/java/qq/service/internals/AioListener.kt b/xposed/src/main/java/qq/service/internals/AioListener.kt index 86e7a27..d8d9ade 100644 --- a/xposed/src/main/java/qq/service/internals/AioListener.kt +++ b/xposed/src/main/java/qq/service/internals/AioListener.kt @@ -1,49 +1,21 @@ +@file:OptIn(DelicateCoroutinesApi::class) + package qq.service.internals -import com.tencent.qqnt.kernel.nativeinterface.BroadcastHelperTransNotifyInfo -import com.tencent.qqnt.kernel.nativeinterface.Contact -import com.tencent.qqnt.kernel.nativeinterface.ContactMsgBoxInfo -import com.tencent.qqnt.kernel.nativeinterface.CustomWithdrawConfig -import com.tencent.qqnt.kernel.nativeinterface.DevInfo -import com.tencent.qqnt.kernel.nativeinterface.DownloadRelateEmojiResultInfo -import com.tencent.qqnt.kernel.nativeinterface.EmojiNotifyInfo -import com.tencent.qqnt.kernel.nativeinterface.EmojiResourceInfo -import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo -import com.tencent.qqnt.kernel.nativeinterface.FirstViewDirectMsgNotifyInfo -import com.tencent.qqnt.kernel.nativeinterface.FirstViewGroupGuildInfo -import com.tencent.qqnt.kernel.nativeinterface.FreqLimitInfo -import com.tencent.qqnt.kernel.nativeinterface.GroupFileListResult -import com.tencent.qqnt.kernel.nativeinterface.GroupGuildNotifyInfo -import com.tencent.qqnt.kernel.nativeinterface.GroupItem -import com.tencent.qqnt.kernel.nativeinterface.GuildInteractiveNotificationItem -import com.tencent.qqnt.kernel.nativeinterface.GuildMsgAbFlag -import com.tencent.qqnt.kernel.nativeinterface.GuildNotificationAbstractInfo -import com.tencent.qqnt.kernel.nativeinterface.HitRelatedEmojiWordsResult -import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener -import com.tencent.qqnt.kernel.nativeinterface.ImportOldDbMsgNotifyInfo -import com.tencent.qqnt.kernel.nativeinterface.InputStatusInfo -import com.tencent.qqnt.kernel.nativeinterface.KickedInfo -import com.tencent.qqnt.kernel.nativeinterface.MsgAbstract import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgRecord -import com.tencent.qqnt.kernel.nativeinterface.MsgSetting -import com.tencent.qqnt.kernel.nativeinterface.RecvdOrder -import com.tencent.qqnt.kernel.nativeinterface.RelatedWordEmojiInfo -import com.tencent.qqnt.kernel.nativeinterface.SearchGroupFileResult -import com.tencent.qqnt.kernel.nativeinterface.TabStatusInfo -import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo -import com.tencent.qqnt.kernel.nativeinterface.UnreadCntInfo +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter +import qq.service.kernel.SimpleKernelMsgListener import qq.service.msg.MessageHelper -object AioListener: IKernelMsgListener { - override fun onRecvMsg(msgs: ArrayList) { - msgs.forEach { +object AioListener: SimpleKernelMsgListener() { + override fun onRecvMsg(records: ArrayList) { + records.forEach { GlobalScope.launch { try { onMsg(it) @@ -103,275 +75,4 @@ object AioListener: IKernelMsgListener { else -> LogCenter.log("不支持PUSH事件: ${record.chatType}") } } - - override fun onMsgRecall(chatType: Int, peerId: String, msgId: Long) { - LogCenter.log("onMsgRecall($chatType, $peerId, $msgId)") - } - - override fun onAddSendMsg(record: MsgRecord) { - - } - - override fun onMsgInfoListUpdate(msgList: ArrayList?) { - - } - - override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo) { - - } - - override fun onMsgAbstractUpdate(arrayList: ArrayList?) { - //arrayList?.forEach { - // LogCenter.log("onMsgAbstractUpdate($it)", Level.WARN) - //} - } - - override fun onRecvMsgSvrRspTransInfo( - j2: Long, - contact: Contact?, - i2: Int, - i3: Int, - str: String?, - bArr: ByteArray? - ) { - LogCenter.log("onRecvMsgSvrRspTransInfo($j2, $contact, $i2, $i3, $str)", Level.DEBUG) - } - - override fun onRecvS2CMsg(arrayList: ArrayList?) { - LogCenter.log("onRecvS2CMsg(${arrayList.toString()})", Level.DEBUG) - } - - override fun onRecvSysMsg(arrayList: ArrayList?) { - LogCenter.log("onRecvSysMsg(${arrayList.toString()})", Level.DEBUG) - } - - override fun onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {} - - override fun onBroadcastHelperProgerssUpdate(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {} - - override fun onChannelFreqLimitInfoUpdate( - contact: Contact?, - z: Boolean, - freqLimitInfo: FreqLimitInfo? - ) { - - } - - override fun onContactUnreadCntUpdate(unreadMap: HashMap>) { - // 推送未读消息数量 - } - - override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) { - LogCenter.log("onCustomWithdrawConfigUpdate: " + customWithdrawConfig.toString(), Level.DEBUG) - } - - override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList?, j2: Long) { - LogCenter.log("onDraftUpdate: " + contact.toString() + "|" + arrayList + "|" + j2.toString(), Level.DEBUG) - } - - override fun onEmojiDownloadComplete(emojiNotifyInfo: EmojiNotifyInfo?) { - - } - - override fun onEmojiResourceUpdate(emojiResourceInfo: EmojiResourceInfo?) { - - } - - override fun onFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { - - } - - override fun onFileMsgCome(arrayList: ArrayList?) { - - } - - override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { - - } - - override fun onFirstViewGroupGuildMapping(arrayList: ArrayList?) { - - } - - override fun onGrabPasswordRedBag( - i2: Int, - str: String?, - i3: Int, - recvdOrder: RecvdOrder?, - msgRecord: MsgRecord? - ) { - - } - - override fun onKickedOffLine(kickedInfo: KickedInfo?) { - LogCenter.log("onKickedOffLine($kickedInfo)") - } - - override fun onRichMediaUploadComplete(notifyInfo: FileTransNotifyInfo) { - LogCenter.log({ "[BDH] 资源上传完成(${notifyInfo.trasferStatus}, ${notifyInfo.fileId}, ${notifyInfo.msgId}, ${notifyInfo.commonFileInfo})" }, Level.DEBUG) - } - - override fun onRecvOnlineFileMsg(arrayList: ArrayList?) { - LogCenter.log(("onRecvOnlineFileMsg" + arrayList?.joinToString { ", " }), Level.DEBUG) - } - - override fun onRichMediaDownloadComplete(fileTransNotifyInfo: FileTransNotifyInfo) { - - } - - override fun onRichMediaProgerssUpdate(fileTransNotifyInfo: FileTransNotifyInfo) { - - } - - override fun onSearchGroupFileInfoUpdate(searchGroupFileResult: SearchGroupFileResult?) { - LogCenter.log("onSearchGroupFileInfoUpdate($searchGroupFileResult)", Level.DEBUG) - } - - override fun onGroupFileInfoAdd(groupItem: GroupItem?) { - LogCenter.log("onGroupFileInfoAdd: " + groupItem.toString(), Level.DEBUG) - } - - override fun onGroupFileInfoUpdate(groupFileListResult: GroupFileListResult?) { - LogCenter.log("onGroupFileInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG) - } - - override fun onGroupGuildUpdate(groupGuildNotifyInfo: GroupGuildNotifyInfo?) { - LogCenter.log("onGroupGuildUpdate: " + groupGuildNotifyInfo.toString(), Level.DEBUG) - } - - override fun onGroupTransferInfoAdd(groupItem: GroupItem?) { - LogCenter.log("onGroupTransferInfoAdd: " + groupItem.toString(), Level.DEBUG) - } - - override fun onGroupTransferInfoUpdate(groupFileListResult: GroupFileListResult?) { - LogCenter.log("onGroupTransferInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG) - } - - override fun onGuildInteractiveUpdate(guildInteractiveNotificationItem: GuildInteractiveNotificationItem?) { - - } - - override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) { - - } - - override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) { - - } - - override fun onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: DownloadRelateEmojiResultInfo?) { - - } - - override fun onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: HitRelatedEmojiWordsResult?) { - - } - - override fun onHitRelatedEmojiResult(relatedWordEmojiInfo: RelatedWordEmojiInfo?) { - - } - - override fun onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: ImportOldDbMsgNotifyInfo?) { - - } - - override fun onInputStatusPush(inputStatusInfo: InputStatusInfo?) { - - } - - override fun onLineDev(devList: ArrayList?) { - //LogCenter.log("onLineDev($arrayList)") - } - - override fun onLogLevelChanged(newLevel: Long) { - - } - - override fun onMsgBoxChanged(arrayList: ArrayList?) { - - } - - override fun onMsgDelete(contact: Contact?, arrayList: ArrayList?) { - - } - - override fun onMsgEventListUpdate(hashMap: HashMap>?) { - - } - - override fun onMsgInfoListAdd(arrayList: ArrayList?) { - - } - - override fun onMsgQRCodeStatusChanged(i2: Int) { - - } - - override fun onMsgSecurityNotify(msgRecord: MsgRecord?) { - LogCenter.log("onMsgSecurityNotify($msgRecord)") - } - - override fun onMsgSettingUpdate(msgSetting: MsgSetting?) { - - } - - override fun onNtFirstViewMsgSyncEnd() { - - } - - override fun onNtMsgSyncEnd() { - LogCenter.log("NTKernel同步消息完成", Level.DEBUG) - } - - override fun onNtMsgSyncStart() { - LogCenter.log("NTKernel同步消息开始", Level.DEBUG) - } - - override fun onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { - - } - - override fun onRecvGroupGuildFlag(i2: Int) { - - } - - override fun onRecvUDCFlag(i2: Int) { - LogCenter.log("onRecvUDCFlag($i2)", Level.DEBUG) - } - - override fun onSendMsgError(j2: Long, contact: Contact?, i2: Int, str: String?) { - LogCenter.log("onSendMsgError($j2, $contact, $j2, $str)", Level.DEBUG) - } - - override fun onSysMsgNotification(i2: Int, j2: Long, j3: Long, arrayList: ArrayList?) { - LogCenter.log("onSysMsgNotification($i2, $j2, $j3, $arrayList)", Level.DEBUG) - } - - override fun onUnreadCntAfterFirstView(hashMap: HashMap>?) { - - } - - override fun onUnreadCntUpdate(hashMap: HashMap>?) { - - } - - override fun onUserChannelTabStatusChanged(z: Boolean) { - - } - - override fun onUserOnlineStatusChanged(z: Boolean) { - - } - - override fun onUserTabStatusChanged(arrayList: ArrayList?) { - LogCenter.log("onUserTabStatusChanged($arrayList)", Level.DEBUG) - } - - override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) { - - } - - override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) { - - } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt b/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt new file mode 100644 index 0000000..47c3c56 --- /dev/null +++ b/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt @@ -0,0 +1,316 @@ +package qq.service.kernel + +import com.tencent.qqnt.kernel.nativeinterface.BroadcastHelperTransNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.ContactMsgBoxInfo +import com.tencent.qqnt.kernel.nativeinterface.CustomWithdrawConfig +import com.tencent.qqnt.kernel.nativeinterface.DevInfo +import com.tencent.qqnt.kernel.nativeinterface.DownloadRelateEmojiResultInfo +import com.tencent.qqnt.kernel.nativeinterface.EmojiNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.EmojiResourceInfo +import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.FirstViewDirectMsgNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.FirstViewGroupGuildInfo +import com.tencent.qqnt.kernel.nativeinterface.FreqLimitInfo +import com.tencent.qqnt.kernel.nativeinterface.GroupFileListResult +import com.tencent.qqnt.kernel.nativeinterface.GroupGuildNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.GroupItem +import com.tencent.qqnt.kernel.nativeinterface.GuildInteractiveNotificationItem +import com.tencent.qqnt.kernel.nativeinterface.GuildMsgAbFlag +import com.tencent.qqnt.kernel.nativeinterface.GuildNotificationAbstractInfo +import com.tencent.qqnt.kernel.nativeinterface.HitRelatedEmojiWordsResult +import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener +import com.tencent.qqnt.kernel.nativeinterface.ImportOldDbMsgNotifyInfo +import com.tencent.qqnt.kernel.nativeinterface.InputStatusInfo +import com.tencent.qqnt.kernel.nativeinterface.KickedInfo +import com.tencent.qqnt.kernel.nativeinterface.MsgAbstract +import com.tencent.qqnt.kernel.nativeinterface.MsgElement +import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.kernel.nativeinterface.MsgSetting +import com.tencent.qqnt.kernel.nativeinterface.RecvdOrder +import com.tencent.qqnt.kernel.nativeinterface.RelatedWordEmojiInfo +import com.tencent.qqnt.kernel.nativeinterface.SearchGroupFileResult +import com.tencent.qqnt.kernel.nativeinterface.TabStatusInfo +import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo +import com.tencent.qqnt.kernel.nativeinterface.UnreadCntInfo +import java.util.ArrayList +import java.util.HashMap + +abstract class SimpleKernelMsgListener: IKernelMsgListener { + override fun onAddSendMsg(msgRecord: MsgRecord?) { + + } + + override fun onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) { + + } + + override fun onBroadcastHelperProgerssUpdate(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) { + + } + + override fun onChannelFreqLimitInfoUpdate( + contact: Contact?, + z: Boolean, + freqLimitInfo: FreqLimitInfo? + ) { + + } + + override fun onContactUnreadCntUpdate(hashMap: HashMap>?) { + + } + + override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) { + + } + + override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList?, j2: Long) { + + } + + override fun onEmojiDownloadComplete(emojiNotifyInfo: EmojiNotifyInfo?) { + + } + + override fun onEmojiResourceUpdate(emojiResourceInfo: EmojiResourceInfo?) { + + } + + override fun onFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { + + } + + override fun onFileMsgCome(arrayList: ArrayList?) { + + } + + override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { + + } + + override fun onFirstViewGroupGuildMapping(arrayList: ArrayList?) { + + } + + override fun onGrabPasswordRedBag( + i2: Int, + str: String?, + i3: Int, + recvdOrder: RecvdOrder?, + msgRecord: MsgRecord? + ) { + + } + + override fun onGroupFileInfoAdd(groupItem: GroupItem?) { + + } + + override fun onGroupFileInfoUpdate(groupFileListResult: GroupFileListResult?) { + + } + + override fun onGroupGuildUpdate(groupGuildNotifyInfo: GroupGuildNotifyInfo?) { + + } + + override fun onGroupTransferInfoAdd(groupItem: GroupItem?) { + + } + + override fun onGroupTransferInfoUpdate(groupFileListResult: GroupFileListResult?) { + + } + + override fun onGuildInteractiveUpdate(guildInteractiveNotificationItem: GuildInteractiveNotificationItem?) { + + } + + override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) { + + } + + override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) { + + } + + override fun onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: DownloadRelateEmojiResultInfo?) { + + } + + override fun onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: HitRelatedEmojiWordsResult?) { + + } + + override fun onHitRelatedEmojiResult(relatedWordEmojiInfo: RelatedWordEmojiInfo?) { + + } + + override fun onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: ImportOldDbMsgNotifyInfo?) { + + } + + override fun onInputStatusPush(inputStatusInfo: InputStatusInfo?) { + + } + + override fun onKickedOffLine(kickedInfo: KickedInfo?) { + + } + + override fun onLineDev(arrayList: ArrayList?) { + + } + + override fun onLogLevelChanged(j2: Long) { + + } + + override fun onMsgAbstractUpdate(arrayList: ArrayList?) { + + } + + override fun onMsgBoxChanged(arrayList: ArrayList?) { + + } + + override fun onMsgDelete(contact: Contact?, arrayList: ArrayList?) { + + } + + override fun onMsgEventListUpdate(hashMap: HashMap>?) { + + } + + override fun onMsgInfoListAdd(arrayList: ArrayList?) { + + } + + override fun onMsgInfoListUpdate(arrayList: ArrayList?) { + + } + + override fun onMsgQRCodeStatusChanged(i2: Int) { + + } + + override fun onMsgRecall(i2: Int, str: String?, j2: Long) { + + } + + override fun onMsgSecurityNotify(msgRecord: MsgRecord?) { + + } + + override fun onMsgSettingUpdate(msgSetting: MsgSetting?) { + + } + + override fun onNtFirstViewMsgSyncEnd() { + + } + + override fun onNtMsgSyncEnd() { + + } + + override fun onNtMsgSyncStart() { + + } + + override fun onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { + + } + + override fun onRecvGroupGuildFlag(i2: Int) { + + } + + override fun onRecvMsg(records: ArrayList) { + + } + + override fun onRecvMsgSvrRspTransInfo( + j2: Long, + contact: Contact?, + i2: Int, + i3: Int, + str: String?, + bArr: ByteArray? + ) { + + } + + override fun onRecvOnlineFileMsg(arrayList: ArrayList?) { + + } + + override fun onRecvS2CMsg(arrayList: ArrayList?) { + + } + + override fun onRecvSysMsg(arrayList: ArrayList?) { + + } + + override fun onRecvUDCFlag(i2: Int) { + + } + + override fun onRichMediaDownloadComplete(fileTransNotifyInfo: FileTransNotifyInfo?) { + + } + + override fun onRichMediaProgerssUpdate(fileTransNotifyInfo: FileTransNotifyInfo?) { + + } + + override fun onRichMediaUploadComplete(fileTransNotifyInfo: FileTransNotifyInfo) { + + } + + override fun onSearchGroupFileInfoUpdate(searchGroupFileResult: SearchGroupFileResult?) { + + } + + override fun onSendMsgError(j2: Long, contact: Contact?, i2: Int, str: String?) { + + } + + override fun onSysMsgNotification(i2: Int, j2: Long, j3: Long, arrayList: ArrayList?) { + + } + + override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo?) { + + } + + override fun onUnreadCntAfterFirstView(hashMap: HashMap>?) { + + } + + override fun onUnreadCntUpdate(hashMap: HashMap>?) { + + } + + override fun onUserChannelTabStatusChanged(z: Boolean) { + + } + + override fun onUserOnlineStatusChanged(z: Boolean) { + + } + + override fun onUserTabStatusChanged(arrayList: ArrayList?) { + + } + + override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) { + + } + + override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) { + + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MessageHelper.kt b/xposed/src/main/java/qq/service/msg/MessageHelper.kt index 29c5c28..8249ac9 100644 --- a/xposed/src/main/java/qq/service/msg/MessageHelper.kt +++ b/xposed/src/main/java/qq/service/msg/MessageHelper.kt @@ -1,12 +1,15 @@ package qq.service.msg import com.tencent.qqnt.kernel.api.IKernelService +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import qq.service.QQInterfaces +import qq.service.contact.ContactHelper import qq.service.internals.msgService import kotlin.coroutines.resume @@ -28,4 +31,25 @@ internal object MessageHelper: QQInterfaces() { } ?: return Result.failure(Exception("获取临时会话信息失败")) return Result.success(info) } + + suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact { + val peerId = when (chatType) { + MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> { + ContactHelper.getUidByUinAsync(id.toLong()) + } + + else -> id + } + return if (chatType == MsgConstant.KCHATTYPEGUILD) { + Contact(chatType, subId, peerId) + } else { + Contact(chatType, peerId, subId) + } + } + + fun generateMsgId(chatType: Int): Long { + return createMessageUniseq(chatType, System.currentTimeMillis()) + } + + external fun createMessageUniseq(chatType: Int, time: Long): Long } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MsgConvertor.kt b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt new file mode 100644 index 0000000..28016f9 --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt @@ -0,0 +1,405 @@ +package qq.service.msg + +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgElement +import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import io.kritor.event.AtElement +import io.kritor.event.Element +import io.kritor.event.ElementKt +import io.kritor.event.ImageType +import io.kritor.event.Scene +import io.kritor.event.atElement +import io.kritor.event.basketballElement +import io.kritor.event.buttonAction +import io.kritor.event.buttonActionPermission +import io.kritor.event.buttonRender +import io.kritor.event.contactElement +import io.kritor.event.diceElement +import io.kritor.event.faceElement +import io.kritor.event.forwardElement +import io.kritor.event.imageElement +import io.kritor.event.jsonElement +import io.kritor.event.locationElement +import io.kritor.event.pokeElement +import io.kritor.event.rpsElement +import io.kritor.event.textElement +import io.kritor.event.videoElement +import io.kritor.event.voiceElement +import moe.fuqiuluo.shamrock.helper.ActionMsgException +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.helper.db.ImageDB +import moe.fuqiuluo.shamrock.helper.db.ImageMapping +import moe.fuqiuluo.shamrock.tools.asJsonArray +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.tools.hex2ByteArray +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import moe.fuqiuluo.shamrock.tools.toHexString +import moe.fuqiuluo.shamrock.utils.PlatformUtils +import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER +import qq.service.bdh.RichProtoSvc +import qq.service.contact.ContactHelper + +typealias NtMessages = ArrayList +typealias Convertor = suspend (MsgRecord, MsgElement) -> Result + +suspend fun NtMessages.toKritorMessages(record: MsgRecord): ArrayList { + val result = arrayListOf() + forEach { + MsgConvertor[it.elementType]?.invoke(record, it)?.onSuccess { + result.add(it) + }?.onFailure { + if (it !is ActionMsgException) { + LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN) + } + } + } + return result +} + +private object MsgConvertor { + private val convertorMap = hashMapOf( + MsgConstant.KELEMTYPETEXT to ::convertText, + MsgConstant.KELEMTYPEFACE to ::convertFace, + MsgConstant.KELEMTYPEPIC to ::convertImage, + MsgConstant.KELEMTYPEPTT to ::convertVoice, + MsgConstant.KELEMTYPEVIDEO to ::convertVideo, + MsgConstant.KELEMTYPEMARKETFACE to ::convertMarketFace, + MsgConstant.KELEMTYPEARKSTRUCT to ::convertStructJson, + MsgConstant.KELEMTYPEREPLY to ::convertReply, + //MsgConstant.KELEMTYPEGRAYTIP to ::convertGrayTips, + MsgConstant.KELEMTYPEFILE to ::convertFile, + MsgConstant.KELEMTYPEMARKDOWN to ::convertMarkdown, + //MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem, + //MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem, + MsgConstant.KELEMTYPEFACEBUBBLE to ::convertBubbleFace, + MsgConstant.KELEMTYPEINLINEKEYBOARD to ::convertInlineKeyboard + ) + + suspend fun convertText(record: MsgRecord, element: MsgElement): Result { + val text = element.textElement + val elem = Element.newBuilder() + if (text.atType != MsgConstant.ATTYPEUNKNOWN) { + elem.setAt(atElement { + this.uid = text.atNtUid + this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong() + }) + } else { + elem.setText(textElement { + this.text = text.content + }) + } + return Result.success(elem.build()) + } + + suspend fun convertFace(record: MsgRecord, element: MsgElement): Result { + val face = element.faceElement + val elem = Element.newBuilder() + if (face.faceType == 5) { + elem.setPoke(pokeElement { + this.id = face.vaspokeId + this.type = face.pokeType + this.strength = face.pokeStrength + }) + } else { + when(face.faceIndex) { + 114 -> elem.setBasketball(basketballElement { + this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0 + }) + 358 -> elem.setDice(diceElement { + this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0 + }) + 359 -> elem.setRps(rpsElement { + this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0 + }) + 394 -> elem.setFace(faceElement { + this.id = face.faceIndex + this.isBig = face.faceType == 3 + this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1 + }) + else -> elem.setFace(faceElement { + this.id = face.faceIndex + this.isBig = face.faceType == 3 + }) + } + } + return Result.success(elem.build()) + } + + suspend fun convertImage(record: MsgRecord, element: MsgElement): Result { + val image = element.picElement + val md5 = (image.md5HexStr ?: image.fileName + .replace("{", "") + .replace("}", "") + .replace("-", "").split(".")[0]) + .uppercase() + + var storeId = 0 + if (PlatformUtils.getQQVersionCode() > QQ_9_0_8_VER) { + storeId = image.storeID + } + + ImageDB.getInstance().imageMappingDao().insert( + ImageMapping( + fileName = md5, + md5 = md5, + chatType = record.chatType, + size = image.fileSize, + sha = "", + fileId = image.fileUuid, + storeId = storeId, + ) + ) + + val originalUrl = image.originImageUrl ?: "" + LogCenter.log({ "receive image: $image" }, Level.DEBUG) + + val elem = Element.newBuilder() + elem.setImage(imageElement { + this.file = md5 + this.url = when (record.chatType) { + MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl( + originalUrl = originalUrl, + md5 = md5, + fileId = image.fileUuid, + width = image.picWidth.toUInt(), + height = image.picHeight.toUInt(), + sha = "", + fileSize = image.fileSize.toULong(), + peer = record.peerUin.toString() + ) + + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl( + originalUrl = originalUrl, + md5 = md5, + fileId = image.fileUuid, + width = image.picWidth.toUInt(), + height = image.picHeight.toUInt(), + sha = "", + fileSize = image.fileSize.toULong(), + peer = record.senderUin.toString(), + storeId = storeId + ) + + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl( + originalUrl = originalUrl, + md5 = md5, + fileId = image.fileUuid, + width = image.picWidth.toUInt(), + height = image.picHeight.toUInt(), + sha = "", + fileSize = image.fileSize.toULong(), + peer = record.channelId.ifNullOrEmpty { record.peerUin.toString() } ?: "0", + subPeer = record.guildId ?: "0" + ) + + else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}") + } + this.type = if (image.isFlashPic == true) ImageType.FLASH else if (image.original) ImageType.ORIGIN else ImageType.COMMON + this.subType = image.picSubType + }) + + return Result.success(elem.build()) + } + + suspend fun convertVoice(record: MsgRecord, element: MsgElement): Result { + val ptt = element.pttElement + val elem = Element.newBuilder() + + val md5 = if (ptt.fileName.startsWith("silk")) + ptt.fileName.substring(5) + else ptt.md5HexStr + + elem.setVoice(voiceElement { + this.url = when (record.chatType) { + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt.fileUuid) + MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl("0", md5.hex2ByteArray(), ptt.fileUuid) + + else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}") + } + this.file = md5 + this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE + }) + + return Result.success(elem.build()) + } + + suspend fun convertVideo(record: MsgRecord, element: MsgElement): Result { + val video = element.videoElement + val elem = Element.newBuilder() + val md5 = if (video.fileName.contains("/")) { + video.videoMd5.takeIf { + !it.isNullOrEmpty() + }?.hex2ByteArray() ?: video.fileName.split("/").let { + it[it.size - 2].hex2ByteArray() + } + } else video.fileName.split(".")[0].hex2ByteArray() + elem.setVideo(videoElement { + this.file = md5.toHexString() + this.url = when (record.chatType) { + MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) + else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}") + } + }) + return Result.success(elem.build()) + } + + suspend fun convertMarketFace(record: MsgRecord, element: MsgElement): Result { + val marketFace = element.marketFaceElement + val elem = Element.newBuilder() + elem.setMarketFace(io.kritor.event.marketFaceElement { + this.id = marketFace.emojiId.lowercase() + }) + return Result.success(elem.build()) + } + + suspend fun convertStructJson(record: MsgRecord, element: MsgElement): Result { + val data = element.arkElement.bytesData.asJsonObject + val elem = Element.newBuilder() + when (data["app"].asString) { + "com.tencent.multimsg" -> { + val info = data["meta"].asJsonObject["detail"].asJsonObject + elem.setForward(forwardElement { + this.id = info["resid"].asString + this.uniseq = info["uniseq"].asString + this.summary = info["summary"].asString + this.description = info["news"].asJsonArray.joinToString("\n") { + it.asJsonObject["text"].asString + } + }) + } + + "com.tencent.troopsharecard" -> { + val info = data["meta"].asJsonObject["contact"].asJsonObject + elem.setContact(contactElement { + this.scene = Scene.GROUP + this.peer = info["jumpUrl"].asString.split("group_code=")[1] + }) + } + + "com.tencent.contact.lua" -> { + val info = data["meta"].asJsonObject["contact"].asJsonObject + elem.setContact(contactElement { + this.scene = Scene.FRIEND + this.peer = info["jumpUrl"].asString.split("uin=")[1] + }) + } + + "com.tencent.map" -> { + val info = data["meta"].asJsonObject["Location.Search"].asJsonObject + elem.setLocation(locationElement { + this.lat = info["lat"].asString.toFloat() + this.lon = info["lng"].asString.toFloat() + this.address = info["address"].asString + this.title = info["name"].asString + }) + } + + else -> elem.setJson(jsonElement { + this.json = data.toString() + }) + } + return Result.success(elem.build()) + } + + suspend fun convertReply(record: MsgRecord, element: MsgElement): Result { + val reply = element.replyElement + val elem = Element.newBuilder() + elem.setReply(io.kritor.event.replyElement { + this.messageId = reply.replayMsgId + }) + return Result.success(elem.build()) + } + + suspend fun convertFile(record: MsgRecord, element: MsgElement): Result { + val fileMsg = element.fileElement + val fileName = fileMsg.fileName + val fileSize = fileMsg.fileSize + val expireTime = fileMsg.expireTime ?: 0 + val fileId = fileMsg.fileUuid + val bizId = fileMsg.fileBizId ?: 0 + val fileSubId = fileMsg.fileSubId ?: "" + val url = when (record.chatType) { + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(record.guildId, record.channelId, fileId, bizId) + else -> RichProtoSvc.getGroupFileDownUrl(record.peerUin, fileId, bizId) + } + val elem = Element.newBuilder() + elem.setFile(io.kritor.event.fileElement { + this.name = fileName + this.size = fileSize + this.url = url + this.expireTime = expireTime + this.id = fileId + this.subId = fileSubId + this.biz = bizId + }) + return Result.success(elem.build()) + } + + suspend fun convertMarkdown(record: MsgRecord, element: MsgElement): Result { + val markdown = element.markdownElement + val elem = Element.newBuilder() + elem.setMarkdown(io.kritor.event.markdownElement { + this.markdown = markdown.content + }) + return Result.success(elem.build()) + } + + suspend fun convertBubbleFace(record: MsgRecord, element: MsgElement): Result { + val bubbleFace = element.faceBubbleElement + val elem = Element.newBuilder() + elem.setBubbleFace(io.kritor.event.bubbleFaceElement { + this.id = bubbleFace.yellowFaceInfo.index + this.count = bubbleFace.faceCount ?: 1 + }) + return Result.success(elem.build()) + } + + suspend fun convertInlineKeyboard(record: MsgRecord, element: MsgElement): Result { + val inlineKeyboard = element.inlineKeyboardElement + val elem = Element.newBuilder() + elem.setButton(io.kritor.event.buttonElement { + inlineKeyboard.rows.forEach { row -> + this.rows.add(io.kritor.event.row { + row.buttons.forEach buttonsLoop@ { button -> + if (button == null) return@buttonsLoop + this.buttons.add(io.kritor.event.button { + this.id = button.id + this.action = buttonAction { + this.type = button.type + this.permission = buttonActionPermission { + this.type = button.permissionType + button.specifyRoleIds?.let { + this.roleIds.addAll(it) + } + button.specifyTinyids?.let { + this.userIds.addAll(it) + } + } + this.unsupportedTips = button.unsupportTips ?: "" + this.data = button.data ?: "" + this.reply = button.isReply + this.enter = button.enter + } + this.renderData = buttonRender { + this.label = button.label ?: "" + this.visitedLabel = button.visitedLabel ?: "" + this.style = button.style + } + }) + } + }) + } + }) + return Result.success(elem.build()) + } + + operator fun get(case: Int): Convertor? { + return convertorMap[case] + } +} + From 3a0711609359f415a179024f32d8e243ae35e298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Fri, 15 Mar 2024 01:37:28 +0800 Subject: [PATCH 17/20] =?UTF-8?q?`Shamrock`:=20=E5=AE=9E=E7=8E=B0=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .../main/java/kritor/service/EventService.kt | 27 +- .../internals/GlobalEventTransmitter.kt | 473 +++++++------ .../java/qq/service/friend/FriendHelper.kt | 67 ++ .../main/java/qq/service/group/GroupHelper.kt | 101 +++ .../java/qq/service/internals/AioListener.kt | 62 ++ .../java/qq/service/internals/MSFHandler.kt | 8 +- .../qq/service/internals/PrimitiveListener.kt | 668 ++++++++++++++++++ .../service/kernel/SimpleKernelMsgListener.kt | 2 +- .../main/java/qq/service/msg/MessageHelper.kt | 21 +- .../main/java/qq/service/msg/MsgConvertor.kt | 24 +- 10 files changed, 1217 insertions(+), 236 deletions(-) create mode 100644 xposed/src/main/java/qq/service/internals/PrimitiveListener.kt diff --git a/xposed/src/main/java/kritor/service/EventService.kt b/xposed/src/main/java/kritor/service/EventService.kt index 58c1235..826e752 100644 --- a/xposed/src/main/java/kritor/service/EventService.kt +++ b/xposed/src/main/java/kritor/service/EventService.kt @@ -1,28 +1,41 @@ package kritor.service +import io.grpc.Status +import io.grpc.StatusRuntimeException import io.kritor.event.EventRequest import io.kritor.event.EventServiceGrpcKt import io.kritor.event.EventStructure import io.kritor.event.EventType +import io.kritor.event.RequestPushEvent import io.kritor.event.eventStructure import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter object EventService: EventServiceGrpcKt.EventServiceCoroutineImplBase() { - override fun registerActiveListener(request: EventRequest): Flow { + override fun registerActiveListener(request: RequestPushEvent): Flow { return channelFlow { when(request.type!!) { - EventType.CORE_EVENT -> TODO() - EventType.MESSAGE -> GlobalEventTransmitter.onMessageEvent { + EventType.EVENT_TYPE_CORE_EVENT -> {} + EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent { send(eventStructure { - this.type = EventType.MESSAGE + this.type = EventType.EVENT_TYPE_MESSAGE this.message = it.second }) } - EventType.NOTICE -> TODO() - EventType.REQUEST -> TODO() - EventType.UNRECOGNIZED -> TODO() + EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onRequestEvent { + send(eventStructure { + this.type = EventType.EVENT_TYPE_NOTICE + this.request = it + }) + } + EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onNoticeEvent { + send(eventStructure { + this.type = EventType.EVENT_TYPE_NOTICE + this.notice = it + }) + } + EventType.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT) } } } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt index eb2de00..9c18f34 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt @@ -4,17 +4,42 @@ package moe.fuqiuluo.shamrock.internals import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import io.kritor.event.GroupApplyType +import io.kritor.event.GroupMemberBanType +import io.kritor.event.GroupMemberDecreasedType +import io.kritor.event.GroupMemberIncreasedType import io.kritor.event.MessageEvent +import io.kritor.event.NoticeEvent +import io.kritor.event.NoticeType +import io.kritor.event.RequestType +import io.kritor.event.RequestsEvent import io.kritor.event.Scene import io.kritor.event.contact +import io.kritor.event.essenceMessageNotice +import io.kritor.event.friendApplyRequest +import io.kritor.event.friendFileComeNotice +import io.kritor.event.friendPokeNotice +import io.kritor.event.friendRecallNotice +import io.kritor.event.groupAdminChangedNotice +import io.kritor.event.groupApplyRequest +import io.kritor.event.groupFileComeNotice +import io.kritor.event.groupMemberBannedNotice +import io.kritor.event.groupMemberDecreasedNotice +import io.kritor.event.groupMemberIncreasedNotice +import io.kritor.event.groupPokeNotice +import io.kritor.event.groupRecallNotice +import io.kritor.event.groupSignNotice +import io.kritor.event.groupUniqueTitleChangedNotice +import io.kritor.event.groupWholeBanNotice import io.kritor.event.messageEvent +import io.kritor.event.noticeEvent +import io.kritor.event.requestsEvent import io.kritor.event.sender import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch -import kotlinx.io.core.BytePacketBuilder import qq.service.QQInterfaces import qq.service.msg.toKritorMessages @@ -22,16 +47,16 @@ internal object GlobalEventTransmitter: QQInterfaces() { private val messageEventFlow by lazy { MutableSharedFlow>() } - //private val noticeEventFlow by lazy { - // MutableSharedFlow() - //} - //private val requestEventFlow by lazy { - // MutableSharedFlow() - //} + private val noticeEventFlow by lazy { + MutableSharedFlow() + } + private val requestEventFlow by lazy { + MutableSharedFlow() + } - //private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent) + private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent) - //private suspend fun pushRequest(requestEvent: RequestEvent) = requestEventFlow.emit(requestEvent) + private suspend fun pushRequest(requestEvent: RequestsEvent) = requestEventFlow.emit(requestEvent) private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message) @@ -135,10 +160,9 @@ internal object GlobalEventTransmitter: QQInterfaces() { } } - /* /** * 文件通知 通知器 - */ + **/ object FileNoticeTransmitter { /** * 推送私聊文件事件 @@ -153,23 +177,19 @@ internal object GlobalEventTransmitter: QQInterfaces() { expireTime: Long, url: String ): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.PrivateUpload, - operatorId = userId, - userId = userId, - senderId = userId, - privateFile = PrivateFileMsg( - id = fileId, - name = fileName, - size = fileSize, - url = url, - subId = fileSubId, - expire = expireTime - ) - )) + pushNotice(noticeEvent { + this.type = NoticeType.FRIEND_FILE_COME + this.time = msgTime.toInt() + this.friendFileCome = friendFileComeNotice { + this.fileId = fileId + this.fileName = fileName + this.operator = userId + this.fileSize = fileSize + this.expireTime = expireTime.toInt() + this.fileSubId = fileSubId + this.url = url + } + }) return true } @@ -186,22 +206,19 @@ internal object GlobalEventTransmitter: QQInterfaces() { bizId: Int, url: String ): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupUpload, - operatorId = userId, - userId = userId, - groupId = groupId, - file = GroupFileMsg( - id = uuid, - name = fileName, - size = fileSize, - busid = bizId.toLong(), - url = url - ) - )) + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_FILE_COME + this.time = msgTime.toInt() + this.groupFileCome = groupFileComeNotice { + this.groupId = groupId + this.operator = userId + this.fileId = uuid + this.fileName = fileName + this.fileSize = fileSize + this.biz = bizId + this.url = url + } + }) return true } } @@ -211,68 +228,80 @@ internal object GlobalEventTransmitter: QQInterfaces() { */ 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 - ) - )) + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_SIGN + this.time = time.toInt() + this.groupSign = groupSignNotice { + this.groupId = groupCode + this.targetUin = target + this.action = action ?: "" + this.suffix = "" + this.rankImage = rankImg ?: "" + } + }) return true } - suspend fun transGroupPoke(time: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?, groupCode: Long): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.Notify, - subType = NoticeSubType.Poke, - operatorId = operation, - userId = operation, - groupId = groupCode, - target = target, - pokeDetail = PokeDetail( - action = action, - suffix = suffix, - actionImg = actionImg - ) - )) + suspend fun transGroupPoke(time: Long, operator: Long, target: Long, action: String?, suffix: String?, actionImg: String?, groupCode: Long): Boolean { + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_POKE + this.time = time.toInt() + this.groupPoke = groupPokeNotice { + this.action = action ?: "" + this.target = target + this.operator = operator + this.suffix = suffix ?: "" + this.actionImage = actionImg ?: "" + } + }) return true } - suspend fun transGroupMemberNumChanged( + suspend fun transGroupMemberNumIncreased( time: Long, target: Long, targetUid: String, groupCode: Long, operator: Long, operatorUid: String, - noticeType: NoticeType, - noticeSubType: NoticeSubType + type: GroupMemberIncreasedType ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = noticeType, - subType = noticeSubType, - operatorId = operator, - userId = target, - senderId = operator, - target = target, - groupId = groupCode, - targetUid = targetUid, - operatorUid = operatorUid, - userUid = targetUid - )) + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_MEMBER_INCREASE + this.time = time.toInt() + this.groupMemberIncrease = groupMemberIncreasedNotice { + this.groupId = groupCode + this.operatorUid = operatorUid + this.operatorUin = operator + this.targetUid = targetUid + this.targetUin = target + this.type = type + } + }) + return true + } + + suspend fun transGroupMemberNumDecreased( + time: Long, + target: Long, + targetUid: String, + groupCode: Long, + operator: Long, + operatorUid: String, + type: GroupMemberDecreasedType + ): Boolean { + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_MEMBER_INCREASE + this.time = time.toInt() + this.groupMemberDecrease = groupMemberDecreasedNotice { + this.groupId = groupCode + this.operatorUid = operatorUid + this.operatorUin = operator + this.targetUid = targetUid + this.targetUin = target + this.type = type + } + }) return true } @@ -283,25 +312,39 @@ internal object GlobalEventTransmitter: QQInterfaces() { groupCode: Long, setAdmin: Boolean ): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupAdminChange, - subType = if (setAdmin) NoticeSubType.Set else NoticeSubType.UnSet, - operatorId = 0, - userId = target, - userUid = targetUid, - target = target, - targetUid = targetUid, - groupId = groupCode - )) + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_ADMIN_CHANGED + this.time = msgTime.toInt() + this.groupAdminChanged = groupAdminChangedNotice { + this.groupId = groupCode + this.targetUid = targetUid + this.targetUin = target + this.isAdmin = setAdmin + } + }) + return true + } + + suspend fun transGroupWholeBan( + msgTime: Long, + operator: Long, + groupCode: Long, + isOpen: Boolean + ): Boolean { + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_WHOLE_BAN + this.time = msgTime.toInt() + this.groupWholeBan = groupWholeBanNotice { + this.groupId = groupCode + this.isWholeBan = isOpen + this.operator = operator + } + }) return true } suspend fun transGroupBan( msgTime: Long, - subType: NoticeSubType, operator: Long, operatorUid: String, target: Long, @@ -309,43 +352,46 @@ internal object GlobalEventTransmitter: QQInterfaces() { groupCode: Long, duration: Int ): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupBan, - subType = subType, - operatorId = operator, - userId = target, - senderId = operator, - target = target, - groupId = groupCode, - duration = duration, - operatorUid = operatorUid, - targetUid = targetUid - )) + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_MEMBER_BANNED + this.time = msgTime.toInt() + this.groupMemberBanned = groupMemberBannedNotice { + this.groupId = groupCode + this.operatorUid = operatorUid + this.operatorUin = operator + this.targetUid = targetUid + this.targetUin = target + this.duration = duration + this.type = if (duration > 0) GroupMemberBanType.BAN + else GroupMemberBanType.LIFT_BAN + } + }) return true } suspend fun transGroupMsgRecall( time: Long, operator: Long, + operatorUid: String, target: Long, + targetUid: String, groupCode: Long, - msgHash: Int, + msgId: Long, tipText: String ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupRecall, - operatorId = operator, - userId = target, - msgId = msgHash, - tip = tipText, - groupId = groupCode - )) + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_RECALL + this.time = time.toInt() + this.groupRecall = groupRecallNotice { + this.groupId = groupCode + this.operatorUid = operatorUid + this.operatorUin = operator + this.targetUid = targetUid + this.targetUin = target + this.messageId = msgId + this.tipText = tipText + } + }) return true } @@ -356,16 +402,7 @@ internal object GlobalEventTransmitter: QQInterfaces() { newCard: String, groupId: Long ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.GroupCard, - userId = targetId, - cardNew = newCard, - cardOld = oldCard, - groupId = groupId - )) + return true } @@ -375,16 +412,15 @@ internal object GlobalEventTransmitter: QQInterfaces() { title: String, groupId: Long ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.Notify, - userId = targetId, - groupId = groupId, - title = title, - subType = NoticeSubType.Title - )) + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_MEMBER_UNIQUE_TITLE_CHANGED + this.time = time.toInt() + this.groupMemberUniqueTitleChanged = groupUniqueTitleChangedNotice { + this.groupId = groupId + this.target = targetId + this.title = title + } + }) return true } @@ -392,21 +428,21 @@ internal object GlobalEventTransmitter: QQInterfaces() { time: Long, senderUin: Long, operatorUin: Long, - msgId: Int, + msgId: Long, groupId: Long, - subType: NoticeSubType + subType: UInt ): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.Essence, - senderId = senderUin, - groupId = groupId, - operatorId = operatorUin, - msgId = msgId, - subType = subType - )) + pushNotice(noticeEvent { + this.type = NoticeType.GROUP_ESSENCE_CHANGED + this.time = time.toInt() + this.groupEssenceChanged = essenceMessageNotice { + this.groupId = groupId + this.messageId = msgId + this.sender = senderUin + this.operator = operatorUin + this.subType = subType.toInt() + } + }) return true } } @@ -415,37 +451,31 @@ internal object GlobalEventTransmitter: QQInterfaces() { * 私聊通知 通知器 */ object PrivateNoticeTransmitter { - suspend fun transPrivatePoke(msgTime: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?): Boolean { - pushNotice(NoticeEvent( - time = msgTime, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.Notify, - subType = NoticeSubType.Poke, - operatorId = operation, - userId = operation, - senderId = operation, - target = target, - pokeDetail = PokeDetail( - actionImg = actionImg, - action = action, - suffix = suffix - ) - )) + suspend fun transPrivatePoke(msgTime: Long, operator: Long, target: Long, action: String?, suffix: String?, actionImg: String?): Boolean { + pushNotice(noticeEvent { + this.type = NoticeType.FRIEND_POKE + this.time = msgTime.toInt() + this.friendPoke = friendPokeNotice { + this.action = action ?: "" + this.target = target + this.operator = operator + this.suffix = suffix ?: "" + this.actionImage = actionImg ?: "" + } + }) return true } - suspend fun transPrivateRecall(time: Long, operation: Long, msgHashId: Int, tipText: String): Boolean { - pushNotice(NoticeEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Notice, - type = NoticeType.FriendRecall, - operatorId = operation, - userId = operation, - msgId = msgHashId, - tip = tipText - )) + suspend fun transPrivateRecall(time: Long, operator: Long, msgId: Long, tipText: String): Boolean { + pushNotice(noticeEvent { + this.type = NoticeType.FRIEND_RECALL + this.time = time.toInt() + this.friendRecall = friendRecallNotice { + this.operator = operator + this.messageId = msgId + this.tipText = tipText + } + }) return true } @@ -455,16 +485,16 @@ internal object GlobalEventTransmitter: QQInterfaces() { * 请求 通知器 */ object RequestTransmitter { - suspend fun transFriendApp(time: Long, operation: Long, tipText: String, flag: String): Boolean { - pushRequest(RequestEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Request, - type = RequestType.Friend, - userId = operation, - comment = tipText, - flag = flag - )) + suspend fun transFriendApp(time: Long, operator: Long, tipText: String, flag: String): Boolean { + pushRequest(requestsEvent { + this.type = RequestType.FRIEND_APPLY + this.time = time.toInt() + this.friendApply = friendApplyRequest { + this.applierUin = operator + this.message = tipText + this.flag = flag + } + }) return true } @@ -475,23 +505,23 @@ internal object GlobalEventTransmitter: QQInterfaces() { reason: String, groupCode: Long, flag: String, - subType: RequestSubType + type: GroupApplyType ): Boolean { - pushRequest(RequestEvent( - time = time, - selfId = app.longAccountUin, - postType = PostType.Request, - type = RequestType.Group, - userId = applier, - userUid = applierUid, - comment = reason, - groupId = groupCode, - subType = subType, - flag = flag - )) + pushRequest(requestsEvent { + this.type = RequestType.GROUP_APPLY + this.time = time.toInt() + this.groupApply = groupApplyRequest { + this.applierUid = applierUid + this.applierUin = applier + this.groupId = groupCode + this.reason = reason + this.flag = flag + this.type = type + } + }) return true } - }*/ + } suspend inline fun onMessageEvent(collector: FlowCollector>) { messageEventFlow.collect { @@ -501,7 +531,6 @@ internal object GlobalEventTransmitter: QQInterfaces() { } } - /* suspend inline fun onNoticeEvent(collector: FlowCollector) { noticeEventFlow.collect { GlobalScope.launch { @@ -510,11 +539,11 @@ internal object GlobalEventTransmitter: QQInterfaces() { } } - suspend inline fun onRequestEvent(collector: FlowCollector) { + suspend inline fun onRequestEvent(collector: FlowCollector) { requestEventFlow.collect { GlobalScope.launch { collector.emit(it) } } - }*/ + } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/friend/FriendHelper.kt b/xposed/src/main/java/qq/service/friend/FriendHelper.kt index d93921e..a71205b 100644 --- a/xposed/src/main/java/qq/service/friend/FriendHelper.kt +++ b/xposed/src/main/java/qq/service/friend/FriendHelper.kt @@ -3,11 +3,15 @@ package qq.service.friend import com.tencent.mobileqq.data.Friends import com.tencent.mobileqq.friend.api.IFriendDataService import com.tencent.mobileqq.friend.api.IFriendHandlerService +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.mobileqq.relation.api.IAddFriendTempApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +import moe.fuqiuluo.shamrock.tools.slice import qq.service.QQInterfaces +import tencent.mobileim.structmsg.structmsg import kotlin.coroutines.resume internal object FriendHelper: QQInterfaces() { @@ -21,6 +25,69 @@ internal object FriendHelper: QQInterfaces() { return Result.success(service.allFriends) } + // ProfileService.Pb.ReqSystemMsgAction.Friend + fun requestFriendRequest(msgSeq: Long, uin: Long, remark: String = "", approve: Boolean? = true, notSee: Boolean? = false) { + val service = QRoute.api(IAddFriendTempApi::class.java) + val action = structmsg.SystemMsgActionInfo() + action.type.set(if (approve != false) 2 else 3) + action.group_id.set(0) + action.remark.set(remark) + val snInfo = structmsg.AddFrdSNInfo() + snInfo.uint32_not_see_dynamic.set(if (notSee != false) 1 else 0) + snInfo.uint32_set_sn.set(0) + action.addFrdSNInfo.set(snInfo) + service.sendFriendSystemMsgAction(1, msgSeq, uin, 1, 2004, 11, 0, action, 0, structmsg.StructMsg(), false, app) + } + + suspend fun requestFriendSystemMsgNew(msgNum: Int, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 3): List? { + if (retryCnt < 0) { + return ArrayList() + } + val req = structmsg.ReqSystemMsgNew() + req.msg_num.set(msgNum) + req.latest_friend_seq.set(latestFriendSeq) + req.latest_group_seq.set(latestGroupSeq) + req.version.set(1000) + req.checktype.set(2) + val flag = structmsg.FlagInfo() +// flag.GrpMsg_Kick_Admin.set(1) +// flag.GrpMsg_HiddenGrp.set(1) +// flag.GrpMsg_WordingDown.set(1) + flag.FrdMsg_GetBusiCard.set(1) +// flag.GrpMsg_GetOfficialAccount.set(1) +// flag.GrpMsg_GetPayInGroup.set(1) + flag.FrdMsg_Discuss2ManyChat.set(1) +// flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1) + flag.FrdMsg_NeedWaitingMsg.set(1) + flag.FrdMsg_uint32_need_all_unread_msg.set(1) +// flag.GrpMsg_NeedAutoAdminWording.set(1) +// flag.GrpMsg_get_transfer_group_msg_flag.set(1) +// flag.GrpMsg_get_quit_pay_group_msg_flag.set(1) +// flag.GrpMsg_support_invite_auto_join.set(1) +// flag.GrpMsg_mask_invite_auto_join.set(1) +// flag.GrpMsg_GetDisbandedByAdmin.set(1) + flag.GrpMsg_GetC2cInviteJoinGroup.set(1) + req.flag.set(flag) + req.is_get_frd_ribbon.set(false) + req.is_get_grp_ribbon.set(false) + req.friend_msg_type_flag.set(1) + req.uint32_req_msg_type.set(1) + req.uint32_need_uid.set(1) + val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Friend", true, req.toByteArray()) + return if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { + ArrayList() + } else { + try { + val msg = structmsg.RspSystemMsgNew() + msg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + return msg.friendmsgs.get() + } catch (err: Throwable) { + requestFriendSystemMsgNew(msgNum, latestFriendSeq, latestGroupSeq, retryCnt - 1) + } + + } + } + private suspend fun requestFriendList(dataService: IFriendDataService): Boolean { val service = app.getRuntimeService(IFriendHandlerService::class.java, "all") service.requestFriendList(true, 0) diff --git a/xposed/src/main/java/qq/service/group/GroupHelper.kt b/xposed/src/main/java/qq/service/group/GroupHelper.kt index 0189399..7062b6c 100644 --- a/xposed/src/main/java/qq/service/group/GroupHelper.kt +++ b/xposed/src/main/java/qq/service/group/GroupHelper.kt @@ -37,6 +37,7 @@ import tencent.im.oidb.cmd0x8fc.Oidb_0x8fc import tencent.im.oidb.cmd0xed3.oidb_cmd0xed3 import tencent.im.oidb.oidb_sso import tencent.im.troop.honor.troop_honor +import tencent.mobileim.structmsg.structmsg import java.lang.reflect.Method import java.lang.reflect.Modifier import java.nio.ByteBuffer @@ -217,6 +218,106 @@ internal object GroupHelper: QQInterfaces() { sendOidb("OidbSvc.0x55c_1", 1372, 1, array) } + // ProfileService.Pb.ReqSystemMsgAction.Group + suspend fun requestGroupRequest( + msgSeq: Long, + uin: Long, + gid: Long, + msg: String? = "", + approve: Boolean? = true, + notSee: Boolean? = false, + subType: String + ): Result{ + val req = structmsg.ReqSystemMsgAction().apply { + if (subType == "invite") { + msg_type.set(1) + src_id.set(3) + sub_src_id.set(10016) + group_msg_type.set(2) + } else { + msg_type.set(2) + src_id.set(2) + sub_src_id.set(30024) + group_msg_type.set(1) + } + msg_seq.set(msgSeq) + req_uin.set(uin) + sub_type.set(1) + action_info.set(structmsg.SystemMsgActionInfo().apply { + type.set(if (approve != false) 11 else 12) + group_code.set(gid) + if (subType == "add") { + this.msg.set(msg) + this.blacklist.set(notSee != false) + } + }) + language.set(1000) + } + val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgAction.Group", true, req.toByteArray()) + ?: return Result.failure(Exception("ReqSystemMsgAction.Group: No Response")) + if (fromServiceMsg.wupBuffer == null) { + return Result.failure(Exception("ReqSystemMsgAction.Group: No WupBuffer")) + } + val rsp = structmsg.RspSystemMsgAction().mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + return if (rsp.head.result.has()) { + if (rsp.head.result.get() == 0) { + Result.success(rsp.msg_detail.get()) + } else { + Result.failure(Exception(rsp.head.msg_fail.get())) + } + } else { + Result.failure(Exception("操作失败")) + } + } + + suspend fun requestGroupSystemMsgNew(msgNum: Int, reqMsgType: Int = 1, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 5): List { + if (retryCnt < 0) { + return ArrayList() + } + val req = structmsg.ReqSystemMsgNew() + req.msg_num.set(msgNum) + req.latest_friend_seq.set(latestFriendSeq) + req.latest_group_seq.set(latestGroupSeq) + req.version.set(1000) + req.checktype.set(3) + val flag = structmsg.FlagInfo() + flag.GrpMsg_Kick_Admin.set(1) + flag.GrpMsg_HiddenGrp.set(1) + flag.GrpMsg_WordingDown.set(1) +// flag.FrdMsg_GetBusiCard.set(1) + flag.GrpMsg_GetOfficialAccount.set(1) + flag.GrpMsg_GetPayInGroup.set(1) + flag.FrdMsg_Discuss2ManyChat.set(1) + flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1) + flag.FrdMsg_NeedWaitingMsg.set(1) +// flag.FrdMsg_uint32_need_all_unread_msg.set(1) + flag.GrpMsg_NeedAutoAdminWording.set(1) + flag.GrpMsg_get_transfer_group_msg_flag.set(1) + flag.GrpMsg_get_quit_pay_group_msg_flag.set(1) + flag.GrpMsg_support_invite_auto_join.set(1) + flag.GrpMsg_mask_invite_auto_join.set(1) + flag.GrpMsg_GetDisbandedByAdmin.set(1) + flag.GrpMsg_GetC2cInviteJoinGroup.set(1) + req.flag.set(flag) + req.is_get_frd_ribbon.set(false) + req.is_get_grp_ribbon.set(false) + req.friend_msg_type_flag.set(1) + req.uint32_req_msg_type.set(reqMsgType) + req.uint32_need_uid.set(1) + val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Group", true, req.toByteArray()) + return if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { + ArrayList() + } else { + try { + val msg = structmsg.RspSystemMsgNew() + msg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + return msg.groupmsgs.get().orEmpty() + } catch (err: Throwable) { + requestGroupSystemMsgNew(msgNum, reqMsgType, latestFriendSeq, latestGroupSeq, retryCnt - 1) + } + } + } + suspend fun setGroupUniqueTitle(groupId: Long, userId: Long, title: String) { val localMemberInfo = getTroopMemberInfoByUin(groupId, userId, true).getOrThrow() val req = Oidb_0x8fc.ReqBody() diff --git a/xposed/src/main/java/qq/service/internals/AioListener.kt b/xposed/src/main/java/qq/service/internals/AioListener.kt index d8d9ade..4412541 100644 --- a/xposed/src/main/java/qq/service/internals/AioListener.kt +++ b/xposed/src/main/java/qq/service/internals/AioListener.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.launch import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter +import qq.service.bdh.RichProtoSvc import qq.service.kernel.SimpleKernelMsgListener import qq.service.msg.MessageHelper @@ -29,6 +30,8 @@ object AioListener: SimpleKernelMsgListener() { private suspend fun onMsg(record: MsgRecord) { when (record.chatType) { MsgConstant.KCHATTYPEGROUP -> { + if (record.senderUin == 0L) return + LogCenter.log("群消息(group = ${record.peerName}(${record.peerUin}), uin = ${record.senderUin}, id = ${record.msgId})") if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(record, record.elements)) { @@ -75,4 +78,63 @@ object AioListener: SimpleKernelMsgListener() { else -> LogCenter.log("不支持PUSH事件: ${record.chatType}") } } + + override fun onFileMsgCome(arrayList: ArrayList?) { + arrayList?.forEach { record -> + GlobalScope.launch { + when (record.chatType) { + MsgConstant.KCHATTYPEGROUP -> onGroupFileMsg(record) + MsgConstant.KCHATTYPEC2C -> onC2CFileMsg(record) + else -> LogCenter.log("不支持该来源的文件上传事件:${record}", Level.WARN) + } + } + } + } + + private suspend fun onC2CFileMsg(record: MsgRecord) { + val userId = record.senderUin + val fileMsg = record.elements.firstOrNull { + it.elementType == MsgConstant.KELEMTYPEFILE + }?.fileElement ?: kotlin.run { + LogCenter.log("消息为私聊文件消息但不包含文件消息,来自:${record.peerUin}", Level.WARN) + return + } + + val fileName = fileMsg.fileName + val fileSize = fileMsg.fileSize + val expireTime = fileMsg.expireTime ?: 0 + val fileId = fileMsg.fileUuid + val fileSubId = fileMsg.fileSubId ?: "" + val url = RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) + + if (!GlobalEventTransmitter.FileNoticeTransmitter + .transPrivateFileEvent(record.msgTime, userId, fileId, fileSubId, fileName, fileSize, expireTime, url) + ) { + LogCenter.log("私聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN) + } + } + + private suspend fun onGroupFileMsg(record: MsgRecord) { + val groupId = record.peerUin + val userId = record.senderUin + val fileMsg = record.elements.firstOrNull { + it.elementType == MsgConstant.KELEMTYPEFILE + }?.fileElement ?: kotlin.run { + LogCenter.log("消息为群聊文件消息但不包含文件消息,来自:${record.peerUin}", Level.WARN) + return + } + //val fileMd5 = fileMsg.fileMd5 + val fileName = fileMsg.fileName + val fileSize = fileMsg.fileSize + val uuid = fileMsg.fileUuid + val bizId = fileMsg.fileBizId + + val url = RichProtoSvc.getGroupFileDownUrl(record.peerUin, uuid, bizId) + + if (!GlobalEventTransmitter.FileNoticeTransmitter + .transGroupFileEvent(record.msgTime, userId, groupId, uuid, fileName, fileSize, bizId, url) + ) { + LogCenter.log("群聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN) + } + } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/internals/MSFHandler.kt b/xposed/src/main/java/qq/service/internals/MSFHandler.kt index 2317332..0cc4c13 100644 --- a/xposed/src/main/java/qq/service/internals/MSFHandler.kt +++ b/xposed/src/main/java/qq/service/internals/MSFHandler.kt @@ -52,8 +52,12 @@ internal object MSFHandler { fun onPush(fromServiceMsg: FromServiceMsg) { val cmd = fromServiceMsg.serviceCmd - val push = mPushHandlers[cmd] - push?.invoke(fromServiceMsg) + if (cmd == "trpc.msg.olpush.OlPushService.MsgPush") { + PrimitiveListener.onPush(fromServiceMsg) + } else { + val push = mPushHandlers[cmd] + push?.invoke(fromServiceMsg) + } } fun onResp(toServiceMsg: ToServiceMsg, fromServiceMsg: FromServiceMsg) { diff --git a/xposed/src/main/java/qq/service/internals/PrimitiveListener.kt b/xposed/src/main/java/qq/service/internals/PrimitiveListener.kt new file mode 100644 index 0000000..c2a9b91 --- /dev/null +++ b/xposed/src/main/java/qq/service/internals/PrimitiveListener.kt @@ -0,0 +1,668 @@ +@file:OptIn(DelicateCoroutinesApi::class) +package qq.service.internals + +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qphone.base.remote.FromServiceMsg +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.msg.api.IMsgService +import io.kritor.event.GroupApplyType +import io.kritor.event.GroupMemberDecreasedType +import io.kritor.event.GroupMemberIncreasedType +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.io.core.ByteReadPacket +import kotlinx.io.core.discardExact +import kotlinx.io.core.readBytes +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.tools.readBuf32Long +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.symbols.decodeProtobuf +import protobuf.message.ContentHead +import protobuf.message.MsgBody +import protobuf.message.ResponseHead +import protobuf.push.C2CCommonTipsEvent +import protobuf.push.C2CRecallEvent +import protobuf.push.FriendApplyEvent +import protobuf.push.GroupAdminChangeEvent +import protobuf.push.GroupApplyEvent +import protobuf.push.GroupBanEvent +import protobuf.push.GroupCommonTipsEvent +import protobuf.push.GroupInviteEvent +import protobuf.push.GroupInvitedApplyEvent +import protobuf.push.GroupListChangeEvent +import protobuf.push.MessagePush +import protobuf.push.MessagePushClientInfo +import qq.service.QQInterfaces +import qq.service.contact.ContactHelper +import qq.service.friend.FriendHelper.requestFriendSystemMsgNew +import qq.service.group.GroupHelper +import qq.service.group.GroupHelper.requestGroupSystemMsgNew +import qq.service.msg.MessageHelper +import kotlin.coroutines.resume + +internal object PrimitiveListener { + + fun onPush(fromServiceMsg: FromServiceMsg) { + if (fromServiceMsg.wupBuffer == null) return + try { + val push = fromServiceMsg.wupBuffer.slice(4) + .decodeProtobuf() + GlobalScope.launch { + onMsgPush(push) + } + } catch (e: Exception) { + LogCenter.log(e.stackTraceToString(), Level.WARN) + } + } + + private suspend fun onMsgPush(push: MessagePush) { + if ( + push.msgBody == null || + push.msgBody!!.contentHead == null || + push.msgBody!!.body == null || + push.msgBody!!.contentHead!!.msgTime == null + ) return + val msgBody = push.msgBody!! + val contentHead = msgBody.contentHead!! + val msgType = contentHead.msgType + val subType = contentHead.msgSubType + val msgTime = contentHead.msgTime!! + val body = msgBody.body!! + try { + when (msgType) { + 33 -> onGroupMemIncreased(msgTime, body) + 34 -> onGroupMemberDecreased(msgTime, body) + 44 -> onGroupAdminChange(msgTime, body) + 82 -> onGroupMessage(msgTime, body) + 84 -> onGroupApply(msgTime, contentHead, body) + 87 -> onInviteGroup(msgTime, msgBody.msgHead!!, body) + 528 -> when (subType) { + 35 -> onFriendApply(msgTime, push.clientInfo!!, body) + 39 -> onCardChange(msgTime, body) + 68 -> onGroupApply(msgTime, contentHead, body) + 138 -> onC2CRecall(msgTime, body) + 290 -> onC2CPoke(msgTime, body) + } + + 732 -> when (subType) { + 12 -> onGroupBan(msgTime, body) + 16 -> onGroupUniqueTitleChange(msgTime, body) + 17 -> onGroupRecall(msgTime, body) + 20 -> onGroupCommonTips(msgTime, body) + 21 -> onEssenceMessage(msgTime, push.clientInfo, body) + } + } + } catch (e: Exception) { + LogCenter.log("onMsgPush(msgType: $msgType, subType: $subType): " + e.stackTraceToString(), Level.WARN) + } + } + + private fun onGroupMessage(msgTime: Long, body: MsgBody) { + + } + + private suspend fun onC2CPoke(msgTime: Long, body: MsgBody) { + val event = body.msgContent!!.decodeProtobuf() + if (event.params == null) return + + val params = event.params!!.associate { + it.key to it.value + } + + val target = params["uin_str2"] ?: return + val operation = params["uin_str1"] ?: return + val suffix = params["suffix_str"] ?: "" + val actionImg = params["action_img_url"] ?: "" + val action = params["alt_str1"] ?: "" + + LogCenter.log("私聊戳一戳: $operation $action $target $suffix") + + if (!GlobalEventTransmitter.PrivateNoticeTransmitter + .transPrivatePoke(msgTime, operation.toLong(), target.toLong(), action, suffix, actionImg) + ) { + LogCenter.log("私聊戳一戳推送失败!", Level.WARN) + } + } + + private suspend fun onFriendApply( + msgTime: Long, + clientInfo: MessagePushClientInfo, + body: MsgBody + ) { + val event = body.msgContent!!.decodeProtobuf() + if (event.head == null) return + val head = event.head!! + val applierUid = head.applierUid + val msg = head.applyMsg ?: "" + val source = head.source ?: "" + var applier = ContactHelper.getUinByUidAsync(applierUid).toLong() + if (applier == 0L) { + applier = clientInfo.liteHead?.sender?.toLong() ?: 0 + } + val src = head.srcId + val subSrc = head.subSrc + val flag: String = try { + val reqs = requestFriendSystemMsgNew(20, 0, 0) + val req = reqs?.first { + it.msg_time.get() == msgTime + } + val seq = req?.msg_seq?.get() + "$seq;$src;$subSrc;$applier" + } catch (err: Throwable) { + "$msgTime;$src;$subSrc;$applier" + } + LogCenter.log("来自$applier 的好友申请:$msg ($source)") + if (!GlobalEventTransmitter.RequestTransmitter + .transFriendApp(msgTime, applier, msg, flag) + ) { + LogCenter.log("好友申请推送失败!", Level.WARN) + } + } + + + private suspend fun onCardChange(msgTime: Long, body: MsgBody) { +// val event = runCatching { +// body.msgContent!!.decodeProtobuf() +// }.getOrElse { +// val readPacket = ByteReadPacket(body.msgContent!!) +// readPacket.readBuf32Long() +// readPacket.discardExact(1) +// +// readPacket.readBytes(readPacket.readShort().toInt()).also { +// readPacket.release() +// }.decodeProtobuf() +// } +// +// val targetId = detail[1, 13, 2].asUtf8String +// val newCardList = detail[1, 13, 3].asList +// var newCard = "" +// newCardList +// .value +// .forEach { +// if (it[1].asInt == 1) { +// newCard = it[2].asUtf8String +// } +// } +// val groupId = detail[1, 13, 4].asLong +// var oldCard = "" +// val targetQQ = ContactHelper.getUinByUidAsync(targetId).toLong() +// LogCenter.log("群组[$groupId]成员$targetQQ 群名片变动 -> $newCard") +// // oldCard暂时获取不到 +// if (!GlobalEventTransmitter.GroupNoticeTransmitter +// .transCardChange(msgTime, targetQQ, oldCard, newCard, groupId) +// ) { +// LogCenter.log("群名片变动推送失败!", Level.WARN) +// } + } + + private suspend fun onGroupUniqueTitleChange(msgTime: Long, body: MsgBody) { + val event = runCatching { + body.msgContent!!.decodeProtobuf() + }.getOrElse { + val readPacket = ByteReadPacket(body.msgContent!!) + readPacket.readBuf32Long() + readPacket.discardExact(1) + + readPacket.readBytes(readPacket.readShort().toInt()).also { + readPacket.release() + }.decodeProtobuf() + } + val groupId = event.groupCode.toLong() + val detail = event.uniqueTitleChangeDetail!!.first() + + //detail = if (detail[5] is ProtoList) { + // (detail[5] as ProtoList).value[0] + //} else { + // detail[5] + // } + + val targetUin = detail.targetUin.toLong() + + // 恭喜<{\"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.wording + if (titleChangeInfo.indexOf("群主授予") == -1) { + return + } + val titleJson = titleChangeInfo.split("获得群主授予的<")[1].replace(">头衔", "") + val titleJsonObj = Json.decodeFromString(titleJson).asJsonObject + val title = titleJsonObj["text"].asString + + LogCenter.log("群组[$groupId]成员$targetUin 获得群头衔 -> $title") + + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transTitleChange(msgTime, targetUin, title, groupId) + ) { + LogCenter.log("群头衔变动推送失败!", Level.WARN) + } + } + + private suspend fun onEssenceMessage( + msgTime: Long, + clientInfo: MessagePushClientInfo?, + body: MsgBody + ) { + if (clientInfo == null) return + val event = runCatching { + body.msgContent!!.decodeProtobuf() + }.getOrElse { + val readPacket = ByteReadPacket(body.msgContent!!) + readPacket.readBuf32Long() + readPacket.discardExact(1) + + readPacket.readBytes(readPacket.readShort().toInt()).also { + readPacket.release() + }.decodeProtobuf() + } + val groupId = event.groupCode.toLong() + val detail = event.essenceMsgInfo!!.first() + + val msgSeq = event.msgSeq.toLong() + val senderUin = detail.sender.toLong() + val operatorUin = detail.operator.toLong() + + when (val type = detail.type) { + 1u -> { + LogCenter.log("群设精消息(groupId=$groupId, sender=$senderUin, msgSeq=$msgSeq, operator=$operatorUin)") + } + 2u -> { + LogCenter.log("群撤精消息(groupId=$groupId, sender=$senderUin, msgId=$msgSeq, operator=$operatorUin)") + } + else -> error("onEssenceMessage unknown type: $type") + } + + val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId.toString()) + val sourceRecord = withTimeoutOrNull(3000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records -> + it.resume(records) + } + } + }?.firstOrNull() + + if (sourceRecord == null) { + LogCenter.log("无法获取源消息记录,无法推送精华消息变动!", Level.WARN) + return + } + + val msgId = sourceRecord.msgId + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transEssenceChange(msgTime, senderUin, operatorUin, msgId, groupId, detail.type) + ) { + LogCenter.log("精华消息变动推送失败!", Level.WARN) + } + } + + + private suspend fun onGroupCommonTips(time: Long, body: MsgBody) { + val event = runCatching { + body.msgContent!!.decodeProtobuf() + }.getOrElse { + val readPacket = ByteReadPacket(body.msgContent!!) + readPacket.discardExact(4) + readPacket.discardExact(1) + + readPacket.readBytes(readPacket.readShort().toInt()).also { + readPacket.release() + }.decodeProtobuf() + } + val groupId = event.groupCode.toLong() + val detail = event.baseTips!!.first() + + val params = detail.params!!.associate { + it.key to it.value + } + + val target = params["uin_str2"] ?: params["mqq_uin"] ?: return + val operation = params["uin_str1"] ?: return + val suffix = params["suffix_str"] ?: "" + val actionImg = params["action_img_url"] ?: "" + val action = params["alt_str1"] + ?: params["action_str"] + ?: params["user_sign"] + ?: "" + val rankImg = params["rank_img"] ?: "" + + when (detail.type) { + 1061u -> { + LogCenter.log("群戳一戳($groupId): $operation $action $target $suffix") + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupId) + ) { + LogCenter.log("群戳一戳推送失败!", Level.WARN) + } + } + + 1068u -> { + 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.type}", Level.WARN) + } + } + } + + private suspend fun onC2CRecall(time: Long, body: MsgBody) { + val event = body.msgContent!!.decodeProtobuf() + val head = event.head!! + + val operationUid = head.operator!! + val operator = ContactHelper.getUinByUidAsync(operationUid).toLong() + + val msgSeq = head.msgSeq + val tipText = head.wording?.wording ?: "" + + val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, operationUid) + val sourceRecord = withTimeoutOrNull(3000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records -> + it.resume(records) + } + } + }?.firstOrNull() + + if (sourceRecord == null) { + LogCenter.log("无法获取源消息记录,无法推送撤回消息!", Level.WARN) + return + } + + val msgId = sourceRecord.msgId + + LogCenter.log("私聊消息撤回: $operator, seq = $msgSeq, msgId = ${msgId}, tip = $tipText") + + if (!GlobalEventTransmitter.PrivateNoticeTransmitter + .transPrivateRecall(time, operator, msgId, tipText) + ) { + LogCenter.log("私聊消息撤回推送失败!", Level.WARN) + } + } + + private suspend fun onGroupMemIncreased(time: Long, body: MsgBody) { + val event = body.msgContent!!.decodeProtobuf() + val groupCode = event.groupCode + val targetUid = event.memberUid + val type = event.type + + GroupHelper.getGroupMemberList(groupCode.toString(), true).onFailure { + LogCenter.log("新成员加入刷新群成员列表失败: $groupCode", Level.WARN) + }.onSuccess { + LogCenter.log("新成员加入刷新群成员列表成功,群成员数量: ${it.size}", Level.INFO) + } + + val operatorUid = event.operatorUid + val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong() + val target = ContactHelper.getUinByUidAsync(targetUid).toLong() + LogCenter.log("群成员增加($groupCode): $target, type = $type") + + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transGroupMemberNumIncreased( + time, + target, + targetUid, + groupCode, + operator, + operatorUid, + when (type) { + 130 -> GroupMemberIncreasedType.APPROVE + 131 -> GroupMemberIncreasedType.INVITE + else -> GroupMemberIncreasedType.APPROVE + } + ) + ) { + LogCenter.log("群成员增加推送失败!", Level.WARN) + } + } + + private suspend fun onGroupMemberDecreased(time: Long, body: MsgBody) { + val event = body.msgContent!!.decodeProtobuf() + val groupCode = event.groupCode + val targetUid = event.memberUid + val type = event.type + val operatorUid = event.operatorUid + + GroupHelper.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 -> GroupMemberDecreasedType.LEAVE + 131 -> GroupMemberDecreasedType.KICK + 3 -> GroupMemberDecreasedType.KICK_ME + else -> GroupMemberDecreasedType.KICK + } + + LogCenter.log("群成员减少($groupCode): $target, type = $subtype ($type)") + + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transGroupMemberNumDecreased( + time, + target, + targetUid, + groupCode, + operator, + operatorUid, + subtype + ) + ) { + LogCenter.log("群成员减少推送失败!", Level.WARN) + } + } + + private suspend fun onGroupAdminChange(msgTime: Long, body: MsgBody) { + val event = body.msgContent!!.decodeProtobuf() + val groupCode = event.groupCode + if (event.operation == null) return + val operation = event.operation!! + if (operation.setInfo == null && operation.unsetInfo == null) return + + val isSetAdmin: Boolean + val targetUid: String + if (operation.setInfo == null) { + isSetAdmin = false + targetUid = operation.unsetInfo!!.targetUid!! + } else { + isSetAdmin = true + targetUid = operation.setInfo!!.targetUid!! + } + + val target = ContactHelper.getUinByUidAsync(targetUid).toLong() + LogCenter.log("群管理员变动($groupCode): $target, isSetAdmin = $isSetAdmin") + + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transGroupAdminChanged(msgTime, target, targetUid, groupCode, isSetAdmin) + ) { + LogCenter.log("群管理员变动推送失败!", Level.WARN) + } + } + + private suspend fun onGroupBan(msgTime: Long, body: MsgBody) { + val event = body.msgContent!!.decodeProtobuf() + val groupCode = event.groupCode.toLong() + val operatorUid = event.operatorUid + val wholeBan = event.target?.target?.targetUid == null + val targetUid = event.target?.target?.targetUid ?: "" + val rawDuration = event.target?.target?.rawDuration?.toInt() ?: 0 + val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong() + val duration = if (wholeBan) -1 else rawDuration + val target = if (wholeBan) 0 else ContactHelper.getUinByUidAsync(targetUid).toLong() + + if (wholeBan) { + LogCenter.log("群全员禁言($groupCode): $operator -> ${if (rawDuration != 0) "开启" else "关闭"}") + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transGroupWholeBan(msgTime, groupCode, operator, rawDuration != 0) + ) { + LogCenter.log("群禁言推送失败!", Level.WARN) + } + } else { + LogCenter.log("群禁言($groupCode): $operator -> $target, 时长 = ${duration}s") + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transGroupBan(msgTime, operator, operatorUid, target, targetUid, groupCode, duration) + ) { + LogCenter.log("群禁言推送失败!", Level.WARN) + } + } + } + + private suspend fun onGroupRecall(time: Long, body: MsgBody) { + val event = runCatching { + body.msgContent!!.decodeProtobuf() + }.getOrElse { + val readPacket = ByteReadPacket(body.msgContent!!) + readPacket.discardExact(4) + readPacket.discardExact(1) + readPacket.readBytes(readPacket.readShort().toInt()).also { + readPacket.release() + }.decodeProtobuf() + } + val groupCode = event.groupCode.toLong() + val detail = event.recallDetails!! + val operatorUid = detail.operatorUid + val targetUid = detail.msgInfo!!.senderUid + val msgSeq = detail.msgInfo!!.msgSeq.toLong() + val tipText = detail.wording?.wording ?: "" + + val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupCode.toString()) + val sourceRecord = withTimeoutOrNull(3000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records -> + it.resume(records) + } + } + }?.firstOrNull() + + if (sourceRecord == null) { + LogCenter.log("无法获取源消息记录,无法推送撤回消息!", Level.WARN) + return + } + + val msgId = sourceRecord.msgId + + val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong() + val target = ContactHelper.getUinByUidAsync(targetUid).toLong() + LogCenter.log("群消息撤回($groupCode): $operator -> $target, seq = $msgSeq, id = $msgId, tip = $tipText") + + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transGroupMsgRecall(time, operator, operatorUid, target, targetUid, groupCode, msgId, tipText) + ) { + LogCenter.log("群消息撤回推送失败!", Level.WARN) + } + } + + private suspend fun onGroupApply(time: Long, contentHead: ContentHead, body: MsgBody) { + when (contentHead.msgType) { + 84 -> { + val event = body.msgContent!!.decodeProtobuf() + val groupCode = event.groupCode + val applierUid = event.applierUid + val reason = event.applyMsg ?: "" + var applier = ContactHelper.getUinByUidAsync(applierUid).toLong() + if (applier == QQInterfaces.app.longAccountUin) { + return + } + val msgSeq = contentHead.msgSeq + val flag = try { + var reqs = requestGroupSystemMsgNew(10, 1) + val riskReqs = requestGroupSystemMsgNew(5, 2) + reqs = reqs + riskReqs + val req = reqs.firstOrNull { + it.msg_time.get() == time && it.msg?.group_code?.get() == groupCode + } + val seq = req?.msg_seq?.get() ?: time + if (applier == 0L) { + applier = req?.req_uin?.get() ?: 0L + } + "$seq;$groupCode;$applier" + } catch (err: Throwable) { + "$time;$groupCode;$applier" + } + LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq") + if (!GlobalEventTransmitter.RequestTransmitter + .transGroupApply(time, applier, applierUid, reason, groupCode, flag, GroupApplyType.GROUP_APPLY_ADD) + ) { + LogCenter.log("入群申请推送失败!", Level.WARN) + } + } + + 528 -> { + val event = body.msgContent!!.decodeProtobuf() + val groupCode = event.applyInfo?.groupCode ?: return + val applierUid = event.applyInfo?.applierUid ?: return + var applier = ContactHelper.getUinByUidAsync(applierUid).toLong() + if (applier == QQInterfaces.app.longAccountUin) { + return + } + if ((event.applyInfo?.type ?: return) < 3) { + // todo + return + } + val flag = try { + var reqs = requestGroupSystemMsgNew(10, 1) + val riskReqs = requestGroupSystemMsgNew(5, 2) + reqs = reqs + riskReqs + val req = reqs.firstOrNull() { + it.msg_time.get() == time + } + val seq = req?.msg_seq?.get() ?: time + if (applier == 0L) { + applier = req?.req_uin?.get() ?: 0L + } + "$seq;$groupCode;$applier" + } catch (err: Throwable) { + "$time;$groupCode;$applier" + } + LogCenter.log("邀请入群申请($groupCode): $applier") + if (!GlobalEventTransmitter.RequestTransmitter + .transGroupApply(time, applier, applierUid, "", groupCode, flag, GroupApplyType.GROUP_APPLY_ADD) + ) { + LogCenter.log("邀请入群申请推送失败!", Level.WARN) + } + } + } + } + + private suspend fun onInviteGroup(time: Long, msgHead: ResponseHead, body: MsgBody) { + val event = body.msgContent!!.decodeProtobuf() + val groupCode = event.groupCode + val invitorUid = event.inviterUid + val invitor = ContactHelper.getUinByUidAsync(invitorUid).toLong() + val uin = msgHead.receiver + LogCenter.log("邀请入群: $groupCode, 邀请者: \"$invitor\"") + val flag = try { + var reqs = requestGroupSystemMsgNew(10, 1) + val riskReqs = requestGroupSystemMsgNew(10, 2) + reqs = reqs + riskReqs + val req = reqs.firstOrNull { + it.msg_time.get() == time + } + val seq = req?.msg_seq?.get() ?: time + "$seq;$groupCode;$uin" + } catch (err: Throwable) { + "$time;$groupCode;$uin" + } + if (!GlobalEventTransmitter.RequestTransmitter + .transGroupApply(time, invitor, invitorUid, "", groupCode, flag, GroupApplyType.GROUP_APPLY_INVITE) + ) { + LogCenter.log("邀请入群推送失败!", Level.WARN) + } + } + + +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt b/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt index 47c3c56..d82a292 100644 --- a/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt +++ b/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt @@ -195,7 +195,7 @@ abstract class SimpleKernelMsgListener: IKernelMsgListener { } - override fun onMsgRecall(i2: Int, str: String?, j2: Long) { + override fun onMsgRecall(chatType: Int, tips: String?, msgId: Long) { } diff --git a/xposed/src/main/java/qq/service/msg/MessageHelper.kt b/xposed/src/main/java/qq/service/msg/MessageHelper.kt index 8249ac9..c66bb73 100644 --- a/xposed/src/main/java/qq/service/msg/MessageHelper.kt +++ b/xposed/src/main/java/qq/service/msg/MessageHelper.kt @@ -3,6 +3,7 @@ package qq.service.msg import com.tencent.qqnt.kernel.api.IKernelService import com.tencent.qqnt.kernel.nativeinterface.Contact import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull @@ -32,12 +33,28 @@ internal object MessageHelper: QQInterfaces() { return Result.success(info) } + suspend fun generateContact(record: MsgRecord): Contact { + val peerId = when (record.chatType) { + MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> record.senderUid + MsgConstant.KCHATTYPEGUILD -> record.channelId + else -> record.peerUin.toString() + } + return Contact(record.chatType, peerId, if (record.chatType == MsgConstant.KCHATTYPEGUILD) { + record.guildId + } else if(record.chatType == MsgConstant.KCHATTYPETEMPC2CFROMGROUP) { + val tempInfo = getTempChatInfo(record.chatType, peerId).getOrThrow() + tempInfo.groupCode + } else { + null + }) + } + suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact { val peerId = when (chatType) { MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> { - ContactHelper.getUidByUinAsync(id.toLong()) + if (id.startsWith("u_")) id + else ContactHelper.getUidByUinAsync(id.toLong()) } - else -> id } return if (chatType == MsgConstant.KCHATTYPEGUILD) { diff --git a/xposed/src/main/java/qq/service/msg/MsgConvertor.kt b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt index 28016f9..9d38347 100644 --- a/xposed/src/main/java/qq/service/msg/MsgConvertor.kt +++ b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt @@ -1,8 +1,10 @@ package qq.service.msg +import com.tencent.mobileqq.qroute.QRoute import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.msg.api.IMsgService import io.kritor.event.AtElement import io.kritor.event.Element import io.kritor.event.ElementKt @@ -21,10 +23,13 @@ import io.kritor.event.imageElement import io.kritor.event.jsonElement import io.kritor.event.locationElement import io.kritor.event.pokeElement +import io.kritor.event.replyElement import io.kritor.event.rpsElement import io.kritor.event.textElement import io.kritor.event.videoElement import io.kritor.event.voiceElement +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.shamrock.helper.ActionMsgException import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter @@ -40,6 +45,7 @@ import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER import qq.service.bdh.RichProtoSvc import qq.service.contact.ContactHelper +import kotlin.coroutines.resume typealias NtMessages = ArrayList typealias Convertor = suspend (MsgRecord, MsgElement) -> Result @@ -308,8 +314,22 @@ private object MsgConvertor { suspend fun convertReply(record: MsgRecord, element: MsgElement): Result { val reply = element.replyElement val elem = Element.newBuilder() - elem.setReply(io.kritor.event.replyElement { - this.messageId = reply.replayMsgId + elem.setReply(replyElement { + val msgSeq = reply.replayMsgSeq + val contact = MessageHelper.generateContact(record) + val sourceRecords = withTimeoutOrNull(3000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records -> + it.resume(records) + } + } + } + if (sourceRecords.isNullOrEmpty()) { + LogCenter.log("无法查询到回复的消息ID: seq = $msgSeq", Level.WARN) + this.messageId = reply.replayMsgId + } else { + this.messageId = sourceRecords.first().msgId + } }) return Result.success(elem.build()) } From 6c9b282c6aacf88c97e4bd8d6d65e7eee0b6470a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Sat, 16 Mar 2024 19:39:45 +0800 Subject: [PATCH 18/20] =?UTF-8?q?`Shamrock`:=20=E5=AE=9E=E7=8E=B0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .../main/java/kritor/service/GroupService.kt | 8 +- .../java/kritor/service/MessageService.kt | 324 +++++++ .../internals/GlobalEventTransmitter.kt | 14 +- .../src/main/java/qq/service/QQInterfaces.kt | 11 + .../main/java/qq/service/bdh/ResourceData.kt | 58 ++ .../src/main/java/qq/service/bdh/Transfer.kt | 135 +++ .../java/qq/service/contact/ContactExt.kt | 21 + .../java/qq/service/contact/ContactHelper.kt | 31 + .../main/java/qq/service/group/GroupHelper.kt | 102 ++- .../qq/service/internals/LineDevListener.kt | 14 + .../qq/service/internals/NTServiceFetcher.kt | 1 + .../service/kernel/SimpleKernelMsgListener.kt | 4 +- .../java/qq/service/lightapp/ArkAppInfo.kt | 40 + .../java/qq/service/lightapp/ArkMsgHelper.kt | 48 + .../java/qq/service/lightapp/LbsHelper.kt | 58 ++ .../java/qq/service/lightapp/MusicHelper.kt | 96 ++ .../main/java/qq/service/lightapp/Region.kt | 10 + .../java/qq/service/lightapp/WeatherHelper.kt | 78 ++ .../main/java/qq/service/msg/MessageData.kt | 71 ++ .../main/java/qq/service/msg/MessageHelper.kt | 149 ++++ .../main/java/qq/service/msg/MsgConvertor.kt | 33 +- .../java/qq/service/msg/MultiConvertor.kt | 275 ++++++ .../java/qq/service/msg/NtMsgConvertor.kt | 838 ++++++++++++++++++ .../qq/service/msg/ReqMessageConvertor.kt | 401 +++++++++ .../java/qq/service/ticket/TicketHelper.kt | 199 +++++ 25 files changed, 2966 insertions(+), 53 deletions(-) create mode 100644 xposed/src/main/java/qq/service/bdh/ResourceData.kt create mode 100644 xposed/src/main/java/qq/service/bdh/Transfer.kt create mode 100644 xposed/src/main/java/qq/service/contact/ContactExt.kt create mode 100644 xposed/src/main/java/qq/service/internals/LineDevListener.kt create mode 100644 xposed/src/main/java/qq/service/lightapp/ArkAppInfo.kt create mode 100644 xposed/src/main/java/qq/service/lightapp/ArkMsgHelper.kt create mode 100644 xposed/src/main/java/qq/service/lightapp/LbsHelper.kt create mode 100644 xposed/src/main/java/qq/service/lightapp/MusicHelper.kt create mode 100644 xposed/src/main/java/qq/service/lightapp/Region.kt create mode 100644 xposed/src/main/java/qq/service/lightapp/WeatherHelper.kt create mode 100644 xposed/src/main/java/qq/service/msg/MessageData.kt create mode 100644 xposed/src/main/java/qq/service/msg/MultiConvertor.kt create mode 100644 xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt create mode 100644 xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt create mode 100644 xposed/src/main/java/qq/service/ticket/TicketHelper.kt diff --git a/xposed/src/main/java/kritor/service/GroupService.kt b/xposed/src/main/java/kritor/service/GroupService.kt index 8e0d5b8..9563b50 100644 --- a/xposed/src/main/java/kritor/service/GroupService.kt +++ b/xposed/src/main/java/kritor/service/GroupService.kt @@ -188,13 +188,13 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase() ) } - GroupHelper.setGroupUniqueTitle(request.groupId, when(request.targetCase!!) { + GroupHelper.setGroupUniqueTitle(request.groupId.toString(), when(request.targetCase!!) { SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT .withDescription("target not set") ) - }, request.uniqueTitle) + }.toString(), request.uniqueTitle) return setGroupUniqueTitleResponse { } } @@ -253,13 +253,13 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase() @Grpc("GroupService", "GetGroupMemberInfo") override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse { - val memberInfo = GroupHelper.getTroopMemberInfoByUin(request.groupId, when(request.targetCase!!) { + val memberInfo = GroupHelper.getTroopMemberInfoByUin(request.groupId.toString(), when(request.targetCase!!) { GetGroupMemberInfoRequest.TargetCase.UIN -> request.uin GetGroupMemberInfoRequest.TargetCase.UID -> ContactHelper.getUinByUidAsync(request.uid).toLong() else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT .withDescription("target not set") ) - }).onFailure { + }.toString()).onFailure { throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member info").withCause(it)) }.getOrThrow() return getGroupMemberInfoResponse { diff --git a/xposed/src/main/java/kritor/service/MessageService.kt b/xposed/src/main/java/kritor/service/MessageService.kt index 3776912..efb3e16 100644 --- a/xposed/src/main/java/kritor/service/MessageService.kt +++ b/xposed/src/main/java/kritor/service/MessageService.kt @@ -1,7 +1,331 @@ package kritor.service +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.msg.api.IMsgService +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.message.ClearMessagesRequest +import io.kritor.message.ClearMessagesResponse +import io.kritor.message.GetForwardMessagesRequest +import io.kritor.message.GetForwardMessagesResponse +import io.kritor.message.GetHistoryMessageRequest +import io.kritor.message.GetHistoryMessageResponse +import io.kritor.message.GetMessageBySeqRequest +import io.kritor.message.GetMessageBySeqResponse +import io.kritor.message.GetMessageRequest +import io.kritor.message.GetMessageResponse import io.kritor.message.MessageServiceGrpcKt +import io.kritor.message.RecallMessageRequest +import io.kritor.message.RecallMessageResponse +import io.kritor.message.Scene +import io.kritor.message.SendMessageByResIdRequest +import io.kritor.message.SendMessageByResIdResponse +import io.kritor.message.SendMessageRequest +import io.kritor.message.SendMessageResponse +import io.kritor.message.clearMessagesResponse +import io.kritor.message.contact +import io.kritor.message.getForwardMessagesResponse +import io.kritor.message.getHistoryMessageResponse +import io.kritor.message.getMessageBySeqResponse +import io.kritor.message.getMessageResponse +import io.kritor.message.messageBody +import io.kritor.message.recallMessageResponse +import io.kritor.message.sendMessageByResIdResponse +import io.kritor.message.sendMessageResponse +import io.kritor.message.sender +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import protobuf.auto.toByteArray +import protobuf.message.ContentHead +import protobuf.message.Elem +import protobuf.message.MsgBody +import protobuf.message.PbSendMsgReq +import protobuf.message.RichText +import protobuf.message.RoutingHead +import protobuf.message.element.GeneralFlags +import protobuf.message.routing.C2C +import protobuf.message.routing.Grp +import qq.service.QQInterfaces +import qq.service.contact.longPeer +import qq.service.internals.NTServiceFetcher +import qq.service.msg.MessageHelper +import qq.service.msg.NtMsgConvertor +import qq.service.msg.toKritorEventMessages +import qq.service.msg.toKritorReqMessages +import qq.service.msg.toKritorResponseMessages +import kotlin.coroutines.resume +import kotlin.random.Random +import kotlin.random.nextUInt internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImplBase() { + override suspend fun sendMessage(request: SendMessageRequest): SendMessageResponse { + val contact = request.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + val uniseq = MessageHelper.generateMsgId(contact.chatType) + return sendMessageResponse { + this.messageId = MessageHelper.sendMessage(contact, NtMsgConvertor.convertToNtMsgs(contact, uniseq, request.elementsList), request.retryCount, uniseq).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withCause(it)) + }.getOrThrow() + } + } + + override suspend fun sendMessageByResId(request: SendMessageByResIdRequest): SendMessageByResIdResponse { + val contact = request.contact + val req = PbSendMsgReq( + routingHead = when (request.contact.scene) { + Scene.GROUP -> RoutingHead(grp = Grp(contact.longPeer().toUInt())) + Scene.FRIEND -> RoutingHead(c2c = C2C(contact.longPeer().toUInt())) + else -> RoutingHead(grp = Grp(contact.longPeer().toUInt())) + }, + contentHead = ContentHead(1, 0, 0, 0), + msgBody = MsgBody( + richText = RichText( + elements = arrayListOf( + Elem( + generalFlags = GeneralFlags( + longTextFlag = 1u, + longTextResid = request.resId + ) + ) + ) + ) + ), + msgSeq = Random.nextUInt(), + msgRand = Random.nextUInt(), + msgVia = 0u + ) + QQInterfaces.sendBuffer("MessageSvc.PbSendMsg", true, req.toByteArray()) + return sendMessageByResIdResponse { } + } + + override suspend fun clearMessages(request: ClearMessagesRequest): ClearMessagesResponse { + val contact = request.contact + val kernelService = NTServiceFetcher.kernelService + val sessionService = kernelService.wrapperSession + val service = sessionService.msgService + val chatType = when(contact.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + } + service.clearMsgRecords(Contact(chatType, contact.peer, contact.subPeer), null) + return clearMessagesResponse { } + } + + override suspend fun recallMessage(request: RecallMessageRequest): RecallMessageResponse { + val contact = request.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + val kernelService = NTServiceFetcher.kernelService + val sessionService = kernelService.wrapperSession + val service = sessionService.msgService + service.recallMsg(contact, arrayListOf(request.messageId)) { code, msg -> + if (code != 0) { + LogCenter.log("消息撤回失败: $code:$msg", Level.WARN) + } + } + + return recallMessageResponse {} + } + + override suspend fun getForwardMessages(request: GetForwardMessagesRequest): GetForwardMessagesResponse { + return getForwardMessagesResponse { + MessageHelper.getForwardMsg(request.resId).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withCause(it)) + }.getOrThrow().forEach { detail -> + messages.add(messageBody { + val peer = when (scene) { + Scene.GROUP -> detail.groupId.toString() + Scene.FRIEND -> detail.sender.userId.toString() + else -> detail.peerId.toString() + } + + this.time = detail.time + this.scene = when(detail.msgType) { + MsgConstant.KCHATTYPEC2C -> Scene.FRIEND + MsgConstant.KCHATTYPEGROUP -> Scene.GROUP + MsgConstant.KCHATTYPEGUILD -> Scene.GUILD + MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Scene.STRANGER_FROM_GROUP + MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN -> Scene.NEARBY + else -> Scene.STRANGER + } + this.messageId = detail.qqMsgId + this.messageSeq = detail.msgSeq + this.contact = contact { + this.scene = scene + this.peer = peer + } + this.sender = sender { + this.uin = detail.sender.userId + this.nick = detail.sender.nickName + this.uid = detail.sender.uid + } + detail.message?.elements?.toKritorResponseMessages(Contact(detail.msgType, peer, null))?.let { + this.elements.addAll(it) + } + }) + } + } + } + + override suspend fun getMessage(request: GetMessageRequest): GetMessageResponse { + val contact = request.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + val msg: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsByMsgId(contact, arrayListOf(request.messageId)) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found")) + + return getMessageResponse { + this.message = messageBody { + this.messageId = msg.msgId + this.scene = request.contact.scene + this.contact = request.contact + this.sender = sender { + this.uin = msg.senderUin + this.nick = msg.sendNickName ?: "" + this.uid = msg.senderUid ?: "" + } + this.messageSeq = msg.msgSeq + this.elements.addAll(msg.elements.toKritorReqMessages(contact)) + } + } + } + + override suspend fun getMessageBySeq(request: GetMessageBySeqRequest): GetMessageBySeqResponse { + val contact = request.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + val msg: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsBySeqAndCount(contact, request.messageSeq, 1, true) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found")) + + return getMessageBySeqResponse { + this.message = messageBody { + this.messageId = msg.msgId + this.scene = request.contact.scene + this.contact = request.contact + this.sender = sender { + this.uin = msg.senderUin + this.nick = msg.sendNickName ?: "" + this.uid = msg.senderUid ?: "" + } + this.messageSeq = msg.msgSeq + this.elements.addAll(msg.elements.toKritorReqMessages(contact)) + } + } + } + + override suspend fun getHistoryMessage(request: GetHistoryMessageRequest): GetHistoryMessageResponse { + val contact = request.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + val msgs: List = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgs(contact, request.startMessageId, request.count, true) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Messages not found")) + + return getHistoryMessageResponse { + msgs.forEach { + messages.add(messageBody { + this.messageId = it.msgId + this.scene = request.contact.scene + this.contact = request.contact + this.sender = sender { + this.uin = it.senderUin + this.nick = it.sendNickName ?: "" + this.uid = it.senderUid ?: "" + } + this.messageSeq = it.msgSeq + this.elements.addAll(it.elements.toKritorReqMessages(contact)) + }) + } + } + } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt index 9c18f34..a90a273 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt @@ -41,7 +41,7 @@ import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import qq.service.QQInterfaces -import qq.service.msg.toKritorMessages +import qq.service.msg.toKritorEventMessages internal object GlobalEventTransmitter: QQInterfaces() { private val messageEventFlow by lazy { @@ -80,7 +80,7 @@ internal object GlobalEventTransmitter: QQInterfaces() { this.uid = record.senderUid this.nick = record.sendNickName } - this.elements.addAll(elements.toKritorMessages(record)) + this.elements.addAll(elements.toKritorEventMessages(record)) }) return true } @@ -104,7 +104,7 @@ internal object GlobalEventTransmitter: QQInterfaces() { this.uid = record.senderUid this.nick = record.sendNickName } - this.elements.addAll(elements.toKritorMessages(record)) + this.elements.addAll(elements.toKritorEventMessages(record)) }) return true } @@ -130,7 +130,7 @@ internal object GlobalEventTransmitter: QQInterfaces() { this.uid = record.senderUid this.nick = record.sendNickName } - this.elements.addAll(elements.toKritorMessages(record)) + this.elements.addAll(elements.toKritorEventMessages(record)) }) return true } @@ -146,15 +146,15 @@ internal object GlobalEventTransmitter: QQInterfaces() { this.messageSeq = record.msgSeq this.contact = contact { this.scene = scene - this.peer = record.channelId.toString() - this.subPeer = record.guildId + this.peer = record.guildId ?: "" + this.subPeer = record.channelId ?: "" } this.sender = sender { this.uin = record.senderUin this.uid = record.senderUid this.nick = record.sendNickName } - this.elements.addAll(elements.toKritorMessages(record)) + this.elements.addAll(elements.toKritorEventMessages(record)) }) return true } diff --git a/xposed/src/main/java/qq/service/QQInterfaces.kt b/xposed/src/main/java/qq/service/QQInterfaces.kt index 2e6b047..22c3169 100644 --- a/xposed/src/main/java/qq/service/QQInterfaces.kt +++ b/xposed/src/main/java/qq/service/QQInterfaces.kt @@ -83,6 +83,17 @@ abstract class QQInterfaces { app.sendToService(to) } + fun sendBuffer( + cmd: String, + isProto: Boolean, + data: ByteArray, + ) { + val toServiceMsg = createToServiceMsg(cmd) + toServiceMsg.putWupBuffer(data) + toServiceMsg.addAttribute("req_pb_protocol_flag", isProto) + sendToServiceMsg(toServiceMsg) + } + @DelicateCoroutinesApi suspend fun sendBufferAW( cmd: String, diff --git a/xposed/src/main/java/qq/service/bdh/ResourceData.kt b/xposed/src/main/java/qq/service/bdh/ResourceData.kt new file mode 100644 index 0000000..5b02d6c --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/ResourceData.kt @@ -0,0 +1,58 @@ +package qq.service.bdh + +import com.tencent.mobileqq.data.MessageRecord +import java.io.File + +internal enum class ContactType { + TROOP, + PRIVATE, +} + +internal interface TransTarget { + val id: String + val type: ContactType + + val mRec: MessageRecord? +} + +internal class Troop( + override val id: String, + override val mRec: MessageRecord? = null +): TransTarget { + override val type: ContactType = ContactType.TROOP +} + +internal class Private( + override val id: String, + override val mRec: MessageRecord? = null +): TransTarget { + override val type: ContactType = ContactType.PRIVATE +} + +internal enum class ResourceType { + Picture, + Video, + Voice +} + +internal interface Resource { + val type: ResourceType +} + +internal data class PictureResource( + val src: File +): Resource { + override val type = ResourceType.Picture +} + +internal data class VideoResource( + val src: File, val thumb: File +): Resource { + override val type = ResourceType.Video +} + +internal data class VoiceResource( + val src: File +): Resource { + override val type = ResourceType.Voice +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/bdh/Transfer.kt b/xposed/src/main/java/qq/service/bdh/Transfer.kt new file mode 100644 index 0000000..f3003cd --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/Transfer.kt @@ -0,0 +1,135 @@ +package qq.service.bdh + +import com.tencent.mobileqq.data.MessageForShortVideo +import com.tencent.mobileqq.data.MessageRecord +import com.tencent.mobileqq.transfile.FileMsg +import com.tencent.mobileqq.transfile.TransferRequest +import moe.fuqiuluo.shamrock.utils.MD5 +import qq.service.bdh.ResourceType.* +import java.io.File + +internal object Transfer: FileTransfer() { + private val ROUTE = mapOf Boolean>>( + ContactType.TROOP to mapOf( + Picture to { uploadGroupPic(id, (it as PictureResource).src, mRec) }, + Voice to { uploadGroupVoice(id, (it as VoiceResource).src) }, + Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) }, + + ), + ContactType.PRIVATE to mapOf( + Picture to { uploadC2CPic(id, (it as PictureResource).src, mRec) }, + Voice to { uploadC2CVoice(id, (it as VoiceResource).src) }, + Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) }, + ) + ) + + suspend fun uploadC2CVideo( + userId: String, + file: File, + thumb: File, + wait: Boolean = true + ): Boolean { + return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_C2C, BUSI_TYPE_SHORT_VIDEO, wait) { + it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4 + it.mRec = MessageForShortVideo().also { + it.busiType = BUSI_TYPE_SHORT_VIDEO + } + it.mThumbPath = thumb.absolutePath + it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath) + } + } + + suspend fun uploadGroupVideo( + groupId: String, + file: File, + thumb: File, + wait: Boolean = true + ): Boolean { + return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_TROOP, BUSI_TYPE_SHORT_VIDEO, wait) { + it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4 + it.mRec = MessageForShortVideo().also { + it.busiType = BUSI_TYPE_SHORT_VIDEO + } + it.mThumbPath = thumb.absolutePath + it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath) + } + } + + suspend fun uploadC2CVoice( + userId: String, + file: File, + wait: Boolean = true + ): Boolean { + return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) { + it.mPttUploadPanel = 3 + it.mPttCompressFinish = true + it.mIsPttPreSend = true + } + } + + suspend fun uploadGroupVoice( + groupId: String, + file: File, + wait: Boolean = true + ): Boolean { + return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) { + it.mPttUploadPanel = 3 + it.mPttCompressFinish = true + it.mIsPttPreSend = true + } + } + + suspend fun uploadC2CPic( + peerId: String, + file: File, + record: MessageRecord? = null, + wait: Boolean = true + ): Boolean { + return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) { + val picUpExtraInfo = TransferRequest.PicUpExtraInfo() + picUpExtraInfo.mIsRaw = false + picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY + it.mPicSendSource = 8 + it.mExtraObj = picUpExtraInfo + it.mIsPresend = true + it.delayShowProgressTimeInMs = 2000 + it.mRec = record + } + } + + suspend fun uploadGroupPic( + groupId: String, + file: File, + record: MessageRecord? = null, + wait: Boolean = true + ): Boolean { + return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) { + val picUpExtraInfo = TransferRequest.PicUpExtraInfo() + picUpExtraInfo.mIsRaw = false + picUpExtraInfo.mUinType = FileMsg.UIN_TROOP + it.mPicSendSource = 8 + it.delayShowProgressTimeInMs = 2000 + it.mExtraObj = picUpExtraInfo + it.mRec = record + } + } + + operator fun get(contactType: ContactType, resourceType: ResourceType): suspend TransTarget.(Resource) -> Boolean { + return (ROUTE[contactType] ?: error("unsupported contact type: $contactType"))[resourceType] + ?: error("Unsupported resource type: $resourceType") + } +} + +internal suspend infix fun TransferTaskBuilder.trans(res: Resource): Boolean { + return Transfer[contact.type, res.type](contact, res) +} + +internal class TransferTaskBuilder { + lateinit var contact: TransTarget +} + +internal infix fun Transfer.with(contact: TransTarget): TransferTaskBuilder { + return TransferTaskBuilder().also { + it.contact = contact + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/contact/ContactExt.kt b/xposed/src/main/java/qq/service/contact/ContactExt.kt new file mode 100644 index 0000000..b2b7962 --- /dev/null +++ b/xposed/src/main/java/qq/service/contact/ContactExt.kt @@ -0,0 +1,21 @@ +package qq.service.contact + +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import io.kritor.message.Scene + +suspend fun Contact.longPeer(): Long { + return when(this.chatType) { + MsgConstant.KCHATTYPEGROUP -> peerUid.toLong() + MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> if (peerUid.startsWith("u_")) ContactHelper.getUinByUidAsync(peerUid).toLong() else peerUid.toLong() + else -> 0L + } +} + +suspend fun io.kritor.message.Contact.longPeer(): Long { + return when(this.scene) { + Scene.GROUP -> peer.toLong() + Scene.FRIEND, Scene.STRANGER, Scene.STRANGER_FROM_GROUP -> if (peer.startsWith("u_")) ContactHelper.getUinByUidAsync(peer).toLong() else peer.toLong() + else -> 0L + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/contact/ContactHelper.kt b/xposed/src/main/java/qq/service/contact/ContactHelper.kt index a516495..4bc7c74 100644 --- a/xposed/src/main/java/qq/service/contact/ContactHelper.kt +++ b/xposed/src/main/java/qq/service/contact/ContactHelper.kt @@ -7,11 +7,15 @@ import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_SELF_UIN import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_TARGET_UIN import com.tencent.mobileqq.profilecard.api.IProfileProtocolService import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver +import com.tencent.protofile.join_group_link.join_group_link import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import moe.fuqiuluo.shamrock.tools.slice import qq.service.internals.NTServiceFetcher import qq.service.QQInterfaces +import tencent.im.oidb.cmd0x11b2.oidb_0x11b2 +import tencent.im.oidb.oidb_sso import kotlin.coroutines.resume internal object ContactHelper: QQInterfaces() { @@ -177,4 +181,31 @@ internal object ContactHelper: QQInterfaces() { } }[peerId]!! } + + suspend fun getSharePrivateArkMsg(peerId: Long): String { + val reqBody = oidb_0x11b2.BusinessCardV3Req() + reqBody.uin.set(peerId) + reqBody.jump_url.set("mqqapi://card/show_pslcard?src_type=internal&source=sharecard&version=1&uin=$peerId") + + val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11ca_0", 4790, 0, reqBody.toByteArray()) + ?: error("unable to fetch contact ark_json_text") + + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidb_0x11b2.BusinessCardV3Rsp() + rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + return rsp.signed_ark_msg.get() + } + + suspend fun getShareTroopArkMsg(groupId: Long): String { + val reqBody = join_group_link.ReqBody() + reqBody.get_ark.set(true) + reqBody.type.set(1) + reqBody.group_code.set(groupId) + val fromServiceMsg = sendBufferAW("GroupSvc.JoinGroupLink", true, reqBody.toByteArray()) + ?: error("unable to fetch contact ark_json_text") + val body = join_group_link.RspBody() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + return body.signed_ark.get().toStringUtf8() + } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/group/GroupHelper.kt b/xposed/src/main/java/qq/service/group/GroupHelper.kt index 7062b6c..95436fa 100644 --- a/xposed/src/main/java/qq/service/group/GroupHelper.kt +++ b/xposed/src/main/java/qq/service/group/GroupHelper.kt @@ -42,6 +42,7 @@ import java.lang.reflect.Method import java.lang.reflect.Modifier import java.nio.ByteBuffer import kotlin.coroutines.resume +import kotlin.time.Duration.Companion.seconds internal object GroupHelper: QQInterfaces() { private val RefreshTroopMemberInfoLock by lazy { Mutex() } @@ -99,6 +100,61 @@ internal object GroupHelper: QQInterfaces() { return Result.success(troopList) } + suspend fun getTroopMemberInfoByUinV2( + groupId: String, + uin: String, + refresh: Boolean = false + ): Result { + val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") + var info = service.getTroopMember(groupId, uin) + if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) { + info = requestTroopMemberInfo(service, groupId, uin, timeout = 2000).getOrNull() + } + if (info == null) { + info = getTroopMemberInfoByUinViaNt(groupId, uin, timeout = 2000L).getOrNull()?.let { + TroopMemberInfo().apply { + troopnick = it.cardName + friendnick = it.nick + } + } + } + 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 fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray(), timeout = 2.seconds) + if (fromServiceMsg != null && fromServiceMsg.wupBuffer != null) { + val rsp = group_member_info.RspBody() + rsp.mergeFrom(fromServiceMsg.wupBuffer.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 { + Result.failure(Exception("获取群成员信息失败")) + } + } + private suspend fun requestGroupInfo( service: ITroopInfoService ): Boolean { @@ -318,12 +374,12 @@ internal object GroupHelper: QQInterfaces() { } } - suspend fun setGroupUniqueTitle(groupId: Long, userId: Long, title: String) { + suspend fun setGroupUniqueTitle(groupId: String, userId: String, title: String) { val localMemberInfo = getTroopMemberInfoByUin(groupId, userId, true).getOrThrow() val req = Oidb_0x8fc.ReqBody() - req.uint64_group_code.set(groupId) + req.uint64_group_code.set(groupId.toLong()) val memberInfo = Oidb_0x8fc.MemberInfo() - memberInfo.uint64_uin.set(userId) + memberInfo.uint64_uin.set(userId.toLong()) memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty { localMemberInfo.troopremark.ifNullOrEmpty { "" } })) @@ -468,13 +524,13 @@ internal object GroupHelper: QQInterfaces() { } suspend fun getTroopMemberInfoByUin( - groupId: Long, - uin: Long, + groupId: String, + uin: String, refresh: Boolean = false ): Result { val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") - var info = service.getTroopMember(groupId.toString(), uin.toString()) - if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) { + var info = service.getTroopMember(groupId, uin) + if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) { info = requestTroopMemberInfo(service, groupId, uin).getOrNull() } if (info == null) { @@ -488,8 +544,8 @@ internal object GroupHelper: QQInterfaces() { try { if (info != null && (info.alias == null || info.alias.isBlank())) { val req = group_member_info.ReqBody() - req.uint64_group_code.set(groupId) - req.uint64_uin.set(uin) + 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) @@ -523,8 +579,8 @@ internal object GroupHelper: QQInterfaces() { } suspend fun getTroopMemberInfoByUinViaNt( - groupId: Long, - qq: Long, + groupId: String, + qq: String, timeout: Long = 5000L ): Result { return runCatching { @@ -533,13 +589,13 @@ internal object GroupHelper: QQInterfaces() { val groupService = sessionService.groupService val info = withTimeoutOrNull(timeout) { suspendCancellableCoroutine { - groupService.getTransferableMemberInfo(groupId) { code, _, data -> + groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data -> if (code != 0) { it.resume(null) return@getTransferableMemberInfo } data.forEach { (_, info) -> - if (info.uin == qq) { + if (info.uin == qq.toLong()) { it.resume(info) return@forEach } @@ -556,21 +612,18 @@ internal object GroupHelper: QQInterfaces() { } } - private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result { + private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String, memberUin: String, timeout: Long = 10_000): Result { val info = RefreshTroopMemberInfoLock.withLock { - val groupIdStr = groupId.toString() - val memberUinStr = memberUin.toString() - - service.deleteTroopMember(groupIdStr, memberUinStr) + service.deleteTroopMember(groupId, memberUin) requestMemberInfoV2(groupId, memberUin) requestMemberInfo(groupId, memberUin) withTimeoutOrNull(timeout) { - while (!service.isMemberInCache(groupIdStr, memberUinStr)) { + while (!service.isMemberInCache(groupId, memberUin)) { delay(200) } - return@withTimeoutOrNull service.getTroopMember(groupIdStr, memberUinStr) + return@withTimeoutOrNull service.getTroopMember(groupId, memberUin) } } return if (info != null) { @@ -580,7 +633,7 @@ internal object GroupHelper: QQInterfaces() { } } - private fun requestMemberInfo(groupId: Long, memberUin: Long) { + private fun requestMemberInfo(groupId: String, memberUin: String) { val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER) if (!::METHOD_REQ_MEMBER_INFO.isInitialized) { @@ -592,10 +645,10 @@ internal object GroupHelper: QQInterfaces() { } } - METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId, memberUin) + METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId.toLong(), memberUin.toLong()) } - private fun requestMemberInfoV2(groupId: Long, memberUin: Long) { + private fun requestMemberInfoV2(groupId: String, memberUin: String) { val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER) if (!::METHOD_REQ_MEMBER_INFO_V2.isInitialized) { @@ -607,7 +660,8 @@ internal object GroupHelper: QQInterfaces() { } } - METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler, groupId.toString(), groupUin2GroupCode(groupId).toString(), arrayListOf(memberUin.toString())) + METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler, + groupId, groupUin2GroupCode(groupId.toLong()).toString(), arrayListOf(memberUin)) } private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String): Result> { diff --git a/xposed/src/main/java/qq/service/internals/LineDevListener.kt b/xposed/src/main/java/qq/service/internals/LineDevListener.kt new file mode 100644 index 0000000..41698c9 --- /dev/null +++ b/xposed/src/main/java/qq/service/internals/LineDevListener.kt @@ -0,0 +1,14 @@ +package qq.service.internals + +import com.tencent.qqnt.kernel.nativeinterface.DevInfo +import com.tencent.qqnt.kernel.nativeinterface.KickedInfo +import qq.service.kernel.SimpleKernelMsgListener +import java.util.ArrayList + +object LineDevListener: SimpleKernelMsgListener() { + override fun onKickedOffLine(kickedInfo: KickedInfo) { + } + + override fun onLineDev(devs: ArrayList) { + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/internals/NTServiceFetcher.kt b/xposed/src/main/java/qq/service/internals/NTServiceFetcher.kt index 5c6f8c8..8e28fd7 100644 --- a/xposed/src/main/java/qq/service/internals/NTServiceFetcher.kt +++ b/xposed/src/main/java/qq/service/internals/NTServiceFetcher.kt @@ -58,6 +58,7 @@ internal object NTServiceFetcher { try { LogCenter.log("Register MSG listener successfully.") msgService.addMsgListener(AioListener) + msgService.addMsgListener(LineDevListener) // 接口缺失 暂不使用 //groupService.addKernelGroupListener(GroupEventListener) diff --git a/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt b/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt index d82a292..d15ed47 100644 --- a/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt +++ b/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt @@ -155,11 +155,11 @@ abstract class SimpleKernelMsgListener: IKernelMsgListener { } - override fun onKickedOffLine(kickedInfo: KickedInfo?) { + override fun onKickedOffLine(kickedInfo: KickedInfo) { } - override fun onLineDev(arrayList: ArrayList?) { + override fun onLineDev(devs: ArrayList) { } diff --git a/xposed/src/main/java/qq/service/lightapp/ArkAppInfo.kt b/xposed/src/main/java/qq/service/lightapp/ArkAppInfo.kt new file mode 100644 index 0000000..d69ae65 --- /dev/null +++ b/xposed/src/main/java/qq/service/lightapp/ArkAppInfo.kt @@ -0,0 +1,40 @@ +package qq.service.lightapp + +sealed class ArkAppInfo( + val appId: Long, + val version: String, + val packageName: String, + val signature: String, + val miniAppId: Long = 0, + val appName: String = "" +) { + data object QQMusic: ArkAppInfo( + appId = 100497308, + version = "0.0.0", + packageName = "com.tencent.qqmusic", + signature = "cbd27cd7c861227d013a25b2d10f0799" + ) + data object NetEaseMusic: ArkAppInfo( + appId = 100495085, + version = "0.0.0", + packageName = "com.netease.cloudmusic", + signature = "da6b069da1e2982db3e386233f68d76d" + ) + + data object DanMaKu: ArkAppInfo( + appId = 100951776, + version = "0.0.0", + packageName = "tv.danmaku.bili", + signature = "7194d531cbe7960a22007b9f6bdaa38b", + miniAppId = 1109937557, + appName = "哔哩哔哩" + ) + + data object Docs: ArkAppInfo( + appId = 0, + version = "0.0.0", + packageName = "", + signature = "f3da3147654d9a21f3237b88f20dce9c", + miniAppId = 1108338344 + ) +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/lightapp/ArkMsgHelper.kt b/xposed/src/main/java/qq/service/lightapp/ArkMsgHelper.kt new file mode 100644 index 0000000..1d185f1 --- /dev/null +++ b/xposed/src/main/java/qq/service/lightapp/ArkMsgHelper.kt @@ -0,0 +1,48 @@ +package qq.service.lightapp + +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import qq.service.QQInterfaces +import qq.service.contact.longPeer +import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77 + +internal object ArkMsgHelper: QQInterfaces() { + suspend fun tryShareMusic( + contact: Contact, + msgId: Long, + arkAppInfo: ArkAppInfo, + title: String, + singer: String, + jumpUrl: String, + previewUrl: String, + musicUrl: String, + ) { + val req = oidb_cmd0xb77.ReqBody() + req.appid.set(arkAppInfo.appId) + req.app_type.set(1) + req.msg_style.set(4) + req.client_info.set(oidb_cmd0xb77.ClientInfo().also { + it.platform.set(1) + it.sdk_version.set(arkAppInfo.version) + it.android_package_name.set(arkAppInfo.packageName) + it.android_signature.set(arkAppInfo.signature) + }) + req.ext_info.set(oidb_cmd0xb77.ExtInfo().also { + it.msg_seq.set(msgId) + }) + req.recv_uin.set(contact.longPeer()) + req.rich_msg_body.set(oidb_cmd0xb77.RichMsgBody().also { + it.title.set(title) + it.summary.set(singer) + it.url.set(jumpUrl) + it.picture_url.set(previewUrl) + it.music_url.set(musicUrl) + }) + when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> req.send_type.set(1) + MsgConstant.KCHATTYPEC2C -> req.send_type.set(0) + else -> error("不支持该聊天类型发送音乐分享: chatType: ${contact.chatType}") + } + sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray()) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/lightapp/LbsHelper.kt b/xposed/src/main/java/qq/service/lightapp/LbsHelper.kt new file mode 100644 index 0000000..2ac7abc --- /dev/null +++ b/xposed/src/main/java/qq/service/lightapp/LbsHelper.kt @@ -0,0 +1,58 @@ +package qq.service.lightapp + +import com.tencent.biz.map.trpcprotocol.LbsSendInfo +import com.tencent.proto.lbsshare.LBSShare +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import moe.fuqiuluo.shamrock.helper.IllegalParamsException +import moe.fuqiuluo.shamrock.tools.slice +import qq.service.QQInterfaces +import qq.service.contact.longPeer +import kotlin.math.roundToInt + +internal object LbsHelper: QQInterfaces() { + suspend fun tryShareLocation(contact: Contact, lat: Double, lon: Double): Result { + val req = LbsSendInfo.SendMessageReq() + req.uint64_peer_account.set(contact.longPeer()) + when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> req.enum_relation_type.set(1) + MsgConstant.KCHATTYPEC2C -> req.enum_relation_type.set(0) + else -> error("Not supported chat type: $contact") + } + req.str_name.set("位置分享") + req.str_address.set(getAddressWithLonLat(lat, lon).onFailure { + return Result.failure(it) + }.getOrNull()) + req.str_lat.set(lat.toString()) + req.str_lng.set(lon.toString()) + sendBuffer("trpc.qq_lbs.qq_lbs_ark.LocationArk.SsoSendMessage", true, req.toByteArray()) + return Result.success(Unit) + } + + suspend fun getAddressWithLonLat(lat: Double, lon: Double): Result { + if (lat > 90 || lat < 0) { + return Result.failure(IllegalParamsException("纬度大小错误")) + } + if (lon > 180 || lon < 0) { + return Result.failure(IllegalParamsException("经度大小错误")) + } + val latO = (lat * 1000000).roundToInt() + val lngO = (lon * 1000000).roundToInt() + val req = LBSShare.LocationReq() + req.lat.set(latO) + req.lng.set(lngO) + req.coordinate.set(1) + req.keyword.set("") + req.category.set("") + req.page.set(0) + req.count.set(20) + req.requireMyLbs.set(1) + req.imei.set("") + val fromServiceMsg = sendBufferAW("LbsShareSvr.location", true, req.toByteArray()) + ?: return Result.failure(Exception("获取位置失败")) + val resp = LBSShare.LocationResp() + resp.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val location = resp.mylbs + return Result.success(location.addr.get()) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/lightapp/MusicHelper.kt b/xposed/src/main/java/qq/service/lightapp/MusicHelper.kt new file mode 100644 index 0000000..349915e --- /dev/null +++ b/xposed/src/main/java/qq/service/lightapp/MusicHelper.kt @@ -0,0 +1,96 @@ +package qq.service.lightapp + +import com.tencent.qqnt.kernel.nativeinterface.Contact +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import kotlinx.serialization.json.Json +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.GlobalClient +import moe.fuqiuluo.shamrock.tools.asInt +import moe.fuqiuluo.shamrock.tools.asJsonArray +import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.tools.asStringOrNull +import moe.fuqiuluo.shamrock.utils.MD5 + +internal object MusicHelper { + suspend fun tryShare163MusicById(contact: Contact, msgId: Long, id: String): Boolean { + try { + val respond = GlobalClient.get("https://music.163.com/api/song/detail/?id=$id&ids=[$id]") + val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songs"].asJsonArray.first().asJsonObject + val name = songInfo["name"].asString + val title = songInfo["name"].asString + val singerName = songInfo["artists"].asJsonArray.first().asJsonObject["name"].asString + val previewUrl = songInfo["album"].asJsonObject["picUrl"].asString + val playUrl = "https://music.163.com/song/media/outer/url?id=$id.mp3" + val jumpUrl = "https://music.163.com/#/song?id=$id" + ArkMsgHelper.tryShareMusic( + contact, + msgId, + ArkAppInfo.NetEaseMusic, + title.ifBlank { name }, + singerName, + jumpUrl, + previewUrl, + playUrl + ) + return true + } catch (e: Throwable) { + LogCenter.log(e.stackTraceToString(), Level.ERROR) + } + return false + } + + suspend fun tryShareQQMusicById(contact: Contact, msgId: Long, id: String): Boolean { + try { + val respond = GlobalClient.get("https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:$id},%22module%22:%22music.pf_song_detail_svr%22}}") + val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songinfo"].asJsonObject + if (songInfo["code"].asInt != 0) { + LogCenter.log("获取QQ音乐($id)的歌曲信息失败。") + return false + } else { + val data = songInfo["data"].asJsonObject + val trackInfo = data["track_info"].asJsonObject + val mid = trackInfo["mid"].asString + val previewMid = trackInfo["album"].asJsonObject["mid"].asString + val singerMid = trackInfo["singer"].asJsonArrayOrNull?.let { + it[0].asJsonObject["mid"].asStringOrNull + } ?: "" + val name = trackInfo["name"].asString + val title = trackInfo["title"].asString + val singerName = trackInfo["singer"].asJsonArray.first().asJsonObject["name"].asString + val vs = trackInfo["vs"].asJsonArrayOrNull?.let { + it[0].asStringOrNull + } ?: "" + val code = MD5.getMd5Hex("${mid}q;z(&l~sdf2!nK".toByteArray()).substring(0 .. 4).uppercase() + val playUrl = "http://c6.y.qq.com/rsc/fcgi-bin/fcg_pyq_play.fcg?songid=&songmid=$mid&songtype=1&fromtag=50&uin=&code=$code" + val previewUrl = if (vs.isNotEmpty()) { + "http://y.gtimg.cn/music/photo_new/T062R150x150M000$vs}.jpg" + } else if (previewMid.isNotEmpty()) { + "http://y.gtimg.cn/music/photo_new/T002R150x150M000$previewMid.jpg" + } else if (singerMid.isNotEmpty()){ + "http://y.gtimg.cn/music/photo_new/T001R150x150M000$singerMid.jpg" + } else { + "" + } + val jumpUrl = "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=${mid}&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" + ArkMsgHelper.tryShareMusic( + contact, + msgId, + ArkAppInfo.QQMusic, + title.ifBlank { name }, + singerName, + jumpUrl, + previewUrl, + playUrl + ) + return true + } + } catch (e: Throwable) { + LogCenter.log(e.stackTraceToString(), Level.ERROR) + } + return false + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/lightapp/Region.kt b/xposed/src/main/java/qq/service/lightapp/Region.kt new file mode 100644 index 0000000..0e07776 --- /dev/null +++ b/xposed/src/main/java/qq/service/lightapp/Region.kt @@ -0,0 +1,10 @@ +package qq.service.lightapp + +import kotlinx.serialization.Serializable + +@Serializable +internal data class Region( + val adcode: Int, + val province: String?, + val city: String? +) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/lightapp/WeatherHelper.kt b/xposed/src/main/java/qq/service/lightapp/WeatherHelper.kt new file mode 100644 index 0000000..161888a --- /dev/null +++ b/xposed/src/main/java/qq/service/lightapp/WeatherHelper.kt @@ -0,0 +1,78 @@ +package qq.service.lightapp + +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.url +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.http.encodeURLQueryComponent +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.GlobalClient +import moe.fuqiuluo.shamrock.tools.GlobalJson +import moe.fuqiuluo.shamrock.tools.asInt +import moe.fuqiuluo.shamrock.tools.asJsonArray +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asStringOrNull +import qq.service.QQInterfaces +import qq.service.ticket.TicketHelper + +internal object WeatherHelper: QQInterfaces() { + suspend fun fetchWeatherCard(code: Int): Result { + val cookie = TicketHelper.getCookie("mp.qq.com") + val resp = GlobalClient.get("https://weather.mp.qq.com/page/poster?_wv=2&&_wwv=4&adcode=$code") { + header("Cookie", cookie) + } + + if (resp.status != HttpStatusCode.OK) { + LogCenter.log("fetchWeatherCard: error: ${resp.status}, cookie: $cookie", Level.ERROR) + return Result.failure(Exception("search city failed")) + } + + val textJson = resp.bodyAsText() + .replace("\n", "") + .split("window.__INITIAL_STATE__ =")[1] + .split("};")[0].trim() + "}" + + //LogCenter.log(textJson) + + return Result.success(Json.parseToJsonElement(textJson).asJsonObject) + } + + suspend fun searchCity(query: String): Result> { + val pskey = TicketHelper.getPSKey(app.currentAccountUin, "mp.qq.com") ?: "" + val cookie = TicketHelper.getCookie("mp.qq.com") + val gtk = TicketHelper.getCSRF(pskey) + val resp = GlobalClient.get { + url("https://weather.mp.qq.com/trpc/weather/SearchRegions?g_tk=$gtk&key=${query.encodeURLQueryComponent()}&offset=0&count=25") + header("Cookie", cookie) + } + + if (resp.status != HttpStatusCode.OK) { + LogCenter.log("GetWeatherCityCode: error: ${resp.status}, cookie: $cookie, bkn: $gtk", Level.ERROR) + return Result.failure(Exception("search city failed")) + } + + val json = GlobalJson.parseToJsonElement(resp.bodyAsText()).asJsonObject + + + val cnt = json["totalCount"].asInt + if (cnt == 0) { + return Result.success(emptyList()) + } + + val regions = json["regions"].asJsonArray.map { + val region = it.asJsonObject + Region( + region["adcode"].asInt, + region["province"].asStringOrNull, + region["city"].asStringOrNull + ) + } + + return Result.success(regions) + } + +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MessageData.kt b/xposed/src/main/java/qq/service/msg/MessageData.kt new file mode 100644 index 0000000..6df7a32 --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/MessageData.kt @@ -0,0 +1,71 @@ +package qq.service.msg + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import protobuf.message.RichText + +@Serializable +internal data class MessageResult( + @SerialName("message_id") val msgId: Int, + @SerialName("time") val time: Long +) + +@Serializable +internal data class UploadForwardMessageResult( + @SerialName("res_id") val resId: String, + @SerialName("filename") val filename: String, + @SerialName("summary") val summary: String, + @SerialName("desc") val desc: String, +) + +@Serializable +internal data class SendForwardMessageResult( + @SerialName("message_id") val msgId: Int, + @SerialName("res_id") val resId: String, + @SerialName("forward_id") val forwardId: String = resId +) + +@Serializable +internal data class MessageDetail( + @SerialName("time") val time: Int, + @SerialName("message_type") val msgType: Int, + @SerialName("message_id") val msgId: Int, + @SerialName("message_id_qq") val qqMsgId: Long, + @SerialName("message_seq") val msgSeq: Long, + @SerialName("real_id") val realId: Long, + @SerialName("sender") val sender: MessageSender, + @SerialName("message") val message: RichText?, + @SerialName("group_id") val groupId: Long = 0, + @SerialName("peer_id") val peerId: Long, + @SerialName("target_id") val targetId: Long = 0, +) + +@Serializable +internal data class GetForwardMsgResult( + @SerialName("messages") val msgs: List +) + +@Serializable +internal data class MessageSender( + @SerialName("user_id") val userId: Long, + @SerialName("nickname") val nickName: String, + @SerialName("sex") val sex: String, + @SerialName("age") val age: Int, + @SerialName("uid") val uid: String, + @SerialName("tiny_id") val tinyId: String, +) + +@Serializable +internal data class EssenceMessage( + @SerialName("sender_id") val senderId: Long, + @SerialName("sender_nick") val senderNick: String, + @SerialName("sender_time") val senderTime: Long, + @SerialName("operator_id") val operatorId: Long, + @SerialName("operator_nick") val operatorNick: String, + @SerialName("operator_time") val operatorTime: Long, + @SerialName("message_id") var messageId: Int, + @SerialName("message_seq") val messageSeq: Int, + @SerialName("real_id") val realId: Int, + @SerialName("message_content") val messageContent: JsonElement, +) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MessageHelper.kt b/xposed/src/main/java/qq/service/msg/MessageHelper.kt index c66bb73..8f99497 100644 --- a/xposed/src/main/java/qq/service/msg/MessageHelper.kt +++ b/xposed/src/main/java/qq/service/msg/MessageHelper.kt @@ -1,20 +1,105 @@ package qq.service.msg +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.mobileqq.troop.api.ITroopMemberNameService import com.tencent.qqnt.kernel.api.IKernelService import com.tencent.qqnt.kernel.nativeinterface.Contact import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo +import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo +import com.tencent.qqnt.msg.api.IMsgService import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.shamrock.tools.toHexString +import moe.fuqiuluo.shamrock.utils.DeflateTools +import moe.fuqiuluo.symbols.decodeProtobuf +import protobuf.auto.toByteArray +import protobuf.message.longmsg.LongMsgAction +import protobuf.message.longmsg.LongMsgPayload +import protobuf.message.longmsg.LongMsgReq +import protobuf.message.longmsg.LongMsgRsp +import protobuf.message.longmsg.LongMsgSettings +import protobuf.message.longmsg.LongMsgUid +import protobuf.message.longmsg.RecvLongMsgInfo import qq.service.QQInterfaces import qq.service.contact.ContactHelper import qq.service.internals.msgService import kotlin.coroutines.resume +typealias MessageId = Long + internal object MessageHelper: QQInterfaces() { + private suspend fun prepareTempChatFromGroup( + groupId: String, + peerId: String + ): Result { + LogCenter.log("主动临时消息,创建临时会话。", Level.INFO) + val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService + ?: return Result.failure(Exception("获取消息服务失败")) + msgService.prepareTempChat( + TempChatPrepareInfo( + MsgConstant.KCHATTYPETEMPC2CFROMGROUP, + ContactHelper.getUidByUinAsync(peerId = peerId.toLong()), + app.getRuntimeService(ITroopMemberNameService::class.java, "all") + .getTroopMemberNameRemarkFirst(groupId, peerId), + groupId, + EMPTY_BYTE_ARRAY, + app.currentUid, + "", + TempChatGameSession() + ) + ) { code, reason -> + if (code != 0) { + LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR) + } + } + return Result.success(Unit) + } + + suspend fun sendMessage(contact: Contact, msgs: ArrayList, retry: Int, uniseq: Long): Result { + if (contact.chatType == MsgConstant.KCHATTYPETEMPC2CFROMGROUP) { + prepareTempChatFromGroup(contact.guildId, contact.peerUid).getOrThrow() + } + return withTimeoutOrNull(5000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).sendMsg(contact, uniseq, msgs) { code: Int, msg: String -> + if (code == 0) { + it.resume(uniseq) + } else { + LogCenter.log("消息发送失败: $code:$msg", Level.WARN) + it.resume(null) + } + } + } + }?.let { Result.success(it) } ?: resendMsg(contact, uniseq, retry) + } + + private suspend fun resendMsg(contact: Contact, msgId: MessageId, retry: Int): Result { + if (retry > 0) { + return withTimeoutOrNull(5000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).resendMsg(contact, msgId) { code, msg -> + if (code == 0) { + it.resume(msgId) + } else { + LogCenter.log("消息重发失败: $code:$msg", Level.WARN) + it.resume(null) + } + } + } + }?.let { Result.success(it) } ?: resendMsg(contact, msgId, retry - 1) + } else { + return Result.failure(Exception("消息发送失败:重试已达上限")) + } + } + suspend fun getTempChatInfo(chatType: Int, uid: String): Result { val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService ?: return Result.failure(Exception("获取消息服务失败")) @@ -64,6 +149,70 @@ internal object MessageHelper: QQInterfaces() { } } + suspend fun getMultiMsg(resId: String): Result> { + val req = LongMsgReq( + recvInfo = RecvLongMsgInfo( + uid = LongMsgUid(app.currentUid), + resId = resId, + u1 = 3 + ), + setting = LongMsgSettings( + field1 = 2, + field2 = 2, + field3 = 9, + field4 = 0 + ) + ) + val fromServiceMsg = sendBufferAW( + "trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg", + true, + req.toByteArray() + ) ?: return Result.failure(Exception("unable to get multi message")) + val rsp = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf() + val zippedPayload = DeflateTools.ungzip( + rsp.recvResult?.payload ?: return Result.failure(Exception("payload is empty")) + ) + LogCenter.log(zippedPayload.toHexString(), Level.DEBUG) + return Result.success( + zippedPayload.decodeProtobuf().action + ?: return Result.failure(Exception("action is empty")) + ) + } + + suspend fun getForwardMsg(resId: String): Result> { + val result = getMultiMsg(resId).getOrElse { return Result.failure(it) } + result.forEach { + if (it.command == "MultiMsg") { + return Result.success(it.data?.body?.map { msg -> + val chatType = if (msg.contentHead!!.msgType == 82) MsgConstant.KCHATTYPEGROUP else MsgConstant.KCHATTYPEC2C + MessageDetail( + time = msg.contentHead?.msgTime?.toInt() ?: 0, + msgType = chatType, + msgId = 0, // msgViaRandom为空 tx不给 + qqMsgId = 0, + msgSeq = msg.contentHead!!.msgSeq ?: 0, + realId = msg.contentHead!!.msgSeq ?: 0, + sender = MessageSender( + msg.msgHead?.peer ?: 0, + msg.msgHead?.responseGrp?.memberCard ?: msg.msgHead?.forward?.friendName ?: "", + "unknown", + 0, + msg.msgHead?.peerUid ?: "", + msg.msgHead?.peerUid ?: "" + ), + message = msg.body?.richText, + peerId = msg.msgHead?.peer ?: 0, + groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong() + ?: 0 else 0, + targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 0 else 0 + ) + } ?: return Result.failure(Exception("Msg is empty"))) + } + } + return Result.failure(Exception("Can't find msg")) + } + + fun generateMsgId(chatType: Int): Long { return createMessageUniseq(chatType, System.currentTimeMillis()) } diff --git a/xposed/src/main/java/qq/service/msg/MsgConvertor.kt b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt index 9d38347..aa2e277 100644 --- a/xposed/src/main/java/qq/service/msg/MsgConvertor.kt +++ b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt @@ -5,9 +5,7 @@ import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.msg.api.IMsgService -import io.kritor.event.AtElement import io.kritor.event.Element -import io.kritor.event.ElementKt import io.kritor.event.ImageType import io.kritor.event.Scene import io.kritor.event.atElement @@ -47,23 +45,13 @@ import qq.service.bdh.RichProtoSvc import qq.service.contact.ContactHelper import kotlin.coroutines.resume +/** + * 将NT消息(com.tencent.qqnt.*)转换为事件消息(io.kritor.event.*)推送 + */ + typealias NtMessages = ArrayList typealias Convertor = suspend (MsgRecord, MsgElement) -> Result -suspend fun NtMessages.toKritorMessages(record: MsgRecord): ArrayList { - val result = arrayListOf() - forEach { - MsgConvertor[it.elementType]?.invoke(record, it)?.onSuccess { - result.add(it) - }?.onFailure { - if (it !is ActionMsgException) { - LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN) - } - } - } - return result -} - private object MsgConvertor { private val convertorMap = hashMapOf( MsgConstant.KELEMTYPETEXT to ::convertText, @@ -423,3 +411,16 @@ private object MsgConvertor { } } +suspend fun NtMessages.toKritorEventMessages(record: MsgRecord): ArrayList { + val result = arrayListOf() + forEach { + MsgConvertor[it.elementType]?.invoke(record, it)?.onSuccess { + result.add(it) + }?.onFailure { + if (it !is ActionMsgException) { + LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN) + } + } + } + return result +} diff --git a/xposed/src/main/java/qq/service/msg/MultiConvertor.kt b/xposed/src/main/java/qq/service/msg/MultiConvertor.kt new file mode 100644 index 0000000..44b9bbd --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/MultiConvertor.kt @@ -0,0 +1,275 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) +package qq.service.msg + +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import io.kritor.message.Element +import io.kritor.message.ElementType +import io.kritor.message.ImageType +import io.kritor.message.Scene +import io.kritor.message.atElement +import io.kritor.message.buttonActionPermission +import io.kritor.message.buttonElement +import io.kritor.message.contactElement +import io.kritor.message.faceElement +import io.kritor.message.forwardElement +import io.kritor.message.imageElement +import io.kritor.message.jsonElement +import io.kritor.message.locationElement +import io.kritor.message.markdownElement +import io.kritor.message.replyElement +import io.kritor.message.textElement +import kotlinx.io.core.ByteReadPacket +import kotlinx.io.core.discardExact +import kotlinx.io.core.readUInt +import moe.fuqiuluo.shamrock.tools.asJsonArray +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.shamrock.tools.toHexString +import moe.fuqiuluo.shamrock.utils.DeflateTools +import moe.fuqiuluo.symbols.decodeProtobuf +import protobuf.message.Elem +import protobuf.message.element.commelem.ButtonExtra +import protobuf.message.element.commelem.MarkdownExtra +import protobuf.message.element.commelem.QFaceExtra +import qq.service.bdh.RichProtoSvc + +/** + * 将合并转发PB(protobuf.message.*)转请求消息(io.kritor.message.*)发送 + */ + +suspend fun List.toKritorResponseMessages(contact: Contact): ArrayList { + val kritorMessages = ArrayList() + forEach { element -> + if (element.text != null) { + val text = element.text!! + if (text.attr6Buf != null) { + val at = ByteReadPacket(text.attr6Buf!!) + at.discardExact(7) + val uin = at.readUInt() + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.AT + this.at = atElement { + this.uin = uin.toLong() + } + }) + } else { + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.TEXT + this.text = textElement { + this.text = text.str ?: "" + } + }) + } + } else if (element.face != null) { + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.FACE + this.face = faceElement { + this.id = element.face!!.index ?: 0 + } + }) + } else if (element.customFace != null) { + val customFace = element.customFace!! + val md5 = customFace.md5.toHexString() + val origUrl = customFace.origUrl!! + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.IMAGE + this.image = imageElement { + this.fileName = md5 + this.type = if (customFace.origin == true) ImageType.ORIGIN else ImageType.COMMON + this.url = when (contact.chatType) { + MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(origUrl, md5) + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5) + else -> throw UnsupportedOperationException("Not supported chat type: $contact") + } + } + }) + } else if (element.notOnlineImage != null) { + require(element.notOnlineImage != null) + val md5 = element.notOnlineImage!!.picMd5.toHexString() + val origUrl = element.notOnlineImage!!.origUrl!! + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.IMAGE + this.image = imageElement { + this.fileName = md5 + this.type = if (element.notOnlineImage?.original == true) ImageType.ORIGIN else ImageType.COMMON + this.url = when (contact.chatType) { + MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(origUrl, md5) + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5) + else -> throw UnsupportedOperationException("Not supported chat type: $contact") + } + } + }) + } else if (element.generalFlags != null) { + val generalFlags = element.generalFlags!! + if (generalFlags.longTextFlag == 1u) { + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.FORWARD + this.forward = forwardElement { + this.id = generalFlags.longTextResid ?: "" + } + }) + } + } else if (element.srcMsg != null) { + val srcMsg = element.srcMsg!! + val msgId = srcMsg.pbReserve?.msgRand?.toLong() ?: 0 + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.REPLY + this.reply = replyElement { + this.messageId = msgId + } + }) + } else if (element.lightApp != null) { + val data = element.lightApp!!.data!! + val jsonStr = (if (data[0].toInt() == 1) DeflateTools.uncompress(data.slice(1)) else data.slice(1)).decodeToString() + val json = jsonStr.asJsonObject + when (json["app"].asString) { + "com.tencent.multimsg" -> { + val info = json["meta"].asJsonObject["detail"].asJsonObject + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.FORWARD + this.forward = forwardElement { + this.id = info["resid"].asString + this.uniseq = info["uniseq"].asString + this.summary = info["summary"].asString + this.description = info["news"].asJsonArray.joinToString("\n") { + it.asJsonObject["text"].asString + } + } + }) + } + + "com.tencent.troopsharecard" -> { + val info = json["meta"].asJsonObject["contact"].asJsonObject + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.CONTACT + this.contact = contactElement { + this.scene = Scene.GROUP + this.peer = info["jumpUrl"].asString.split("group_code=")[1] + } + }) + + } + + "com.tencent.contact.lua" -> { + val info = json["meta"].asJsonObject["contact"].asJsonObject + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.CONTACT + this.contact = contactElement { + this.scene = Scene.FRIEND + this.peer = info["jumpUrl"].asString.split("uin=")[1] + } + }) + } + + "com.tencent.map" -> { + val info = json["meta"].asJsonObject["Location.Search"].asJsonObject + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.LOCATION + this.location = locationElement { + this.lat = info["lat"].asString.toFloat() + this.lon = info["lng"].asString.toFloat() + this.address = info["address"].asString + this.title = info["name"].asString + } + }) + } + else -> { + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.JSON + this.json = jsonElement { + this.json = jsonStr + } + }) + } + } + } else if (element.commonElem != null) { + val commonElem = element.commonElem!! + when (commonElem.serviceType) { + 37 -> { + val qFaceExtra = commonElem.elem!!.decodeProtobuf() + when (qFaceExtra.faceId) { + 358 -> kritorMessages.add(io.kritor.message.element { + this.type = ElementType.DICE + this.dice = io.kritor.message.diceElement { + this.id = qFaceExtra.result!!.toInt() + } + }) + + 359 -> kritorMessages.add(io.kritor.message.element { + this.type = ElementType.RPS + this.rps = io.kritor.message.rpsElement { + this.id = qFaceExtra.result!!.toInt() + } + }) + + else -> kritorMessages.add(io.kritor.message.element { + this.type = ElementType.FACE + this.face = faceElement { + this.id = qFaceExtra.faceId ?: 0 + this.isBig = false + this.result = qFaceExtra.result?.toInt() ?: 0 + } + }) + } + } + + 45 -> { + val markdownExtra = commonElem.elem!!.decodeProtobuf() + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.MARKDOWN + this.markdown = markdownElement { + this.markdown = markdownExtra.content!! + } + }) + } + + 46 -> { + val buttonExtra = commonElem.elem!!.decodeProtobuf() + kritorMessages.add(io.kritor.message.element { + this.type = ElementType.BUTTON + this.button = buttonElement { + buttonExtra.field1!!.rows?.forEach { row -> + this.rows.add(io.kritor.message.row { + row.buttons?.forEach { button -> + this.buttons.add(io.kritor.message.button { + val renderData = button.renderData + val action = button.action + val permission = action?.permission + this.id = button.id ?: "" + this.renderData = io.kritor.message.buttonRender { + this.label = renderData?.label ?: "" + this.visitedLabel = renderData?.visitedLabel ?: "" + this.style = renderData?.style ?: 0 + } + this.action = io.kritor.message.buttonAction { + this.type = action?.type ?: 0 + this.permission = buttonActionPermission { + this.type = permission?.type ?: 0 + this.roleIds.addAll( + permission?.specifyRoleIds ?: emptyList() + ) + this.userIds.addAll( + permission?.specifyUserIds ?: emptyList() + ) + } + this.unsupportedTips = action?.unsupportTips ?: "" + this.data = action?.data ?: "" + this.reply = action?.reply ?: false + this.enter = action?.enter ?: false + } + }) + } + }) + } + } + }) + } + } + } + } + return kritorMessages +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt b/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt new file mode 100644 index 0000000..74e9581 --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt @@ -0,0 +1,838 @@ +package qq.service.msg + +import android.graphics.BitmapFactory +import android.util.Base64 +import androidx.exifinterface.media.ExifInterface +import com.tencent.mobileqq.emoticon.QQSysFaceUtil +import com.tencent.mobileqq.pb.ByteStringMicro +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qphone.base.remote.ToServiceMsg +import com.tencent.qqnt.aio.adapter.api.IAIOPttApi +import com.tencent.qqnt.kernel.nativeinterface.* +import com.tencent.qqnt.msg.api.IMsgService +import io.kritor.message.AtElement +import io.kritor.message.Button +import io.kritor.message.Element +import io.kritor.message.ElementType +import io.kritor.message.ElementType.* +import io.kritor.message.ImageElement +import io.kritor.message.ImageType +import io.kritor.message.MusicPlatform +import io.kritor.message.Scene +import io.kritor.message.VoiceElement +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.config.EnableOldBDH +import moe.fuqiuluo.shamrock.config.get +import moe.fuqiuluo.shamrock.helper.ActionMsgException +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.helper.LogicException +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import moe.fuqiuluo.shamrock.utils.AudioUtils +import moe.fuqiuluo.shamrock.utils.DownloadUtils +import moe.fuqiuluo.shamrock.utils.FileUtils +import moe.fuqiuluo.shamrock.utils.MediaType +import moe.fuqiuluo.shamrock.utils.PlatformUtils +import mqq.app.MobileQQ +import qq.service.QQInterfaces.Companion.app +import qq.service.bdh.FileTransfer +import qq.service.bdh.PictureResource +import qq.service.bdh.Private +import qq.service.bdh.Transfer +import qq.service.bdh.Troop +import qq.service.bdh.VideoResource +import qq.service.bdh.VoiceResource +import qq.service.bdh.trans +import qq.service.bdh.with +import qq.service.contact.ContactHelper +import qq.service.contact.longPeer +import qq.service.group.GroupHelper +import qq.service.internals.NTServiceFetcher +import qq.service.internals.msgService +import qq.service.lightapp.ArkAppInfo +import qq.service.lightapp.ArkMsgHelper +import qq.service.lightapp.LbsHelper +import qq.service.lightapp.MusicHelper +import qq.service.lightapp.WeatherHelper +import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77 +import tencent.im.oidb.cmd0xdc2.oidb_cmd0xdc2 +import tencent.im.oidb.oidb_sso +import java.io.ByteArrayInputStream +import java.io.File +import kotlin.coroutines.resume +import kotlin.math.roundToInt +import kotlin.random.Random +import kotlin.random.nextInt + +/** + * 将请求消息(io.kritor.message)转成NT消息(com.tencent.qqnt.*)发送 + */ + + +typealias Messages = Collection +private typealias NtConvertor = suspend (Contact, Long, Element) -> Result + +object NtMsgConvertor { + private val ntConvertors = mapOf( + TEXT to ::textConvertor, + AT to ::atConvertor, + FACE to ::faceConvertor, + BUBBLE_FACE to ::bubbleFaceConvertor, + REPLY to ::replyConvertor, + IMAGE to ::imageConvertor, + VOICE to ::voiceConvertor, + VIDEO to ::videoConvertor, + BASKETBALL to ::basketballConvertor, + DICE to ::diceConvertor, + RPS to ::rpsConvertor, + POKE to ::pokeConvertor, + MUSIC to ::musicConvertor, + WEATHER to ::weatherConvertor, + LOCATION to ::locationConvertor, + SHARE to ::shareConvertor, + CONTACT to ::contactConvertor, + JSON to ::jsonConvertor, + MARKDOWN to ::markdownConvertor, + BUTTON to ::buttonConvertor, + ) + + suspend fun convertToNtMsgs(contact: Contact, msgId: Long, msgs: Messages): ArrayList { + val ntMsgs = ArrayList() + msgs.forEach { + val convertor = ntConvertors[it.type] + if (convertor == null) { + LogCenter.log("未知的消息类型: ${it.type}", Level.WARN) + } else { + try { + ntMsgs.add(convertor(contact, msgId, it).getOrThrow()) + } catch (e: Throwable) { + if (e !is ActionMsgException) { + LogCenter.log("消息转换失败: ${it.type}", Level.WARN) + } + } + } + } + return ntMsgs + } + + private suspend fun textConvertor(contact: Contact, msgId: Long, text: Element): Result { + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPETEXT + elem.textElement = TextElement() + elem.textElement.content = text.text.text + return Result.success(elem) + } + + private suspend fun atConvertor(contact: Contact, msgId: Long, sourceAt: Element): Result { + if (contact.chatType != MsgConstant.KCHATTYPEGROUP) { + LogCenter.log("暂不支持非群聊的@元素", Level.WARN) + return Result.failure(ActionMsgException) + } + + val elem = MsgElement() + val at = TextElement() + if (sourceAt.at.accountCase == AtElement.AccountCase.UIN) { + val uin = sourceAt.at.uin + if (uin == 0L) { + at.content = "@全体成员" + at.atType = MsgConstant.ATTYPEALL + at.atNtUid = "0" + } else { + val info = GroupHelper.getTroopMemberInfoByUinV2(contact.peerUid, uin.toString(), true).onFailure { + LogCenter.log("无法获取群成员信息: contact=$contact, id=${uin}", Level.WARN) + }.getOrNull() + at.content = "@${ + info?.troopnick.ifNullOrEmpty { info?.friendnick } + ?: uin.toString() + }" + at.atType = MsgConstant.ATTYPEONE + at.atNtUid = ContactHelper.getUidByUinAsync(uin) + } + } else { + val uid = sourceAt.at.uid + if (uid == "all" || uid == "0") { + at.content = "@全体成员" + at.atType = MsgConstant.ATTYPEALL + at.atNtUid = "0" + } else { + val uin = ContactHelper.getUinByUidAsync(uid) + val info = GroupHelper.getTroopMemberInfoByUinV2(contact.peerUid, uin, true).onFailure { + LogCenter.log("无法获取群成员信息: contact=$contact, id=${uin}", Level.WARN) + }.getOrNull() + at.content = "@${ + info?.troopnick.ifNullOrEmpty { info?.friendnick } + ?: uin + }" + at.atType = MsgConstant.ATTYPEONE + at.atNtUid = uid + } + } + elem.textElement = at + elem.elementType = MsgConstant.KELEMTYPETEXT + return Result.success(elem) + } + + private suspend fun faceConvertor(contact: Contact, msgId: Long, sourceFace: Element): Result { + val serverId = sourceFace.face.id + val big = sourceFace.face.isBig || serverId == 394 + + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEFACE + val face = FaceElement() + + // 1 old face + // 2 normal face + // 3 super face + // 4 is market face + // 5 is vas poke + face.faceType = if (big) 3 else 2 + face.faceIndex = serverId + face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId)) + if (serverId == 394) { + face.stickerId = "40" + face.packId = "1" + face.sourceType = 1 + face.stickerType = 3 + face.randomType = 1 + face.resultId = Random.nextInt(1..5).toString() + } else if (big) { + face.imageType = 0 + face.stickerId = "30" + face.packId = "1" + face.sourceType = 1 + face.stickerType = 1 + face.randomType = 1 + } else { + face.imageType = 0 + face.packId = "0" + } + elem.faceElement = face + + return Result.success(elem) + } + + private suspend fun bubbleFaceConvertor(contact: Contact, msgId: Long, sourceBubbleFace: Element): Result { + val faceId = sourceBubbleFace.bubbleFace.id + val local = QQSysFaceUtil.convertToLocal(faceId) + val name = QQSysFaceUtil.getFaceDescription(local) + val count = sourceBubbleFace.bubbleFace.count + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEFACEBUBBLE + val face = FaceBubbleElement() + face.faceType = 13 + face.faceCount = count + face.faceSummary = QQSysFaceUtil.getPrueFaceDescription(name) + val smallYellowFaceInfo = SmallYellowFaceInfo() + smallYellowFaceInfo.index = faceId + smallYellowFaceInfo.compatibleText = face.faceSummary + smallYellowFaceInfo.text = face.faceSummary + face.yellowFaceInfo = smallYellowFaceInfo + face.faceFlag = 0 + face.content = "[${face.faceSummary}]x$count" + elem.faceBubbleElement = face + return Result.success(elem) + } + + private suspend fun replyConvertor(contact: Contact, msgId: Long, sourceReply: Element): Result { + val element = MsgElement() + element.elementType = MsgConstant.KELEMTYPEREPLY + val reply = ReplyElement() + + reply.replayMsgId = sourceReply.reply.messageId + reply.sourceMsgIdInRecords = reply.replayMsgId + + if (reply.replayMsgId == 0L) { + LogCenter.log("无法获取被回复消息", Level.ERROR) + } + + withTimeoutOrNull(3000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).getMsgsByMsgId(contact, arrayListOf(reply.replayMsgId)) { _, _, records -> + it.resume(records) + } + } + }?.firstOrNull()?.let { + reply.replayMsgSeq = it.msgSeq + //reply.sourceMsgText = it.elements.firstOrNull { it.elementType == MsgConstant.KELEMTYPETEXT }?.textElement?.content + reply.replyMsgTime = it.msgTime + reply.senderUidStr = it.senderUid + reply.senderUid = it.senderUin + } + + element.replyElement = reply + return Result.success(element) + } + + private suspend fun imageConvertor(contact: Contact, msgId: Long, sourceImage: Element): Result { + val isOriginal = sourceImage.image.type == ImageType.ORIGIN + val isFlash = sourceImage.image.type == ImageType.FLASH + val file = when(sourceImage.image.dataCase!!) { + ImageElement.DataCase.FILE_NAME -> { + val fileMd5 = sourceImage.image.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase() + FileUtils.getFileByMd5(fileMd5) + } + ImageElement.DataCase.FILE_PATH -> { + val filePath = sourceImage.image.filePath + File(filePath).inputStream().use { + FileUtils.saveFileToCache(it) + } + } + ImageElement.DataCase.FILE_BASE64 -> { + FileUtils.saveFileToCache(ByteArrayInputStream( + Base64.decode(sourceImage.image.fileBase64, Base64.DEFAULT) + )) + } + ImageElement.DataCase.URL -> { + val tmp = FileUtils.getTmpFile() + if(DownloadUtils.download(sourceImage.image.url, tmp)) { + tmp.inputStream().use { + FileUtils.saveFileToCache(it) + }.also { + tmp.delete() + } + } else { + tmp.delete() + return Result.failure(LogicException("图片资源下载失败: ${sourceImage.image.url}")) + } + } + ImageElement.DataCase.DATA_NOT_SET -> return Result.failure(IllegalArgumentException("ImageElement data is not set")) + } + + if (EnableOldBDH.get()) { + Transfer with when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> Troop(contact.peerUid) + MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> Private(contact.longPeer().toString()) + MsgConstant.KCHATTYPEGUILD -> Troop(contact.peerUid) + else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for PictureMsg")) + } trans PictureResource(file) + } + + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEPIC + val pic = PicElement() + pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) + + val msgService = NTServiceFetcher.kernelService.msgService!! + val originalPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + 2, 0, pic.md5HexStr, file.name, 1, 0, null, "", true + ) + ) + if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != file.length()) { + val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + 2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true + ) + ) + QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) + QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath) + } + + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(file.absolutePath, options) + val exifInterface = ExifInterface(file.absolutePath) + val orientation = exifInterface.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED + ) + if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) { + pic.picWidth = options.outWidth + pic.picHeight = options.outHeight + } else { + pic.picWidth = options.outHeight + pic.picHeight = options.outWidth + } + pic.sourcePath = file.absolutePath + pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath) + pic.original = isOriginal + pic.picType = FileUtils.getPicType(file) + pic.picSubType = 0 + pic.isFlashPic = isFlash + + elem.picElement = pic + + return Result.success(elem) + } + + private suspend fun voiceConvertor(contact: Contact, msgId: Long, sourceVoice: Element): Result { + var file = when(sourceVoice.voice.dataCase!!) { + VoiceElement.DataCase.FILE_NAME -> { + val fileMd5 = sourceVoice.voice.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase() + FileUtils.getFileByMd5(fileMd5) + } + VoiceElement.DataCase.FILE_PATH -> { + val filePath = sourceVoice.voice.filePath + File(filePath).inputStream().use { + FileUtils.saveFileToCache(it) + } + } + VoiceElement.DataCase.FILE_BASE64 -> { + FileUtils.saveFileToCache(ByteArrayInputStream( + Base64.decode(sourceVoice.voice.fileBase64, Base64.DEFAULT) + )) + } + VoiceElement.DataCase.URL -> { + val tmp = FileUtils.getTmpFile() + if(DownloadUtils.download(sourceVoice.voice.url, tmp)) { + tmp.inputStream().use { + FileUtils.saveFileToCache(it) + }.also { + tmp.delete() + } + } else { + tmp.delete() + return Result.failure(LogicException("音频资源下载失败: ${sourceVoice.voice.url}")) + } + } + VoiceElement.DataCase.DATA_NOT_SET -> return Result.failure(IllegalArgumentException("VoiceElement data is not set")) + } + + val isMagic = sourceVoice.voice.magic + + val ptt = PttElement() + + when (AudioUtils.getMediaType(file)) { + MediaType.Silk -> { + LogCenter.log({ "Silk: $file" }, Level.DEBUG) + ptt.formatType = MsgConstant.KPTTFORMATTYPESILK + ptt.duration = QRoute.api(IAIOPttApi::class.java) + .getPttFileDuration(file.absolutePath) + } + + MediaType.Amr -> { + LogCenter.log({ "Amr: $file" }, Level.DEBUG) + ptt.duration = AudioUtils.getDurationSec(file) + ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR + } + + MediaType.Pcm -> { + LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG) + val result = AudioUtils.pcmToSilk(file) + ptt.duration = (result.second * 0.001).roundToInt() + file = result.first + ptt.formatType = MsgConstant.KPTTFORMATTYPESILK + } + + else -> { + LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG) + val result = AudioUtils.audioToSilk(file) + ptt.duration = runCatching { + QRoute.api(IAIOPttApi::class.java) + .getPttFileDuration(result.second.absolutePath) + }.getOrElse { + result.first + } + file = result.second + ptt.formatType = MsgConstant.KPTTFORMATTYPESILK + } + } + + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEPTT + ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) + + if (EnableOldBDH.get()) { + if (!(Transfer with when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> Troop(contact.peerUid) + MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> Private(contact.longPeer().toString()) + MsgConstant.KCHATTYPEGUILD -> Troop(contact.peerUid) + else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for VoiceMsg")) + } trans VoiceResource(file)) + ) { + return Result.failure(RuntimeException("上传语音失败: $file")) + } + ptt.filePath = file.absolutePath + } else { + val msgService = NTServiceFetcher.kernelService.msgService!! + + val originalPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + MsgConstant.KELEMTYPEPTT, 0, ptt.md5HexStr, file.name, 1, 0, null, "", true + ) + ) + if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != file.length()) { + QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) + } + if (originalPath != null) { + ptt.filePath = originalPath + } else { + ptt.filePath = file.absolutePath + } + } + + ptt.canConvert2Text = true + ptt.fileId = 0 + ptt.fileUuid = "" + ptt.text = "" + + if (!isMagic) { + ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD + ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE + } else { + ptt.voiceType = MsgConstant.KPTTVOICETYPEVOICECHANGE + ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPEECHO + } + + elem.pttElement = ptt + + return Result.success(elem) + } + + private suspend fun videoConvertor(contact: Contact, msgId: Long, sourceVideo: Element): Result { + val elem = MsgElement() + val video = VideoElement() + + val file = when(sourceVideo.video.dataCase!!) { + io.kritor.message.VideoElement.DataCase.FILE_NAME -> { + val fileMd5 = sourceVideo.video.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase() + FileUtils.getFileByMd5(fileMd5) + } + io.kritor.message.VideoElement.DataCase.FILE_PATH -> { + val filePath = sourceVideo.video.filePath + File(filePath).inputStream().use { + FileUtils.saveFileToCache(it) + } + } + io.kritor.message.VideoElement.DataCase.FILE_BASE64 -> { + FileUtils.saveFileToCache(ByteArrayInputStream( + Base64.decode(sourceVideo.video.fileBase64, Base64.DEFAULT) + )) + } + io.kritor.message.VideoElement.DataCase.URL -> { + val tmp = FileUtils.getTmpFile() + if(DownloadUtils.download(sourceVideo.video.url, tmp)) { + tmp.inputStream().use { + FileUtils.saveFileToCache(it) + }.also { + tmp.delete() + } + } else { + tmp.delete() + return Result.failure(LogicException("视频资源下载失败: ${sourceVideo.video.url}")) + } + } + io.kritor.message.VideoElement.DataCase.DATA_NOT_SET -> return Result.failure(IllegalArgumentException("VideoElement data is not set")) + } + + video.videoMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) + + val msgService = NTServiceFetcher.kernelService.msgService!! + val originalPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + 5, 2, video.videoMd5, file.name, 1, 0, null, "", true + ) + ) + val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( + RichMediaFilePathInfo( + 5, 1, video.videoMd5, file.name, 2, 0, null, "", true + ) + ) + if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( + originalPath + ) != file.length() + ) { + QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) + AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!) + } + + if (EnableOldBDH.get()) { + Transfer with when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> Troop(contact.peerUid) + MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> Private(contact.longPeer().toString()) + MsgConstant.KCHATTYPEGUILD -> Troop(contact.peerUid) + else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for VideoMsg")) + } trans VideoResource(file, File(thumbPath.toString())) + } + + video.fileTime = AudioUtils.getVideoTime(file) + video.fileSize = file.length() + video.fileName = file.name + video.fileFormat = FileTransfer.VIDEO_FORMAT_MP4 + video.thumbSize = QQNTWrapperUtil.CppProxy.getFileSize(thumbPath).toInt() + val options = BitmapFactory.Options() + BitmapFactory.decodeFile(thumbPath, options) + video.thumbWidth = options.outWidth + video.thumbHeight = options.outHeight + video.thumbMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(thumbPath) + video.thumbPath = hashMapOf(0 to thumbPath) + + elem.videoElement = video + elem.elementType = MsgConstant.KELEMTYPEVIDEO + + return Result.success(elem) + } + + private suspend fun basketballConvertor(contact: Contact, msgId: Long, sourceBasketball: Element): Result { + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEFACE + val face = FaceElement() + face.faceIndex = 114 + face.faceText = "/篮球" + face.faceType = 3 + face.packId = "1" + face.stickerId = "13" + face.sourceType = 1 + face.stickerType = 2 + face.resultId = Random.nextInt(1..5).toString() + face.surpriseId = "" + face.randomType = 1 + elem.faceElement = face + return Result.success(elem) + } + + private suspend fun diceConvertor(contact: Contact, msgId: Long, sourceDice: Element): Result { + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEMARKETFACE + val market = MarketFaceElement( + 6, 1, 11464, 3, 0, 200, 200, + "[骰子]", "4823d3adb15df08014ce5d6796b76ee1", "409e2a69b16918f9", + null, null, 0, 0, 0, 1, 0, + null, null, null, // jumpurl + "", null, null, + null, null, arrayListOf(MarketFaceSupportSize(200, 200)), null + ) + elem.marketFaceElement = market + return Result.success(elem) + } + + private suspend fun rpsConvertor(contact: Contact, msgId: Long, sourceRps: Element): Result { + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEMARKETFACE + val market = MarketFaceElement( + 6, 1, 11415, 3, 0, 200, 200, + "[猜拳]", "83C8A293AE65CA140F348120A77448EE", "7de39febcf45e6db", + null, null, 0, 0, 0, 1, 0, + null, null, null, + "", null, null, + null, null, arrayListOf(MarketFaceSupportSize(200, 200)), null + ) + elem.marketFaceElement = market + return Result.success(elem) + } + + private suspend fun pokeConvertor(contact: Contact, msgId: Long, sourcePoke: Element): Result { + val elem = MsgElement() + val face = FaceElement() + face.faceIndex = 0 + face.faceText = "" + face.faceType = 5 + face.packId = null + face.pokeType = sourcePoke.poke.type + face.spokeSummary = "" + face.doubleHit = 0 + face.vaspokeId = sourcePoke.poke.id + face.vaspokeName = "" + face.vaspokeMinver = "" + face.pokeStrength = sourcePoke.poke.strength + face.msgType = 0 + face.faceBubbleCount = 0 + face.oldVersionStr = "[截一戳]请使用最新版手机QQ体验新功能。" + face.pokeFlag = 0 + elem.elementType = MsgConstant.KELEMTYPEFACE + elem.faceElement = face + return Result.success(elem) + } + + private suspend fun musicConvertor(contact: Contact, msgId: Long, sourceMusic: Element): Result { + when (val type = sourceMusic.music.platform) { + MusicPlatform.QQ -> { + val id = sourceMusic.music.id + if (!MusicHelper.tryShareQQMusicById(contact, msgId, id)) { + LogCenter.log("无法发送QQ音乐分享", Level.ERROR) + } + } + + MusicPlatform.NetEase -> { + val id = sourceMusic.music.id + if (!MusicHelper.tryShare163MusicById(contact, msgId, id)) { + LogCenter.log("无法发送网易云音乐分享", Level.ERROR) + } + } + + MusicPlatform.Custom -> { + val data = sourceMusic.music.custom + ArkMsgHelper.tryShareMusic( + contact, + msgId, + ArkAppInfo.QQMusic, + data.title, + data.author, + data.url, + data.pic, + data.audio + ) + } + + else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR) + } + + return Result.failure(ActionMsgException) + } + + private suspend fun weatherConvertor(contact: Contact, msgId: Long, sourceWeather: Element): Result { + val code = if (sourceWeather.weather.code.isNullOrEmpty()) { + val city = sourceWeather.weather.city + WeatherHelper.searchCity(city).onFailure { + LogCenter.log("无法获取城市天气: $city", Level.ERROR) + }.getOrThrow().first().adcode + } else sourceWeather.weather.code.toInt() + WeatherHelper.fetchWeatherCard(code).onSuccess { + val element = MsgElement() + element.elementType = MsgConstant.KELEMTYPEARKSTRUCT + val share = it["weekStore"] + .asJsonObject["share"] + .asJsonObject["data"].toString() + element.arkElement = + ArkElement(share, null, MsgConstant.ARKSTRUCTELEMENTSUBTYPEUNKNOWN) + return Result.success(element) + }.onFailure { + return Result.failure(it) + } + return Result.failure(ActionMsgException) + } + + private suspend fun locationConvertor(contact: Contact, msgId: Long, sourceLocation: Element): Result { + LbsHelper.tryShareLocation(contact, sourceLocation.location.lat.toDouble(), sourceLocation.location.lon.toDouble()).onFailure { + LogCenter.log("无法发送位置分享", Level.ERROR) + } + return Result.failure(ActionMsgException) + } + + private suspend fun shareConvertor(contact: Contact, msgId: Long, sourceShare: Element): Result { + val url = sourceShare.share.url + val image = sourceShare.share.image.ifNullOrEmpty { + val startWithPrefix = url.startsWith("http://") || url.startsWith("https://") + val endWithPrefix = url.startsWith("/") + "http://" + url.split("/")[if (startWithPrefix) 2 else 0] + if (!endWithPrefix) { + "/favicon.ico" + } else { + "favicon.ico" + } + }!! + val title = sourceShare.share.title + val content = sourceShare.share.content + + val reqBody = oidb_cmd0xdc2.ReqBody() + val info = oidb_cmd0xb77.ReqBody() + info.appid.set(100446242L) + info.app_type.set(1) + info.msg_style.set(0) + info.recv_uin.set(contact.longPeer()) + val clientInfo = oidb_cmd0xb77.ClientInfo() + clientInfo.platform.set(1) + info.client_info.set(clientInfo) + val richMsgBody = oidb_cmd0xb77.RichMsgBody() + richMsgBody.using_ark.set(true) + richMsgBody.title.set(title) + richMsgBody.summary.set(content ?: url) + richMsgBody.brief.set("[分享] $title") + richMsgBody.url.set(url) + richMsgBody.picture_url.set(image) + info.ext_info.set(oidb_cmd0xb77.ExtInfo().also { + it.msg_seq.set(msgId) + }) + info.rich_msg_body.set(richMsgBody) + reqBody.msg_body.set(info) + val sendTo = oidb_cmd0xdc2.BatchSendReq() + when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> sendTo.send_type.set(1) + MsgConstant.KCHATTYPEC2C -> sendTo.send_type.set(0) + else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for ShareMsg")) + } + sendTo.recv_uin.set(contact.peerUid.toLong()) + reqBody.batch_send_req.add(sendTo) + val to = ToServiceMsg("mobileqq.service", app.currentAccountUin, "OidbSvc.0xdc2_34") + val oidb = oidb_sso.OIDBSSOPkg() + oidb.uint32_command.set(0xdc2) + oidb.uint32_service_type.set(34) + oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(reqBody.toByteArray())) + oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext())) + to.putWupBuffer(oidb.toByteArray()) + to.addAttribute("req_pb_protocol_flag", true) + app.sendToService(to) + return Result.failure(ActionMsgException) + } + + private suspend fun contactConvertor(contact: Contact, msgId: Long, sourceContact: Element): Result { + val elem = MsgElement() + + when (val scene = sourceContact.contact.scene) { + Scene.FRIEND -> { + val ark = ArkElement(ContactHelper.getSharePrivateArkMsg(contact.longPeer()), null, null) + elem.arkElement = ark + } + + Scene.GROUP -> { + val ark = ArkElement(ContactHelper.getShareTroopArkMsg(contact.longPeer()), null, null) + elem.arkElement = ark + } + + else -> return Result.failure(LogicException("不支持的联系人分享类型: $scene")) + } + + elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT + return Result.success(elem) + } + + private suspend fun jsonConvertor(contact: Contact, msgId: Long, sourceJson: Element): Result { + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT + val ark = ArkElement(sourceJson.json.json, null, null) + elem.arkElement = ark + return Result.success(elem) + } + + private suspend fun markdownConvertor(contact: Contact, msgId: Long, sourceMarkdown: Element): Result { + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEMARKDOWN + val markdownElement = MarkdownElement(sourceMarkdown.markdown.markdown) + elem.markdownElement = markdownElement + return Result.success(elem) + } + + private suspend fun buttonConvertor(contact: Contact, msgId: Long, sourceButton: Element): Result { + fun tryNewKeyboardButton(button: Button): InlineKeyboardButton { + val renderData = button.renderData + val action = button.action + val permission = action.permission + return runCatching { + InlineKeyboardButton(button.id, renderData.label, renderData.visitedLabel, renderData.style, + action.type, 0, + action.unsupportedTips, + action.data, false, + permission.type, + ArrayList(permission.roleIdsList), + ArrayList(permission.userIdsList), + false, 0, false, arrayListOf() + ) + }.getOrElse { + InlineKeyboardButton(button.id, renderData.label, renderData.visitedLabel, renderData.style, + action.type, 0, + action.unsupportedTips, + action.data, false, + permission.type, + ArrayList(permission.roleIdsList), + ArrayList(permission.userIdsList) + ) + } + } + + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEINLINEKEYBOARD + val rows = arrayListOf() + + val keyboard = sourceButton.button + keyboard.rowsList.forEach { row -> + val buttons = arrayListOf() + row.buttonsList.forEach { button -> + buttons.add(tryNewKeyboardButton(button)) + } + rows.add(InlineKeyboardRow(buttons)) + } + elem.inlineKeyboardElement = InlineKeyboardElement(rows, 0) + return Result.success(elem) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt b/xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt new file mode 100644 index 0000000..8636a54 --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt @@ -0,0 +1,401 @@ +package qq.service.msg + +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qqnt.kernel.nativeinterface.* +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.msg.api.IMsgService +import io.kritor.message.* +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.ActionMsgException +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.helper.db.ImageDB +import moe.fuqiuluo.shamrock.helper.db.ImageMapping +import moe.fuqiuluo.shamrock.tools.asJsonArray +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.tools.hex2ByteArray +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import moe.fuqiuluo.shamrock.tools.toHexString +import moe.fuqiuluo.shamrock.utils.PlatformUtils +import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER +import qq.service.bdh.RichProtoSvc +import qq.service.contact.ContactHelper +import qq.service.contact.longPeer +import kotlin.coroutines.resume + +/** + * 将NT消息(com.tencent.qqnt.*)转换为请求消息(io.kritor.message.*)推送 + */ + +private typealias ReqConvertor = suspend (Contact, MsgElement) -> Result + +private object ReqMsgConvertor { + private val convertorMap = hashMapOf( + MsgConstant.KELEMTYPETEXT to ::convertText, + MsgConstant.KELEMTYPEFACE to ::convertFace, + MsgConstant.KELEMTYPEPIC to ::convertImage, + MsgConstant.KELEMTYPEPTT to ::convertVoice, + MsgConstant.KELEMTYPEVIDEO to ::convertVideo, + MsgConstant.KELEMTYPEMARKETFACE to ::convertMarketFace, + MsgConstant.KELEMTYPEARKSTRUCT to ::convertStructJson, + MsgConstant.KELEMTYPEREPLY to ::convertReply, + //MsgConstant.KELEMTYPEGRAYTIP to ::convertGrayTips, + MsgConstant.KELEMTYPEFILE to ::convertFile, + MsgConstant.KELEMTYPEMARKDOWN to ::convertMarkdown, + //MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem, + //MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem, + MsgConstant.KELEMTYPEFACEBUBBLE to ::convertBubbleFace, + MsgConstant.KELEMTYPEINLINEKEYBOARD to ::convertInlineKeyboard + ) + + suspend fun convertText(contact: Contact, element: MsgElement): Result { + val text = element.textElement + val elem = Element.newBuilder() + if (text.atType != MsgConstant.ATTYPEUNKNOWN) { + elem.setAt(atElement { + this.uid = text.atNtUid + this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong() + }) + } else { + elem.setText(textElement { + this.text = text.content + }) + } + return Result.success(elem.build()) + } + + suspend fun convertFace(contact: Contact, element: MsgElement): Result { + val face = element.faceElement + val elem = Element.newBuilder() + if (face.faceType == 5) { + elem.setPoke(pokeElement { + this.id = face.vaspokeId + this.type = face.pokeType + this.strength = face.pokeStrength + }) + } else { + when(face.faceIndex) { + 114 -> elem.setBasketball(basketballElement { + this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0 + }) + 358 -> elem.setDice(diceElement { + this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0 + }) + 359 -> elem.setRps(rpsElement { + this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0 + }) + 394 -> elem.setFace(faceElement { + this.id = face.faceIndex + this.isBig = face.faceType == 3 + this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1 + }) + else -> elem.setFace(faceElement { + this.id = face.faceIndex + this.isBig = face.faceType == 3 + }) + } + } + return Result.success(elem.build()) + } + + suspend fun convertImage(contact: Contact, element: MsgElement): Result { + val image = element.picElement + val md5 = (image.md5HexStr ?: image.fileName + .replace("{", "") + .replace("}", "") + .replace("-", "").split(".")[0]) + .uppercase() + + var storeId = 0 + if (PlatformUtils.getQQVersionCode() > QQ_9_0_8_VER) { + storeId = image.storeID + } + + ImageDB.getInstance().imageMappingDao().insert( + ImageMapping( + fileName = md5, + md5 = md5, + chatType = contact.chatType, + size = image.fileSize, + sha = "", + fileId = image.fileUuid, + storeId = storeId, + ) + ) + + val originalUrl = image.originImageUrl ?: "" + LogCenter.log({ "receive image: $image" }, Level.DEBUG) + + val elem = Element.newBuilder() + elem.setImage(imageElement { + this.file = md5 + this.url = when (contact.chatType) { + MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl( + originalUrl = originalUrl, + md5 = md5, + fileId = image.fileUuid, + width = image.picWidth.toUInt(), + height = image.picHeight.toUInt(), + sha = "", + fileSize = image.fileSize.toULong(), + peer = contact.longPeer().toString() + ) + + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl( + originalUrl = originalUrl, + md5 = md5, + fileId = image.fileUuid, + width = image.picWidth.toUInt(), + height = image.picHeight.toUInt(), + sha = "", + fileSize = image.fileSize.toULong(), + peer = contact.longPeer().toString(), + storeId = storeId + ) + + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl( + originalUrl = originalUrl, + md5 = md5, + fileId = image.fileUuid, + width = image.picWidth.toUInt(), + height = image.picHeight.toUInt(), + sha = "", + fileSize = image.fileSize.toULong(), + peer = contact.longPeer().toString(), + subPeer ="0" + ) + + else -> throw UnsupportedOperationException("Not supported chat type: ${contact.chatType}") + } + this.type = if (image.isFlashPic == true) ImageType.FLASH else if (image.original) ImageType.ORIGIN else ImageType.COMMON + this.subType = image.picSubType + }) + + return Result.success(elem.build()) + } + + suspend fun convertVoice(contact: Contact, element: MsgElement): Result { + val ptt = element.pttElement + val elem = Element.newBuilder() + + val md5 = if (ptt.fileName.startsWith("silk")) + ptt.fileName.substring(5) + else ptt.md5HexStr + + elem.setVoice(voiceElement { + this.url = when (contact.chatType) { + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt.fileUuid) + MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl("0", md5.hex2ByteArray(), ptt.fileUuid) + + else -> throw UnsupportedOperationException("Not supported chat type: ${contact.chatType}") + } + this.file = md5 + this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE + }) + + return Result.success(elem.build()) + } + + suspend fun convertVideo(contact: Contact, element: MsgElement): Result { + val video = element.videoElement + val elem = Element.newBuilder() + val md5 = if (video.fileName.contains("/")) { + video.videoMd5.takeIf { + !it.isNullOrEmpty() + }?.hex2ByteArray() ?: video.fileName.split("/").let { + it[it.size - 2].hex2ByteArray() + } + } else video.fileName.split(".")[0].hex2ByteArray() + elem.setVideo(videoElement { + this.file = md5.toHexString() + this.url = when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) + else -> throw UnsupportedOperationException("Not supported chat type: ${contact.chatType}") + } + }) + return Result.success(elem.build()) + } + + suspend fun convertMarketFace(contact: Contact, element: MsgElement): Result { + val marketFace = element.marketFaceElement + val elem = Element.newBuilder() + return Result.failure(ActionMsgException) + } + + suspend fun convertStructJson(contact: Contact, element: MsgElement): Result { + val data = element.arkElement.bytesData.asJsonObject + val elem = Element.newBuilder() + when (data["app"].asString) { + "com.tencent.multimsg" -> { + val info = data["meta"].asJsonObject["detail"].asJsonObject + elem.setForward(forwardElement { + this.id = info["resid"].asString + this.uniseq = info["uniseq"].asString + this.summary = info["summary"].asString + this.description = info["news"].asJsonArray.joinToString("\n") { + it.asJsonObject["text"].asString + } + }) + } + + "com.tencent.troopsharecard" -> { + val info = data["meta"].asJsonObject["contact"].asJsonObject + elem.setContact(contactElement { + this.scene = Scene.GROUP + this.peer = info["jumpUrl"].asString.split("group_code=")[1] + }) + } + + "com.tencent.contact.lua" -> { + val info = data["meta"].asJsonObject["contact"].asJsonObject + elem.setContact(contactElement { + this.scene = Scene.FRIEND + this.peer = info["jumpUrl"].asString.split("uin=")[1] + }) + } + + "com.tencent.map" -> { + val info = data["meta"].asJsonObject["Location.Search"].asJsonObject + elem.setLocation(locationElement { + this.lat = info["lat"].asString.toFloat() + this.lon = info["lng"].asString.toFloat() + this.address = info["address"].asString + this.title = info["name"].asString + }) + } + + else -> elem.setJson(jsonElement { + this.json = data.toString() + }) + } + return Result.success(elem.build()) + } + + suspend fun convertReply(contact: Contact, element: MsgElement): Result { + val reply = element.replyElement + val elem = Element.newBuilder() + elem.setReply(replyElement { + val msgSeq = reply.replayMsgSeq + val sourceRecords = withTimeoutOrNull(3000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records -> + it.resume(records) + } + } + } + if (sourceRecords.isNullOrEmpty()) { + LogCenter.log("无法查询到回复的消息ID: seq = $msgSeq", Level.WARN) + this.messageId = reply.replayMsgId + } else { + this.messageId = sourceRecords.first().msgId + } + }) + return Result.success(elem.build()) + } + + suspend fun convertFile(contact: Contact, element: MsgElement): Result { + val fileMsg = element.fileElement + val fileName = fileMsg.fileName + val fileSize = fileMsg.fileSize + val expireTime = fileMsg.expireTime ?: 0 + val fileId = fileMsg.fileUuid + val bizId = fileMsg.fileBizId ?: 0 + val fileSubId = fileMsg.fileSubId ?: "" + val url = when (contact.chatType) { + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(contact.guildId, contact.longPeer().toString(), fileId, bizId) + else -> RichProtoSvc.getGroupFileDownUrl(contact.longPeer(), fileId, bizId) + } + val elem = Element.newBuilder() + elem.setFile(fileElement { + this.name = fileName + this.size = fileSize + this.url = url + this.expireTime = expireTime + this.id = fileId + this.subId = fileSubId + this.biz = bizId + }) + return Result.success(elem.build()) + } + + suspend fun convertMarkdown(contact: Contact, element: MsgElement): Result { + val markdown = element.markdownElement + val elem = Element.newBuilder() + elem.setMarkdown(markdownElement { + this.markdown = markdown.content + }) + return Result.success(elem.build()) + } + + suspend fun convertBubbleFace(contact: Contact, element: MsgElement): Result { + val bubbleFace = element.faceBubbleElement + val elem = Element.newBuilder() + elem.setBubbleFace(bubbleFaceElement { + this.id = bubbleFace.yellowFaceInfo.index + this.count = bubbleFace.faceCount ?: 1 + }) + return Result.success(elem.build()) + } + + suspend fun convertInlineKeyboard(contact: Contact, element: MsgElement): Result { + val inlineKeyboard = element.inlineKeyboardElement + val elem = Element.newBuilder() + elem.setButton(buttonElement { + inlineKeyboard.rows.forEach { row -> + this.rows.add(row { + row.buttons.forEach buttonsLoop@ { button -> + if (button == null) return@buttonsLoop + this.buttons.add(button { + this.id = button.id + this.action = buttonAction { + this.type = button.type + this.permission = buttonActionPermission { + this.type = button.permissionType + button.specifyRoleIds?.let { + this.roleIds.addAll(it) + } + button.specifyTinyids?.let { + this.userIds.addAll(it) + } + } + this.unsupportedTips = button.unsupportTips ?: "" + this.data = button.data ?: "" + this.reply = button.isReply + this.enter = button.enter + } + this.renderData = buttonRender { + this.label = button.label ?: "" + this.visitedLabel = button.visitedLabel ?: "" + this.style = button.style + } + }) + } + }) + } + }) + return Result.success(elem.build()) + } + + operator fun get(case: Int): ReqConvertor? { + return convertorMap[case] + } +} + +suspend fun NtMessages.toKritorReqMessages(contact: Contact): ArrayList { + val result = arrayListOf() + forEach { + ReqMsgConvertor[it.elementType]?.invoke(contact, it)?.onSuccess { + result.add(it) + }?.onFailure { + if (it !is ActionMsgException) { + LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN) + } + } + } + return result +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/ticket/TicketHelper.kt b/xposed/src/main/java/qq/service/ticket/TicketHelper.kt new file mode 100644 index 0000000..70bb823 --- /dev/null +++ b/xposed/src/main/java/qq/service/ticket/TicketHelper.kt @@ -0,0 +1,199 @@ +package qq.service.ticket + +import com.tencent.guild.api.transfile.IGuildTransFileApi +import com.tencent.mobileqq.app.QQAppInterface +import com.tencent.mobileqq.pskey.oidb.cmd0x102a.oidb_cmd0x102a +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qqnt.kernel.nativeinterface.BigDataTicket +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.request.get +import io.ktor.client.request.header +import moe.fuqiuluo.shamrock.tools.GlobalClient +import moe.fuqiuluo.shamrock.tools.slice +import mqq.app.MobileQQ +import mqq.manager.TicketManager +import oicq.wlogin_sdk.request.Ticket +import qq.service.QQInterfaces +import tencent.im.oidb.oidb_sso + +internal object TicketHelper: QQInterfaces() { + object SigType { + const val WLOGIN_A5 = 2 + const val WLOGIN_RESERVED = 16 + const val WLOGIN_STWEB = 32 // TLV 103 + const val WLOGIN_A2 = 64 + const val WLOGIN_ST = 128 + const val WLOGIN_AQSIG = 2097152 + const val WLOGIN_D2 = 262144 + const val WLOGIN_DA2 = 33554432 + const val WLOGIN_LHSIG = 4194304 + const val WLOGIN_LSKEY = 512 + const val WLOGIN_OPENKEY = 16384 + const val WLOGIN_PAYTOKEN = 8388608 + const val WLOGIN_PF = 16777216 + const val WLOGIN_PSKEY = 1048576 + const val WLOGIN_PT4Token = 134217728 + const val WLOGIN_QRPUSH = 67108864 + const val WLOGIN_SID = 524288 + const val WLOGIN_SIG64 = 8192 + const val WLOGIN_SKEY = 4096 + const val WLOGIN_TOKEN = 32768 + const val WLOGIN_VKEY = 131072 + + val ALL_TICKET = arrayOf( + WLOGIN_A5, WLOGIN_RESERVED, WLOGIN_STWEB, WLOGIN_A2, WLOGIN_ST, WLOGIN_AQSIG, WLOGIN_D2, WLOGIN_DA2, + WLOGIN_LHSIG, WLOGIN_LSKEY, WLOGIN_OPENKEY, WLOGIN_PAYTOKEN, WLOGIN_PF, WLOGIN_PSKEY, WLOGIN_PT4Token, + WLOGIN_QRPUSH, WLOGIN_SID, WLOGIN_SIG64, WLOGIN_SKEY, WLOGIN_TOKEN, WLOGIN_VKEY + ) + } + + fun getUin(): String { + return app.currentUin.ifBlank { "0" } + } + + fun getLongUin(): Long { + return app.longAccountUin + } + + fun getUid(): String { + return app.currentUid.ifBlank { "u_" } + } + + fun getNickname(): String { + return app.currentNickname + } + + fun getCookie(): String { + val uin = getUin() + val skey = getRealSkey(uin) + val pskey = getPSKey(uin) + return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey" + } + + suspend fun getCookie(domain: String): String { + val uin = getUin() + val skey = getRealSkey(uin) + val pskey = getPSKey(uin, domain) ?: "" + val pt4token = getPt4Token(uin, domain) ?: "" + return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token" + } + + fun getBigdataTicket(): BigDataTicket? { + return runCatching { + QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let { + BigDataTicket(it.getSessionKey(), it.getSessionSig()) + } + }.getOrNull() + } + + fun getCSRF(pskey: String = getPSKey(getUin())): String { + if (pskey.isEmpty()) { + return "0" + } + var v = 5381 + for (element in pskey) { + v += ((v shl 5) + element.code.toLong()).toInt() + } + return (v and Int.MAX_VALUE).toString() + } + + suspend fun getCSRF(uin: String, domain: String): String { + // 是不是要用Skey? + return getBkn(getPSKey(uin, domain) ?: getSKey(uin)) + } + + fun getBkn(arg: String): String { + var v: Long = 5381 + for (element in arg) { + v += (v shl 5 and 2147483647L) + element.code.toLong() + } + return (v and 2147483647L).toString() + } + + fun getTicket(uin: String, id: Int): Ticket? { + require(app is QQAppInterface) + return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getLocalTicket(uin, id) + } + + fun getStWeb(uin: String): String { + require(app is QQAppInterface) + return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getStweb(uin) + } + + fun getSKey(uin: String): String { + require(app is QQAppInterface) + return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getSkey(uin) + } + + fun getRealSkey(uin: String): String { + require(app is QQAppInterface) + return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getRealSkey(uin) + } + + fun getPSKey(uin: String): String { + require(app is QQAppInterface) + val manager = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager) + manager.reloadCache(MobileQQ.getContext()) + return manager.getSuperkey(uin) ?: "" + } + + suspend fun getLessPSKey(vararg domain: String): Result> { + val req = oidb_cmd0x102a.GetPSkeyRequest() + req.domains.set(domain.toList()) + val fromServiceMsg = sendOidbAW("OidbSvcTcp.0x102a", 4138, 0, req.toByteArray()) + ?: return Result.failure(Exception("getLessPSKey failed")) + if (fromServiceMsg.wupBuffer == null) return Result.failure(Exception("getLessPSKey failed: no response")) + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidb_cmd0x102a.GetPSkeyResponse().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + return Result.success(rsp.private_keys.get()) + } + + suspend fun getPSKey(uin: String, domain: String): String? { + require(app is QQAppInterface) + return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPskey(uin, domain).let { + if (it.isNullOrBlank()) + getLessPSKey(domain).getOrNull()?.firstOrNull()?.key?.get() + else it + } + } + + fun getPt4Token(uin: String, domain: String): String? { + require(app is QQAppInterface) + return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPt4Token(uin, domain) + } + + suspend fun getHttpCookies(appid: String, daid: String, jumpurl: String): String? { + val client = HttpClient { + followRedirects = false + + install(HttpTimeout) { + requestTimeoutMillis = 15000 + connectTimeoutMillis = 15000 + socketTimeoutMillis = 15000 + } + } + val uin = getUin() + val clientkey = getStWeb(uin) + var url = "https://ui.ptlogin2.qq.com/cgi-bin/login?pt_hide_ad=1&style=9&appid=$appid&pt_no_auth=1&pt_wxtest=1&daid=$daid&s_url=$jumpurl" + var cookie = client.get(url).headers.getAll("Set-Cookie")?.joinToString(";") + url = "https://ssl.ptlogin2.qq.com/jump?u1=$jumpurl&pt_report=1&daid=$daid&style=9&keyindex=19&clientuin=$uin&clientkey=$clientkey" + client.get(url) { + header("Cookie", cookie) + }.let { + cookie = it.headers.getAll("Set-Cookie")?.joinToString(";") + url = it.headers["Location"].toString() + } + cookie = client.get(url).headers.getAll("Set-Cookie")?.joinToString(";") + val extractedCookie = StringBuilder() + val cookies = cookie?.split(";") + cookies?.filter { cookie -> + val cookiePair = cookie.trim().split("=") + cookiePair.size == 2 && cookiePair[1].isNotBlank() && cookiePair[0].trim() in listOf("uin", "skey", "p_uin", "p_skey", "pt4_token") + }?.forEach { + extractedCookie.append("$it; ") + } + return extractedCookie.toString().trim() + } +} \ No newline at end of file From ee5fcc3403b626d32648086e53089f5757ed3a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Mon, 18 Mar 2024 11:49:38 +0800 Subject: [PATCH 19/20] =?UTF-8?q?`Shamrock`:=20=E7=B2=BE=E5=8D=8E=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .../src/main/java/kritor/service/Grpc.kt | 8 +- .../shamrock/ui/fragment/DashboardFragment.kt | 4 +- .../moe/fuqiuluo/ksp/impl/GrpcProcessor.kt | 96 +++ .../fuqiuluo/ksp/providers/GrpcProvider.kt | 17 + xposed/src/main/assets/config.properties | 2 +- .../main/java/kritor/client/KritorClient.kt | 142 +++++ .../main/java/kritor/handlers/GrpcHandlers.kt | 6 + .../main/java/kritor/server/KritorServer.kt | 2 + .../kritor/service/ForwardMessageService.kt | 50 ++ .../java/kritor/service/GroupFileService.kt | 1 + .../main/java/kritor/service/GroupService.kt | 3 +- .../main/java/kritor/service/KritorService.kt | 15 + .../java/kritor/service/MessageService.kt | 138 +++++ .../main/java/kritor/service/WebService.kt | 72 +++ .../fuqiuluo/shamrock/config/EnableOldBDH.kt | 2 +- .../xposed/actions/InitRemoteService.kt | 19 + .../java/qq/service/bdh/NtV2RichMediaSvc.kt | 2 +- .../qq/service/msg/ForwardMessageHelper.kt | 227 +++++++ .../main/java/qq/service/msg/MessageData.kt | 4 +- .../main/java/qq/service/msg/MessageHelper.kt | 105 ++++ .../java/qq/service/msg/NtMsgConvertor.kt | 56 ++ .../java/qq/service/msg/ReqMultiConvertor.kt | 575 ++++++++++++++++++ .../java/qq/service/ticket/TicketHelper.kt | 10 +- 23 files changed, 1536 insertions(+), 20 deletions(-) create mode 100644 processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt create mode 100644 processor/src/main/java/moe/fuqiuluo/ksp/providers/GrpcProvider.kt create mode 100644 xposed/src/main/java/kritor/client/KritorClient.kt create mode 100644 xposed/src/main/java/kritor/handlers/GrpcHandlers.kt create mode 100644 xposed/src/main/java/kritor/service/ForwardMessageService.kt create mode 100644 xposed/src/main/java/kritor/service/WebService.kt create mode 100644 xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt create mode 100644 xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt diff --git a/annotations/src/main/java/kritor/service/Grpc.kt b/annotations/src/main/java/kritor/service/Grpc.kt index 0a66477..2d6fe33 100644 --- a/annotations/src/main/java/kritor/service/Grpc.kt +++ b/annotations/src/main/java/kritor/service/Grpc.kt @@ -1,7 +1,11 @@ package kritor.service +import kotlin.reflect.KClass + +@Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.FUNCTION) annotation class Grpc( val serviceName: String, - val funcName: String -) \ No newline at end of file + val funcName: String, + +) diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt index 68daf10..2b3bfa2 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt @@ -118,9 +118,7 @@ private fun APIInfoCard( text = rpcAddress, hint = "请输入回调地址", error = "输入的地址不合法", - checker = { - it.isEmpty() || it.contains(":") - }, + checker = { true }, confirm = { ShamrockConfig[ctx, RPCAddress] = rpcAddress.value AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。") diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt b/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt new file mode 100644 index 0000000..adb69ec --- /dev/null +++ b/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt @@ -0,0 +1,96 @@ +@file:Suppress("UNCHECKED_CAST") +@file:OptIn(KspExperimental::class) + +package moe.fuqiuluo.ksp.impl + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getAnnotationsByType +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.getJavaClassByName +import com.google.devtools.ksp.getKotlinClassByName +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeParameter +import com.google.devtools.ksp.symbol.Modifier +import com.google.devtools.ksp.validate +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import kritor.service.Grpc + +class GrpcProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger +): SymbolProcessor { + private val subPackage = arrayOf("contact", "core", "file", "friend", "group", "message", "web") + + override fun process(resolver: Resolver): List { + val symbols = resolver.getSymbolsWithAnnotation(Grpc::class.qualifiedName!!) + val actions = (symbols as Sequence).toList() + + if (actions.isEmpty()) return emptyList() + + // 怎么返回nullable的结果 + val packageName = "kritor.handlers" + val funcBuilder = FunSpec.builder("handleGrpc") + .addModifiers(KModifier.SUSPEND) + .addParameter("cmd", String::class) + .addParameter("data", ByteArray::class) + .returns(ByteArray::class) + val fileSpec = FileSpec.scriptBuilder("AutoGrpcHandlers", packageName) + + logger.warn("Found ${actions.size} grpc-actions") + + //logger.error(resolver.getClassDeclarationByName("io.kritor.AuthReq").toString()) + //logger.error(resolver.getJavaClassByName("io.kritor.AuthReq").toString()) + //logger.error(resolver.getKotlinClassByName("io.kritor.AuthReq").toString()) + + actions.forEach { action -> + val methodName = action.qualifiedName?.asString()!! + val grpcMethod = action.getAnnotationsByType(Grpc::class).first() + val service = grpcMethod.serviceName + val funcName = grpcMethod.funcName + funcBuilder.addStatement("if (cmd == \"${service}.${funcName}\") {\t") + + val reqType = action.parameters[0].type.toString() + val rspType = action.returnType.toString() + funcBuilder.addStatement("val resp: $rspType = $methodName($reqType.parseFrom(data))") + funcBuilder.addStatement("return resp.toByteArray()") + + funcBuilder.addStatement("}") + } + funcBuilder.addStatement("return EMPTY_BYTE_ARRAY") + fileSpec + .addStatement("import io.kritor.*") + .addStatement("import io.kritor.core.*") + .addStatement("import io.kritor.contact.*") + .addStatement("import io.kritor.group.*") + .addStatement("import io.kritor.friend.*") + .addStatement("import io.kritor.file.*") + .addStatement("import io.kritor.message.*") + .addStatement("import io.kritor.web.*") + .addFunction(funcBuilder.build()) + .addImport("moe.fuqiuluo.symbols", "EMPTY_BYTE_ARRAY") + runCatching { + codeGenerator.createNewFile( + dependencies = Dependencies(aggregating = false), + packageName = packageName, + fileName = fileSpec.name + ).use { outputStream -> + outputStream.writer().use { + fileSpec.build().writeTo(it) + } + } + } + + return emptyList() + } +} \ No newline at end of file diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/providers/GrpcProvider.kt b/processor/src/main/java/moe/fuqiuluo/ksp/providers/GrpcProvider.kt new file mode 100644 index 0000000..008110c --- /dev/null +++ b/processor/src/main/java/moe/fuqiuluo/ksp/providers/GrpcProvider.kt @@ -0,0 +1,17 @@ +package moe.fuqiuluo.ksp.providers + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import moe.fuqiuluo.ksp.impl.GrpcProcessor + +@AutoService(SymbolProcessorProvider::class) +class GrpcProvider: SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return GrpcProcessor( + environment.codeGenerator, + environment.logger + ) + } +} \ No newline at end of file diff --git a/xposed/src/main/assets/config.properties b/xposed/src/main/assets/config.properties index 6505ef1..b715d29 100644 --- a/xposed/src/main/assets/config.properties +++ b/xposed/src/main/assets/config.properties @@ -34,7 +34,7 @@ active_ticket= enable_self_message=false # 旧BDH兼容开关 -enable_old_bdh=false +enable_old_bdh=true # 反JVM调用栈跟踪 anti_jvm_trace=true diff --git a/xposed/src/main/java/kritor/client/KritorClient.kt b/xposed/src/main/java/kritor/client/KritorClient.kt new file mode 100644 index 0000000..c31a0f1 --- /dev/null +++ b/xposed/src/main/java/kritor/client/KritorClient.kt @@ -0,0 +1,142 @@ +@file:OptIn(DelicateCoroutinesApi::class) +package kritor.client + +import com.google.protobuf.ByteString +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import io.kritor.ReverseServiceGrpcKt +import io.kritor.event.EventServiceGrpcKt +import io.kritor.event.EventType +import io.kritor.event.eventStructure +import io.kritor.event.messageEvent +import io.kritor.reverse.ReqCode +import io.kritor.reverse.Request +import io.kritor.reverse.Response +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch +import kritor.handlers.handleGrpc +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter +import kotlin.time.Duration.Companion.seconds + +internal class KritorClient( + val host: String, + val port: Int +) { + private lateinit var channel: ManagedChannel + + private lateinit var channelJob: Job + private val senderChannel = MutableSharedFlow() + + fun start() { + runCatching { + if (::channel.isInitialized && isActive()){ + channel.shutdown() + } + channel = ManagedChannelBuilder + .forAddress(host, port) + .usePlaintext() + .enableRetry() // 允许尝试 + .executor(Dispatchers.IO.asExecutor()) // 使用协程的调度器 + .build() + }.onFailure { + LogCenter.log("KritorClient start failed: ${it.stackTraceToString()}", Level.ERROR) + } + } + + fun listen(retryCnt: Int = -1) { + if (::channelJob.isInitialized && channelJob.isActive) { + channelJob.cancel() + } + channelJob = GlobalScope.launch(Dispatchers.IO) { + runCatching { + val stub = ReverseServiceGrpcKt.ReverseServiceCoroutineStub(channel) + registerEvent(EventType.EVENT_TYPE_MESSAGE) + registerEvent(EventType.EVENT_TYPE_CORE_EVENT) + registerEvent(EventType.EVENT_TYPE_REQUEST) + registerEvent(EventType.EVENT_TYPE_NOTICE) + stub.reverseStream(channelFlow { + senderChannel.collect { send(it) } + }).collect { + onReceive(it) + } + }.onFailure { + LogCenter.log("KritorClient listen failed, retry after 15s: ${it.stackTraceToString()}", Level.WARN) + } + delay(15.seconds) + LogCenter.log("KritorClient listen retrying, retryCnt = $retryCnt", Level.WARN) + if (retryCnt != 0) listen(retryCnt - 1) + } + } + + fun registerEvent(eventType: EventType) { + GlobalScope.launch(Dispatchers.IO) { + runCatching { + EventServiceGrpcKt.EventServiceCoroutineStub(channel).registerPassiveListener(channelFlow { + when(eventType) { + EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent { + send(eventStructure { + this.type = EventType.EVENT_TYPE_MESSAGE + this.message = it.second + }) + } + EventType.EVENT_TYPE_CORE_EVENT -> {} + EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onNoticeEvent { + send(eventStructure { + this.type = EventType.EVENT_TYPE_NOTICE + this.notice = it + }) + } + EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onRequestEvent { + send(eventStructure { + this.type = EventType.EVENT_TYPE_REQUEST + this.request = it + }) + } + EventType.UNRECOGNIZED -> {} + } + }) + }.onFailure { + LogCenter.log("KritorClient registerEvent failed: ${it.stackTraceToString()}", Level.ERROR) + } + } + } + + private suspend fun onReceive(request: Request) = GlobalScope.launch { + //LogCenter.log("KritorClient onReceive: $request") + runCatching { + val rsp = handleGrpc(request.cmd, request.buf.toByteArray()) + senderChannel.emit(Response.newBuilder() + .setCmd(request.cmd) + .setCode(ReqCode.SUCCESS) + .setMsg("success") + .setSeq(request.seq) + .setBuf(ByteString.copyFrom(rsp)) + .build()) + }.onFailure { + senderChannel.emit(Response.newBuilder() + .setCmd(request.cmd) + .setCode(ReqCode.INTERNAL) + .setMsg(it.stackTraceToString()) + .setSeq(request.seq) + .setBuf(ByteString.EMPTY) + .build()) + } + } + + fun isActive(): Boolean { + return !channel.isShutdown + } + + fun close() { + channel.shutdown() + } +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/handlers/GrpcHandlers.kt b/xposed/src/main/java/kritor/handlers/GrpcHandlers.kt new file mode 100644 index 0000000..9bd3aa8 --- /dev/null +++ b/xposed/src/main/java/kritor/handlers/GrpcHandlers.kt @@ -0,0 +1,6 @@ +package kritor.handlers + +internal object GrpcHandlers { + + +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index 9496286..c073ea8 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -26,6 +26,8 @@ class KritorServer( .addService(GroupFileService) .addService(MessageService) .addService(EventService) + .addService(ForwardMessageService) + .addService(WebService) .build()!! fun start(block: Boolean = false) { diff --git a/xposed/src/main/java/kritor/service/ForwardMessageService.kt b/xposed/src/main/java/kritor/service/ForwardMessageService.kt new file mode 100644 index 0000000..a6b91f6 --- /dev/null +++ b/xposed/src/main/java/kritor/service/ForwardMessageService.kt @@ -0,0 +1,50 @@ +package kritor.service + +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgElement +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.message.Element +import io.kritor.message.ElementType +import io.kritor.message.ForwardMessageRequest +import io.kritor.message.ForwardMessageResponse +import io.kritor.message.ForwardMessageServiceGrpcKt +import io.kritor.message.Scene +import io.kritor.message.element +import io.kritor.message.forwardMessageResponse +import qq.service.contact.longPeer +import qq.service.msg.ForwardMessageHelper +import qq.service.msg.MessageHelper +import qq.service.msg.NtMsgConvertor + +internal object ForwardMessageService: ForwardMessageServiceGrpcKt.ForwardMessageServiceCoroutineImplBase() { + @Grpc("ForwardMessageService", "ForwardMessage") + override suspend fun forwardMessage(request: ForwardMessageRequest): ForwardMessageResponse { + val contact = request.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + + val forwardMessage = ForwardMessageHelper.uploadMultiMsg(contact.chatType, contact.longPeer().toString(), contact.guildId, request.messagesList).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withCause(it)) + }.getOrThrow() + + val uniseq = MessageHelper.generateMsgId(contact.chatType) + return forwardMessageResponse { + this.messageId = MessageHelper.sendMessage(contact, NtMsgConvertor.convertToNtMsgs(contact, uniseq, arrayListOf(element { + this.type = ElementType.FORWARD + this.forward = forwardMessage + })), request.retryCount, uniseq).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withCause(it)) + }.getOrThrow() + this.resId = forwardMessage.id + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/GroupFileService.kt b/xposed/src/main/java/kritor/service/GroupFileService.kt index 1daaa63..b67685a 100644 --- a/xposed/src/main/java/kritor/service/GroupFileService.kt +++ b/xposed/src/main/java/kritor/service/GroupFileService.kt @@ -118,6 +118,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti return renameFolderResponse { } } + @Grpc("GroupFileService", "GetFileSystemInfo") override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse { return getGroupFileSystemInfo(request.groupId) } diff --git a/xposed/src/main/java/kritor/service/GroupService.kt b/xposed/src/main/java/kritor/service/GroupService.kt index 9563b50..8f98627 100644 --- a/xposed/src/main/java/kritor/service/GroupService.kt +++ b/xposed/src/main/java/kritor/service/GroupService.kt @@ -61,7 +61,6 @@ import io.kritor.group.prohibitedUserInfo import io.kritor.group.setGroupAdminResponse import io.kritor.group.setGroupUniqueTitleResponse import io.kritor.group.setGroupWholeBanResponse -import moe.fuqiuluo.shamrock.helper.TroopHonorHelper import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty import qq.service.contact.ContactHelper @@ -89,7 +88,7 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase() } } - @Grpc("GroupService", "PokeMember") + @Grpc("GroupService", "PokeMember", ) override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse { GroupHelper.pokeMember(request.groupId, when(request.targetCase!!) { PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin diff --git a/xposed/src/main/java/kritor/service/KritorService.kt b/xposed/src/main/java/kritor/service/KritorService.kt index 45eb251..2190d3c 100644 --- a/xposed/src/main/java/kritor/service/KritorService.kt +++ b/xposed/src/main/java/kritor/service/KritorService.kt @@ -10,6 +10,8 @@ import io.kritor.core.DownloadFileRequest import io.kritor.core.DownloadFileResponse import io.kritor.core.GetCurrentAccountRequest import io.kritor.core.GetCurrentAccountResponse +import io.kritor.core.GetDeviceBatteryRequest +import io.kritor.core.GetDeviceBatteryResponse import io.kritor.core.GetVersionRequest import io.kritor.core.GetVersionResponse import io.kritor.core.KritorServiceGrpcKt @@ -18,6 +20,7 @@ import io.kritor.core.SwitchAccountResponse import io.kritor.core.clearCacheResponse import io.kritor.core.downloadFileResponse import io.kritor.core.getCurrentAccountResponse +import io.kritor.core.getDeviceBatteryResponse import io.kritor.core.getVersionResponse import io.kritor.core.switchAccountResponse import moe.fuqiuluo.shamrock.tools.ShamrockVersion @@ -25,6 +28,7 @@ import moe.fuqiuluo.shamrock.utils.DownloadUtils import moe.fuqiuluo.shamrock.utils.FileUtils import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.utils.MMKVFetcher +import moe.fuqiuluo.shamrock.utils.PlatformUtils import mqq.app.MobileQQ import qq.service.QQInterfaces import qq.service.QQInterfaces.Companion.app @@ -118,4 +122,15 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() { } return switchAccountResponse { } } + + @Grpc("KritorService", "GetDeviceBattery") + override suspend fun getDeviceBattery(request: GetDeviceBatteryRequest): GetDeviceBatteryResponse { + return getDeviceBatteryResponse { + PlatformUtils.getDeviceBattery().let { + this.battery = it.battery + this.scale = it.scale + this.status = it.status + } + } + } } \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/MessageService.kt b/xposed/src/main/java/kritor/service/MessageService.kt index efb3e16..0d2ad7b 100644 --- a/xposed/src/main/java/kritor/service/MessageService.kt +++ b/xposed/src/main/java/kritor/service/MessageService.kt @@ -9,6 +9,10 @@ import io.grpc.Status import io.grpc.StatusRuntimeException import io.kritor.message.ClearMessagesRequest import io.kritor.message.ClearMessagesResponse +import io.kritor.message.DeleteEssenceMsgRequest +import io.kritor.message.DeleteEssenceMsgResponse +import io.kritor.message.GetEssenceMessagesRequest +import io.kritor.message.GetEssenceMessagesResponse import io.kritor.message.GetForwardMessagesRequest import io.kritor.message.GetForwardMessagesResponse import io.kritor.message.GetHistoryMessageRequest @@ -25,8 +29,15 @@ import io.kritor.message.SendMessageByResIdRequest import io.kritor.message.SendMessageByResIdResponse import io.kritor.message.SendMessageRequest import io.kritor.message.SendMessageResponse +import io.kritor.message.SetEssenceMessageRequest +import io.kritor.message.SetEssenceMessageResponse +import io.kritor.message.SetMessageCommentEmojiRequest +import io.kritor.message.SetMessageCommentEmojiResponse import io.kritor.message.clearMessagesResponse import io.kritor.message.contact +import io.kritor.message.deleteEssenceMsgResponse +import io.kritor.message.essenceMessage +import io.kritor.message.getEssenceMessagesResponse import io.kritor.message.getForwardMessagesResponse import io.kritor.message.getHistoryMessageResponse import io.kritor.message.getMessageBySeqResponse @@ -36,6 +47,8 @@ import io.kritor.message.recallMessageResponse import io.kritor.message.sendMessageByResIdResponse import io.kritor.message.sendMessageResponse import io.kritor.message.sender +import io.kritor.message.setEssenceMessageResponse +import io.kritor.message.setMessageCommentEmojiResponse import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.shamrock.helper.Level @@ -63,6 +76,7 @@ import kotlin.random.Random import kotlin.random.nextUInt internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImplBase() { + @Grpc("MessageService", "SendMessage") override suspend fun sendMessage(request: SendMessageRequest): SendMessageResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -84,6 +98,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } + @Grpc("MessageService", "SendMessageByResId") override suspend fun sendMessageByResId(request: SendMessageByResIdRequest): SendMessageByResIdResponse { val contact = request.contact val req = PbSendMsgReq( @@ -113,6 +128,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl return sendMessageByResIdResponse { } } + @Grpc("MessageService", "ClearMessages") override suspend fun clearMessages(request: ClearMessagesRequest): ClearMessagesResponse { val contact = request.contact val kernelService = NTServiceFetcher.kernelService @@ -131,6 +147,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl return clearMessagesResponse { } } + @Grpc("MessageService", "RecallMessage") override suspend fun recallMessage(request: RecallMessageRequest): RecallMessageResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -155,6 +172,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl return recallMessageResponse {} } + @Grpc("MessageService", "GetForwardMessages") override suspend fun getForwardMessages(request: GetForwardMessagesRequest): GetForwardMessagesResponse { return getForwardMessagesResponse { MessageHelper.getForwardMsg(request.resId).onFailure { @@ -195,6 +213,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } + @Grpc("MessageService", "GetMessage") override suspend fun getMessage(request: GetMessageRequest): GetMessageResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -239,6 +258,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } + @Grpc("MessageService", "GetMessageBySeq") override suspend fun getMessageBySeq(request: GetMessageBySeqRequest): GetMessageBySeqResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -283,6 +303,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } + @Grpc("MessageService", "GetHistoryMessage") override suspend fun getHistoryMessage(request: GetHistoryMessageRequest): GetHistoryMessageResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -328,4 +349,121 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } } + + @Grpc("MessageService", "DeleteEssenceMsg") + override suspend fun deleteEssenceMsg(request: DeleteEssenceMsgRequest): DeleteEssenceMsgResponse { + val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString()) + val msg: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsByMsgId(contact, arrayListOf(request.messageId)) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found")) + if(MessageHelper.deleteEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null) + throw StatusRuntimeException(Status.NOT_FOUND.withDescription("delete essence message failed")) + return deleteEssenceMsgResponse { } + } + + @Grpc("MessageService", "GetEssenceMessages") + override suspend fun getEssenceMessages(request: GetEssenceMessagesRequest): GetEssenceMessagesResponse { + val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString()) + return getEssenceMessagesResponse { + MessageHelper.getEssenceMessageList(request.groupId, request.page, request.pageSize).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withCause(it)) + }.getOrThrow().forEach { + essenceMessage.add(essenceMessage { + withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsBySeqAndCount(contact, it.messageSeq, 1, true) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + }?.let { + this.messageId = it.msgId + } + this.messageSeq = it.messageSeq + this.msgTime = it.senderTime.toInt() + this.senderNick = it.senderNick + this.senderUin = it.senderId + this.operationTime = it.operatorTime.toInt() + this.operatorNick = it.operatorNick + this.operatorUin = it.operatorId + this.jsonElements = it.messageContent.toString() + }) + } + } + } + + @Grpc("MessageService", "SetEssenceMessage") + override suspend fun setEssenceMessage(request: SetEssenceMessageRequest): SetEssenceMessageResponse { + val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString()) + val msg: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsByMsgId(contact, arrayListOf(request.messageId)) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found")) + if (MessageHelper.setEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null) { + throw StatusRuntimeException(Status.NOT_FOUND.withDescription("set essence message failed")) + } + return setEssenceMessageResponse { } + } + + @Grpc("MessageService", "SetMessageCommentEmoji") + override suspend fun setMessageCommentEmoji(request: SetMessageCommentEmojiRequest): SetMessageCommentEmojiResponse { + val contact = request.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + val msg: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsByMsgId(contact, arrayListOf(request.messageId)) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found")) + MessageHelper.setGroupMessageCommentFace(request.contact.longPeer(), msg.msgSeq.toULong(), request.faceId.toString(), request.isComment) + return setMessageCommentEmojiResponse { } + } } \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/WebService.kt b/xposed/src/main/java/kritor/service/WebService.kt new file mode 100644 index 0000000..81e3def --- /dev/null +++ b/xposed/src/main/java/kritor/service/WebService.kt @@ -0,0 +1,72 @@ +package kritor.service + +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.web.GetCSRFTokenRequest +import io.kritor.web.GetCSRFTokenResponse +import io.kritor.web.GetCookiesRequest +import io.kritor.web.GetCookiesResponse +import io.kritor.web.GetCredentialsRequest +import io.kritor.web.GetCredentialsResponse +import io.kritor.web.GetHttpCookiesRequest +import io.kritor.web.GetHttpCookiesResponse +import io.kritor.web.WebServiceGrpcKt +import io.kritor.web.getCSRFTokenResponse +import io.kritor.web.getCookiesResponse +import io.kritor.web.getCredentialsResponse +import io.kritor.web.getHttpCookiesResponse +import qq.service.ticket.TicketHelper + +internal object WebService: WebServiceGrpcKt.WebServiceCoroutineImplBase() { + @Grpc("WebService", "GetCookies") + override suspend fun getCookies(request: GetCookiesRequest): GetCookiesResponse { + return getCookiesResponse { + if (request.domain.isNullOrEmpty()) { + this.cookie = TicketHelper.getCookie() + } else { + this.cookie = TicketHelper.getCookie(request.domain) + } + } + } + + @Grpc("WebService", "GetCredentials") + override suspend fun getCredentials(request: GetCredentialsRequest): GetCredentialsResponse { + return getCredentialsResponse { + if (request.domain.isNullOrEmpty()) { + val uin = TicketHelper.getUin() + val skey = TicketHelper.getRealSkey(uin) + val pskey = TicketHelper.getPSKey(uin) + this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey;" + this.bkn = TicketHelper.getCSRF(pskey) + } else { + val uin = TicketHelper.getUin() + val skey = TicketHelper.getRealSkey(uin) + val pskey = TicketHelper.getPSKey(uin, request.domain) ?: "" + val pt4token = TicketHelper.getPt4Token(uin, request.domain) ?: "" + this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token;" + this.bkn = TicketHelper.getCSRF(pskey) + } + } + } + + @Grpc("WebService", "GetCSRFToken") + override suspend fun getCSRFToken(request: GetCSRFTokenRequest): GetCSRFTokenResponse { + return getCSRFTokenResponse { + if (request.domain.isNullOrEmpty()) { + this.bkn = TicketHelper.getCSRF() + } else { + this.bkn = TicketHelper.getCSRF(TicketHelper.getUin(), request.domain) + } + } + } + + @Grpc("WebService", "GetHttpCookies") + override suspend fun getHttpCookies(request: GetHttpCookiesRequest): GetHttpCookiesResponse { + return getHttpCookiesResponse { + this.cookie = TicketHelper.getHttpCookies(request.appid, request.daid, request.jumpUrl) + ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get http cookies")) + } + } + + +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt index e723c80..2af8dbc 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt @@ -2,5 +2,5 @@ package moe.fuqiuluo.shamrock.config object EnableOldBDH: ConfigKey() { override fun name() = "enable_old_bdh" - override fun default() = false + override fun default() = true } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt index 0be7f92..8bce023 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt @@ -6,8 +6,11 @@ import android.content.Context import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import kritor.client.KritorClient import kritor.server.KritorServer import moe.fuqiuluo.shamrock.config.ActiveRPC +import moe.fuqiuluo.shamrock.config.PassiveRPC +import moe.fuqiuluo.shamrock.config.RPCAddress import moe.fuqiuluo.shamrock.config.RPCPort import moe.fuqiuluo.shamrock.config.ShamrockConfig import moe.fuqiuluo.shamrock.config.get @@ -17,6 +20,7 @@ import moe.fuqiuluo.symbols.Process import moe.fuqiuluo.symbols.XposedHook private lateinit var server: KritorServer +private lateinit var client: KritorClient @XposedHook(Process.MAIN, priority = 10) internal class InitRemoteService : IAction { @@ -32,6 +36,21 @@ internal class InitRemoteService : IAction { LogCenter.log("ActiveRPC is disabled, KritorServer will not be started.") } + if (PassiveRPC.get()) { + if (!::client.isInitialized) { + val hostAndPort = RPCAddress.get().split(":").let { + it.first() to it.last().toInt() + } + LogCenter.log("Connect RPC to ${hostAndPort.first}:${hostAndPort.second}") + client = KritorClient(hostAndPort.first, hostAndPort.second) + client.start() + client.listen() + + } + } else { + LogCenter.log("PassiveRPC is disabled, KritorServer will not be started.") + } + }.onFailure { LogCenter.log("Start RPC failed: ${it.message}", Level.ERROR) diff --git a/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt index 233a55e..e25ffd7 100644 --- a/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt +++ b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt @@ -65,7 +65,7 @@ import kotlin.time.Duration.Companion.seconds internal object NtV2RichMediaSvc: QQInterfaces() { private val requestIdSeq = atomic(1L) - private fun fetchGroupResUploadTo(): String { + fun fetchGroupResUploadTo(): String { return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!! } diff --git a/xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt b/xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt new file mode 100644 index 0000000..eb1c8a4 --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt @@ -0,0 +1,227 @@ +package qq.service.msg + +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.msg.api.IMsgService +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.message.Element +import io.kritor.message.ElementType +import io.kritor.message.ForwardElement +import io.kritor.message.ForwardMessageBody +import io.kritor.message.Scene +import io.kritor.message.forwardElement +import io.kritor.message.nodeOrNull +import io.kritor.message.senderOrNull +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.shamrock.utils.DeflateTools +import moe.fuqiuluo.symbols.decodeProtobuf +import protobuf.auto.toByteArray +import protobuf.message.* +import protobuf.message.longmsg.* +import qq.service.QQInterfaces +import qq.service.contact.ContactHelper +import qq.service.msg.MessageHelper.getMultiMsg +import qq.service.ticket.TicketHelper +import java.util.UUID +import kotlin.coroutines.resume +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds + +internal object ForwardMessageHelper: QQInterfaces() { + suspend fun uploadMultiMsg( + chatType: Int, + peerId: String, + fromId: String = peerId, + messages: List, + ): Result { + var i = -1 + val desc = MutableList(messages.size) { "" } + val forwardMsg = mutableMapOf() + + val msgs = messages.mapNotNull { msg -> + kotlin.runCatching { + val contact = msg.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + val node = msg.elementsList.find { it.type == ElementType.NODE }?.nodeOrNull + if (node != null) { + val msgId = node.messageId + val record: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsByMsgId(contact, arrayListOf(msgId)) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: error("合并转发消息节点消息(id = $msgId)获取失败") + PushMsgBody( + msgHead = ResponseHead( + peerUid = record.senderUid, + receiverUid = record.peerUid, + forward = ResponseForward( + friendName = record.sendNickName + ), + responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp( + groupCode = record.peerUin.toULong(), + memberCard = record.sendMemberName, + u1 = 2 + ) else null + ), + contentHead = ContentHead( + msgType = when (record.chatType) { + MsgConstant.KCHATTYPEC2C -> 9 + MsgConstant.KCHATTYPEGROUP -> 82 + else -> throw UnsupportedOperationException("Unsupported chatType: $chatType") + }, + msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, + divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, + msgViaRandom = record.msgId, + sequence = record.msgSeq, // idk what this is(i++) + msgTime = record.msgTime, + u2 = 1, + u6 = 0, + u7 = 0, + msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm + forwardHead = ForwardHead( + u1 = 0, + u2 = 0, + u3 = 0, + ub641 = "", + avatar = "" + ) + ), + body = MsgBody( + richText = record.elements.toKritorReqMessages(contact).toRichText(contact).onFailure { + error("消息合成失败: ${it.stackTraceToString()}") + }.onSuccess { + desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first + }.getOrThrow().second + ) + ) + } else { + PushMsgBody( + msgHead = ResponseHead( + peer = msg.senderOrNull?.uin ?: TicketHelper.getUin().toLong(), + peerUid = msg.senderOrNull?.uid ?: TicketHelper.getUid(), + receiverUid = TicketHelper.getUid(), + forward = ResponseForward( + friendName = msg.senderOrNull?.nick ?: TicketHelper.getNickname() + ) + ), + contentHead = ContentHead( + msgType = 9, + msgSubType = 175, + divSeq = 175, + msgViaRandom = Random.nextLong(), + sequence = msg.messageSeq.toLong(), + msgTime = msg.messageTime.toLong(), + u2 = 1, + u6 = 0, + u7 = 0, + msgSeq = msg.messageSeq.toLong(), + forwardHead = ForwardHead( + u1 = 0, + u2 = 0, + u3 = 2, + ub641 = "", + avatar = "" + ) + ), + body = MsgBody( + richText = msg.elementsList.toRichText(contact).onSuccess { + desc[++i] = (msg.senderOrNull?.nick ?: TicketHelper.getNickname()) + ": " + it.first + }.onFailure { + error("消息合成失败: ${it.stackTraceToString()}") + }.getOrThrow().second + ) + ) + } + }.onFailure { + LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN) + }.getOrNull() + }.ifEmpty { + return Result.failure(Exception("消息节点为空")) + } + + val payload = LongMsgPayload( + action = mutableListOf( + LongMsgAction( + command = "MultiMsg", + data = LongMsgContent( + body = msgs + ) + ) + ).apply { + forwardMsg.map { msg -> + addAll(getMultiMsg(msg.value).getOrElse { return Result.failure(Exception("无法获取嵌套转发消息: $it")) } + .map { action -> + if (action.command == "MultiMsg") LongMsgAction( + command = msg.key, + data = action.data + ) else action + }) + } + } + ) + + val req = LongMsgReq( + sendInfo = when (chatType) { + MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo( + type = 1, + uid = LongMsgUid(if(peerId.startsWith("u_")) peerId else ContactHelper.getUidByUinAsync(peerId.toLong()) ), + payload = DeflateTools.gzip(payload.toByteArray()) + ) + MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo( + type = 3, + uid = LongMsgUid(fromId), + groupUin = fromId.toULong(), + payload = DeflateTools.gzip(payload.toByteArray()) + ) + else -> throw UnsupportedOperationException("Unsupported chatType: $chatType") + }, + setting = LongMsgSettings( + field1 = 4, + field2 = 2, + field3 = 9, + field4 = 0 + ) + ).toByteArray() + + val fromServiceMsg = sendBufferAW("trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", true, req, timeout = 60.seconds) + ?: return Result.failure(Exception("unable to upload multi message, response timeout")) + val rsp = runCatching { + fromServiceMsg.wupBuffer.slice(4).decodeProtobuf() + }.getOrElse { + fromServiceMsg.wupBuffer.decodeProtobuf() + } + val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message")) + + return Result.success(forwardElement { + this.id = resId + this.summary = summary + this.uniseq = UUID.randomUUID().toString() + this.description = desc.slice(0..if (i < 3) i else 3).joinToString("\n") + }) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MessageData.kt b/xposed/src/main/java/qq/service/msg/MessageData.kt index 6df7a32..1553530 100644 --- a/xposed/src/main/java/qq/service/msg/MessageData.kt +++ b/xposed/src/main/java/qq/service/msg/MessageData.kt @@ -64,8 +64,6 @@ internal data class EssenceMessage( @SerialName("operator_id") val operatorId: Long, @SerialName("operator_nick") val operatorNick: String, @SerialName("operator_time") val operatorTime: Long, - @SerialName("message_id") var messageId: Int, - @SerialName("message_seq") val messageSeq: Int, - @SerialName("real_id") val realId: Int, + @SerialName("message_seq") val messageSeq: Long, @SerialName("message_content") val messageContent: JsonElement, ) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MessageHelper.kt b/xposed/src/main/java/qq/service/msg/MessageHelper.kt index 8f99497..a70edb3 100644 --- a/xposed/src/main/java/qq/service/msg/MessageHelper.kt +++ b/xposed/src/main/java/qq/service/msg/MessageHelper.kt @@ -11,11 +11,26 @@ import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo import com.tencent.qqnt.msg.api.IMsgService +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.header import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.jsonObject import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY +import moe.fuqiuluo.shamrock.tools.EmptyJsonArray +import moe.fuqiuluo.shamrock.tools.GlobalClient +import moe.fuqiuluo.shamrock.tools.asInt +import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asLong +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.tools.asStringOrNull import moe.fuqiuluo.shamrock.tools.slice import moe.fuqiuluo.shamrock.tools.toHexString import moe.fuqiuluo.shamrock.utils.DeflateTools @@ -28,14 +43,104 @@ import protobuf.message.longmsg.LongMsgRsp import protobuf.message.longmsg.LongMsgSettings import protobuf.message.longmsg.LongMsgUid import protobuf.message.longmsg.RecvLongMsgInfo +import protobuf.oidb.cmd0x9082.Oidb0x9082 import qq.service.QQInterfaces import qq.service.contact.ContactHelper import qq.service.internals.msgService +import qq.service.ticket.TicketHelper +import tencent.im.oidb.cmd0xeac.oidb_0xeac +import tencent.im.oidb.oidb_sso import kotlin.coroutines.resume typealias MessageId = Long internal object MessageHelper: QQInterfaces() { + suspend fun getEssenceMessageList(groupId: Long, page: Int = 0, pageSize: Int = 20): Result>{ + val cookie = TicketHelper.getCookie("qun.qq.com") + val bkn = TicketHelper.getBkn(TicketHelper.getRealSkey(TicketHelper.getUin())) + val url = "https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=${bkn}&group_code=${groupId}&page_start=${page}&page_limit=${pageSize}" + val response = GlobalClient.get(url) { + header("Cookie", cookie) + } + val body = Json.decodeFromStream(response.body()) + if (body.jsonObject["retcode"].asInt == 0) { + val data = body.jsonObject["data"].asJsonObject + val list = data["msg_list"].asJsonArrayOrNull + ?: // is_end + return Result.success(ArrayList()) + return Result.success(list.map { + val obj = it.jsonObject + val msgSeq = obj["msg_seq"].asLong + EssenceMessage( + senderId = obj["sender_uin"].asString.toLong(), + senderNick = obj["sender_nick"].asString, + senderTime = obj["sender_time"].asLong, + operatorId = obj["add_digest_uin"].asString.toLong(), + operatorNick = obj["add_digest_nick"].asString, + operatorTime = obj["add_digest_time"].asLong, + messageSeq = msgSeq, + messageContent = obj["msg_content"] ?: EmptyJsonArray + ) + }) + } else { + return Result.failure(Exception(body.jsonObject["retmsg"].asStringOrNull)) + } + } + + fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) { + val serviceId = if (isSet) 1 else 2 + sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082( + peer = peer.toULong(), + msgSeq = msgSeq, + faceIndex = faceIndex, + flag = 1u, + u1 = 0u, + u2 = 0u + ).toByteArray()) + } + + suspend fun setEssenceMessage(groupId: Long, seq: Long, rand: Long): String? { + val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_1", 3756, 1, oidb_0xeac.ReqBody().apply { + group_code.set(groupId) + msg_seq.set(seq.toInt()) + msg_random.set(rand.toInt()) + }.toByteArray()) + if (fromServiceMsg?.wupBuffer == null) { + return "no response" + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + return if (result.wording.has()) { + LogCenter.log("设置群精华失败: ${result.wording.get()}", Level.WARN) + "设置群精华失败: ${result.wording.get()}" + } else { + LogCenter.log("设置群精华 -> $groupId: $seq") + null + } + } + + suspend fun deleteEssenceMessage(groupId: Long, seq: Long, rand: Long): String? { + val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_2", 3756, 2, oidb_0xeac.ReqBody().apply { + group_code.set(groupId) + msg_seq.set(seq.toInt()) + msg_random.set(rand.toInt()) + }.toByteArray()) + if (fromServiceMsg?.wupBuffer == null) { + return "no response" + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + return if (result.wording.has()) { + LogCenter.log("移除群精华失败: ${result.wording.get()}", Level.WARN) + "移除群精华失败: ${result.wording.get()}" + } else { + LogCenter.log("移除群精华 -> $groupId: $seq") + null + } + } + private suspend fun prepareTempChatFromGroup( groupId: String, peerId: String diff --git a/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt b/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt index 74e9581..a943efe 100644 --- a/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt +++ b/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt @@ -30,6 +30,7 @@ import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogicException import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import moe.fuqiuluo.shamrock.tools.json import moe.fuqiuluo.shamrock.utils.AudioUtils import moe.fuqiuluo.shamrock.utils.DownloadUtils import moe.fuqiuluo.shamrock.utils.FileUtils @@ -94,6 +95,7 @@ object NtMsgConvertor { SHARE to ::shareConvertor, CONTACT to ::contactConvertor, JSON to ::jsonConvertor, + FORWARD to ::forwardConvertor, MARKDOWN to ::markdownConvertor, BUTTON to ::buttonConvertor, ) @@ -835,4 +837,58 @@ object NtMsgConvertor { elem.inlineKeyboardElement = InlineKeyboardElement(rows, 0) return Result.success(elem) } + + private suspend fun forwardConvertor(contact: Contact, msgId: Long, sourceForward: Element): Result { + val resId = sourceForward.forward.id + val filename = sourceForward.forward.uniseq + var summary = sourceForward.forward.summary + val descriptions = sourceForward.forward.description + var news = descriptions?.split("\n")?.map { "text" to it } + + if (news == null || summary == null) { + val forwardMsg = MessageHelper.getForwardMsg(resId).getOrElse { return Result.failure(it) } + if (news == null) { + news = forwardMsg.map { + "text" to it.sender.nickName + ": " + descriptions + } + } + if (summary == null) { + summary = "查看${forwardMsg.size}条转发消息" + } + } + + val json = mapOf( + "app" to "com.tencent.multimsg", + "config" to mapOf( + "autosize" to 1, + "forward" to 1, + "round" to 1, + "type" to "normal", + "width" to 300 + ), + "desc" to "[聊天记录]", + "extra" to mapOf( + "filename" to filename, + "tsum" to 2 + ).json.toString(), + "meta" to mapOf( + "detail" to mapOf( + "news" to news, + "resid" to resId, + "source" to "群聊的聊天记录", + "summary" to summary, + "uniseq" to filename + ) + ), + "prompt" to "[聊天记录]", + "ver" to "0.0.0.5", + "view" to "contact" + ) + + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT + val ark = ArkElement(json.json.toString(), null, null) + elem.arkElement = ark + return Result.success(elem) + } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt b/xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt new file mode 100644 index 0000000..0f2d833 --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt @@ -0,0 +1,575 @@ +package qq.service.msg + +import android.graphics.BitmapFactory +import android.util.Base64 +import androidx.exifinterface.media.ExifInterface +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.msg.api.IMsgService +import io.kritor.message.AtElement +import io.kritor.message.Element +import io.kritor.message.ElementType +import io.kritor.message.ImageElement +import io.kritor.message.ImageType +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.helper.LogicException +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.tools.hex2ByteArray +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import moe.fuqiuluo.shamrock.tools.json +import moe.fuqiuluo.shamrock.tools.putBuf32Long +import moe.fuqiuluo.shamrock.utils.DeflateTools +import moe.fuqiuluo.shamrock.utils.DownloadUtils +import moe.fuqiuluo.shamrock.utils.FileUtils +import protobuf.auto.toByteArray +import protobuf.message.Elem +import protobuf.message.RichText +import protobuf.message.element.* +import protobuf.message.element.commelem.Action +import protobuf.message.element.commelem.Button +import protobuf.message.element.commelem.ButtonExtra +import protobuf.message.element.commelem.MarkdownExtra +import protobuf.message.element.commelem.Object1 +import protobuf.message.element.commelem.Permission +import protobuf.message.element.commelem.PokeExtra +import protobuf.message.element.commelem.QFaceExtra +import protobuf.message.element.commelem.RenderData +import protobuf.message.element.commelem.Row +import protobuf.oidb.cmd0x11c5.C2CUserInfo +import protobuf.oidb.cmd0x11c5.GroupUserInfo +import qq.service.QQInterfaces +import qq.service.bdh.NtV2RichMediaSvc +import qq.service.bdh.NtV2RichMediaSvc.fetchGroupResUploadTo +import qq.service.contact.ContactHelper +import qq.service.contact.longPeer +import qq.service.group.GroupHelper +import qq.service.lightapp.WeatherHelper +import java.io.ByteArrayInputStream +import java.io.File +import java.nio.ByteBuffer +import java.util.UUID +import kotlin.coroutines.resume +import kotlin.random.Random +import kotlin.random.nextULong +import kotlin.time.Duration.Companion.seconds + +/** + * 请求消息(io.kritor.message.*)转换合并转发消息 + */ + +suspend fun List.toRichText(contact: Contact): Result> { + val summary = StringBuilder() + val elems = ArrayList() + forEach { + try { + when(it.type!!) { + ElementType.TEXT -> { + val text = it.text.text + val elem = Elem( + text = TextMsg(text) + ) + elems.add(elem) + summary.append(text) + } + ElementType.AT -> { + when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> { + val qq = when (it.at.accountCase) { + AtElement.AccountCase.UIN -> it.at.uin.toString() + else -> ContactHelper.getUinByUidAsync(it.at.uid) + } + val type: Int + val nick = if (it.at.uid == "all" || it.at.uin == 0L) { + type = 1 + "@全体成员" + } else { + type = 0 + "@" + (GroupHelper.getTroopMemberInfoByUinV2(contact.longPeer().toString(), qq, true).let { + val info = it.getOrNull() + if (info == null) + LogCenter.log("无法获取群成员信息: $qq", Level.ERROR) + else info.troopnick + .ifNullOrEmpty { info.friendnick } + .ifNullOrEmpty { qq } + }) + } + val attr6 = ByteBuffer.allocate(6) + attr6.put(byteArrayOf(0, 1, 0, 0, 0)) + attr6.put(nick.length.toByte()) + attr6.putChar(type.toChar()) + attr6.putBuf32Long(qq.toLong()) + attr6.put(byteArrayOf(0, 0)) + val elem = Elem( + text = TextMsg(str = nick, attr6Buf = attr6.array()) + ) + elems.add(elem) + summary.append(nick) + } + + MsgConstant.KCHATTYPEC2C -> { + val qq = when (it.at.accountCase) { + AtElement.AccountCase.UIN -> it.at.uin.toString() + else -> ContactHelper.getUinByUidAsync(it.at.uid) + } + val display = "@" + (ContactHelper.getProfileCard(qq.toLong()).onSuccess { + it.strNick.ifNullOrEmpty { qq } + }.onFailure { + LogCenter.log("无法获取QQ信息: $qq", Level.WARN) + }) + val elem = Elem( + text = TextMsg(str = display) + ) + elems.add(elem) + summary.append(display) + } + else -> throw UnsupportedOperationException("Unsupported chatType($contact) for AtMsg") + } + } + ElementType.FACE -> { + val faceId = it.face.id + val elem = if (it.face.isBig) { + Elem( + commonElem = CommonElem( + serviceType = 37, + elem = QFaceExtra( + packId = "1", + stickerId = "1", + faceId = faceId, + field4 = 1, + field5 = 1, + result = "", + faceText = "", //todo 表情名字 + field9 = 1 + ).toByteArray(), + businessType = 1 + ) + ) + } else { + Elem( + face = FaceMsg( + index = faceId + ) + ) + } + elems.add(elem) + summary.append("[表情]") + } + ElementType.BUBBLE_FACE -> throw UnsupportedOperationException("Unsupported ElementType.BUBBLE_FACE") + ElementType.REPLY -> { + val msgId = it.reply.messageId + withTimeoutOrNull(3000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).getMsgsByMsgId(contact, arrayListOf(msgId)) { _, _, records -> + it.resume(records) + } + } + }?.firstOrNull()?.let { + val sourceContact = MessageHelper.generateContact(it) + elems.add(Elem( + srcMsg = SourceMsg( + origSeqs = listOf(it.msgSeq.toInt()), + senderUin = it.senderUin.toULong(), + time = it.msgTime.toULong(), + flag = 1u, + elems = it.elements + .toKritorReqMessages(sourceContact) + .toRichText(contact).getOrThrow().second.elements, + type = 0u, + pbReserve = SourceMsg.Companion.PbReserve( + msgRand = Random.nextULong(), + senderUid = it.senderUid, + receiverUid = QQInterfaces.app.currentUid, + field8 = Random.nextInt(0, 10000) + ), + ) + )) + } + summary.append("[回复消息]") + } + ElementType.IMAGE -> { + val type = it.image.type + val isOriginal = type == ImageType.ORIGIN + val file = when(it.image.dataCase!!) { + ImageElement.DataCase.FILE_NAME -> { + val fileMd5 = it.image.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase() + FileUtils.getFileByMd5(fileMd5) + } + ImageElement.DataCase.FILE_PATH -> { + val filePath = it.image.filePath + File(filePath).inputStream().use { + FileUtils.saveFileToCache(it) + } + } + ImageElement.DataCase.FILE_BASE64 -> { + FileUtils.saveFileToCache( + ByteArrayInputStream( + Base64.decode(it.image.fileBase64, Base64.DEFAULT) + ) + ) + } + ImageElement.DataCase.URL -> { + val tmp = FileUtils.getTmpFile() + if(DownloadUtils.download(it.image.url, tmp)) { + tmp.inputStream().use { + FileUtils.saveFileToCache(it) + }.also { + tmp.delete() + } + } else { + tmp.delete() + throw LogicException("图片资源下载失败: ${it.image.url}") + } + } + ImageElement.DataCase.DATA_NOT_SET -> throw IllegalArgumentException("ImageElement data is not set") + } + + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(file.absolutePath, options) + val exifInterface = ExifInterface(file.absolutePath) + val orientation = exifInterface.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED + ) + val picWidth: Int + val picHeight: Int + if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) { + picWidth = options.outWidth + picHeight = options.outHeight + } else { + picWidth = options.outHeight + picHeight = options.outWidth + } + + val fileInfo = NtV2RichMediaSvc.tryUploadResourceByNt( + chatType = contact.chatType, + elementType = MsgConstant.KELEMTYPEPIC, + resources = arrayListOf(file), + timeout = 30.seconds + ).getOrThrow().first() + + runCatching { + fileInfo.uuid.toUInt() + }.onFailure { + NtV2RichMediaSvc.requestUploadNtPic(file, fileInfo.md5, fileInfo.sha, fileInfo.fileName, picWidth.toUInt(), picHeight.toUInt(), 5) { + when(contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> { + sceneType = 2u + grp = GroupUserInfo(fetchGroupResUploadTo().toULong()) + } + MsgConstant.KCHATTYPEC2C -> { + sceneType = 1u + c2c = C2CUserInfo( + accountType = 2u, + uid = contact.peerUid + ) + } + else -> error("不支持的合并转发图片类型") + } + }.onFailure { + LogCenter.log("获取MultiMedia图片信息失败: $it", Level.ERROR) + }.onSuccess { + //LogCenter.log({ "获取MultiMedia图片信息成功: ${it.hashCode()}" }, Level.INFO) + elems.add(Elem( + commonElem = CommonElem( + serviceType = 48, + businessType = 10, + elem = it.msgInfo!!.toByteArray() + ) + )) + } + }.onSuccess { uuid -> + elems.add(when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> Elem( + customFace = CustomFace( + filePath = fileInfo.fileName, + fileId = uuid, + serverIp = 0u, + serverPort = 0u, + fileType = FileUtils.getPicType(file).toUInt(), + useful = 1u, + md5 = fileInfo.md5.hex2ByteArray(), + bizType = 0u, + imageType = FileUtils.getPicType(file).toUInt(), + width = picWidth.toUInt(), + height = picHeight.toUInt(), + size = fileInfo.fileSize.toUInt(), + origin = isOriginal, + thumbWidth = 0u, + thumbHeight = 0u, + pbReserve = CustomFace.Companion.PbReserve( + field1 = 0, + field3 = 0, + field4 = 0, + field10 = 0, + field21 = CustomFace.Companion.Object1( + field1 = 0, + field2 = "", + field3 = 0, + field4 = 0, + field5 = 0, + md5Str = fileInfo.md5 + ) + ) + ) + ) + MsgConstant.KCHATTYPEC2C -> Elem( + notOnlineImage = NotOnlineImage( + filePath = fileInfo.fileName, + fileLen = fileInfo.fileSize.toUInt(), + downloadPath = fileInfo.uuid, + imgType = FileUtils.getPicType(file).toUInt(), + picMd5 = fileInfo.md5.hex2ByteArray(), + picHeight = picWidth.toUInt(), + picWidth = picHeight.toUInt(), + resId = fileInfo.uuid, + original = isOriginal, // true + pbReserve = NotOnlineImage.Companion.PbReserve( + field1 = 0, + field3 = 0, + field4 = 0, + field10 = 0, + field20 = NotOnlineImage.Companion.Object1( + field1 = 0, + field2 = "", + field3 = 0, + field4 = 0, + field5 = 0, + field7 = "", + ), + md5Str = fileInfo.md5 + ) + ) + ) + else -> throw LogicException("Not supported chatType($contact) for PictureMsg") + }) + } + + summary.append("[图片]") + } + ElementType.VOICE -> throw UnsupportedOperationException("Unsupported ElementType.VOICE") + ElementType.VIDEO -> throw UnsupportedOperationException("Unsupported ElementType.VIDEO") + ElementType.BASKETBALL -> throw UnsupportedOperationException("Unsupported ElementType.BASKETBALL") + ElementType.DICE -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 37, + elem = QFaceExtra( + packId = "1", + stickerId = "33", + faceId = 358, + field4 = 1, + field5 = 2, + result = "", + faceText = "/骰子", + field9 = 1 + ).toByteArray(), + businessType = 2 + ) + ) + elems.add(elem) + summary .append( "[骰子]" ) + } + ElementType.RPS -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 37, + elem = QFaceExtra( + packId = "1", + stickerId = "34", + faceId = 359, + field4 = 1, + field5 = 2, + result = "", + faceText = "/包剪锤", + field9 = 1 + ).toByteArray(), + businessType = 1 + ) + ) + elems.add(elem) + summary .append( "[包剪锤]" ) + } + ElementType.POKE -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 2, + elem = PokeExtra( + type = it.poke.type, + field7 = 0, + field8 = 0 + ).toByteArray(), + businessType = it.poke.id + ) + ) + elems.add(elem) + summary .append( "[戳一戳]" ) + } + ElementType.MUSIC -> throw UnsupportedOperationException("Unsupported ElementType.MUSIC") + ElementType.WEATHER -> { + var code = it.weather.code.toIntOrNull() + if (code == null) { + val city = it.weather.city + WeatherHelper.searchCity(city).onFailure { + LogCenter.log("无法获取城市天气: $city", Level.ERROR) + }.getOrNull()?.firstOrNull()?.let { + code = it.adcode + } + } + + if (code != null) { + val weatherCard = WeatherHelper.fetchWeatherCard(code!!).getOrThrow() + val elem = Elem( + lightApp = LightAppElem( + data = byteArrayOf(1) + DeflateTools.compress( + weatherCard["weekStore"] + .asJsonObject["share"].asString.toByteArray() + ) + ) + ) + elems.add(elem) + summary .append( "[天气卡片]" ) + } else { + throw LogicException("无法获取城市天气") + } + } + ElementType.LOCATION -> throw UnsupportedOperationException("Unsupported ElementType.LOCATION") + ElementType.SHARE -> throw UnsupportedOperationException("Unsupported ElementType.SHARE") + ElementType.GIFT -> throw UnsupportedOperationException("Unsupported ElementType.GIFT") + ElementType.MARKET_FACE -> throw UnsupportedOperationException("Unsupported ElementType.MARKET_FACE") + ElementType.FORWARD -> { + val resId = it.forward.id + val filename = UUID.randomUUID().toString().uppercase() + var content = it.forward.summary + val descriptions = it.forward.description + var news = descriptions?.split("\n")?.map { "text" to it } + + if (news == null || content == null) { + val forwardMsg = MessageHelper.getForwardMsg(resId).getOrThrow() + if (news == null) { + news = forwardMsg.map { + "text" to it.sender.nickName + ": " + descriptions + } + } + if (content == null) { + content = "查看${forwardMsg.size}条转发消息" + } + } + + val json = mapOf( + "app" to "com.tencent.multimsg", + "config" to mapOf( + "autosize" to 1, + "forward" to 1, + "round" to 1, + "type" to "normal", + "width" to 300 + ), + "desc" to "[聊天记录]", + "extra" to mapOf( + "filename" to filename, + "tsum" to 2 + ).json.toString(), + "meta" to mapOf( + "detail" to mapOf( + "news" to news, + "resid" to resId, + "source" to "群聊的聊天记录", + "summary" to content, + "uniseq" to filename + ) + ), + "prompt" to "[聊天记录]", + "ver" to "0.0.0.5", + "view" to "contact" + ) + val elem = Elem( + lightApp = LightAppElem( + data = byteArrayOf(1) + DeflateTools.compress(json.json.toString().toByteArray()) + ) + ) + elems.add(elem) + summary.append( "[聊天记录]" ) + } + ElementType.CONTACT -> throw UnsupportedOperationException("Unsupported ElementType.CONTACT") + ElementType.JSON -> { + val elem = Elem( + lightApp = LightAppElem( + data = byteArrayOf(1) + DeflateTools.compress(it.json.json.toByteArray()) + ) + ) + elems.add(elem) + summary .append( "[Json消息]" ) + } + ElementType.XML -> throw UnsupportedOperationException("Unsupported ElementType.XML") + ElementType.FILE -> throw UnsupportedOperationException("Unsupported ElementType.FILE") + ElementType.MARKDOWN -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 45, + elem = MarkdownExtra(it.markdown.markdown).toByteArray(), + businessType = 1 + ) + ) + elems.add(elem) + summary.append("[Markdown消息]") + } + ElementType.BUTTON -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 46, + elem = ButtonExtra( + field1 = Object1( + rows = it.button.rowsList.map { row -> + Row(buttons = row.buttonsList.map { button -> + val renderData = button.renderData + val action = button.action + val permission = action.permission + Button( + id = button.id, + renderData = RenderData( + label = renderData.label, + visitedLabel = renderData.visitedLabel, + style = renderData.style + ), + action = Action( + type = action.type, + permission = Permission( + type = permission.type, + specifyRoleIds = permission.roleIdsList, + specifyUserIds = permission.userIdsList + ), + unsupportTips = action.unsupportedTips, + data = action.data, + reply = action.reply, + enter = action.enter + ) + ) + }) + }, + appid = 0 + ) + ).toByteArray(), + businessType = 1 + ) + ) + elems.add(elem) + summary.append("[Button消息]") + } + ElementType.NODE -> throw UnsupportedOperationException("Unsupported ElementType.NODE") + ElementType.UNRECOGNIZED -> throw UnsupportedOperationException("Unsupported ElementType.UNRECOGNIZED") + } + } catch (e: Throwable) { + LogCenter.log("转换消息失败(Multi): ${e.stackTraceToString()}", Level.ERROR) + } + } + return Result.success(summary.toString() to RichText( + elements = elems + )) +} + diff --git a/xposed/src/main/java/qq/service/ticket/TicketHelper.kt b/xposed/src/main/java/qq/service/ticket/TicketHelper.kt index 70bb823..955d583 100644 --- a/xposed/src/main/java/qq/service/ticket/TicketHelper.kt +++ b/xposed/src/main/java/qq/service/ticket/TicketHelper.kt @@ -48,19 +48,15 @@ internal object TicketHelper: QQInterfaces() { ) } - fun getUin(): String { + inline fun getUin(): String { return app.currentUin.ifBlank { "0" } } - fun getLongUin(): Long { - return app.longAccountUin - } - fun getUid(): String { return app.currentUid.ifBlank { "u_" } } - fun getNickname(): String { + inline fun getNickname(): String { return app.currentNickname } @@ -123,7 +119,7 @@ internal object TicketHelper: QQInterfaces() { fun getSKey(uin: String): String { require(app is QQAppInterface) - return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getSkey(uin) + return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getRealSkey(uin) } fun getRealSkey(uin: String): String { From d66358a1f3464884141fde2b403748b7574ed862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Mon, 18 Mar 2024 21:02:05 +0800 Subject: [PATCH 20/20] =?UTF-8?q?`Shamrock`:=20=E6=8F=90=E4=BE=9B=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E8=80=85=E6=9C=8D=E5=8A=A1=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- .../moe/fuqiuluo/ksp/impl/GrpcProcessor.kt | 3 +- .../main/java/kritor/server/KritorServer.kt | 1 + .../java/kritor/service/Authentication.kt | 2 +- .../java/kritor/service/ContactService.kt | 2 +- .../java/kritor/service/DeveloperService.kt | 50 +++++++++++++++++++ .../main/java/kritor/service/EventService.kt | 2 +- .../main/java/kritor/service/FriendService.kt | 2 +- .../main/java/kritor/service/KritorService.kt | 2 +- 8 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 xposed/src/main/java/kritor/service/DeveloperService.kt diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt b/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt index adb69ec..bc80b18 100644 --- a/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt +++ b/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt @@ -30,8 +30,6 @@ class GrpcProcessor( private val codeGenerator: CodeGenerator, private val logger: KSPLogger ): SymbolProcessor { - private val subPackage = arrayOf("contact", "core", "file", "friend", "group", "message", "web") - override fun process(resolver: Resolver): List { val symbols = resolver.getSymbolsWithAnnotation(Grpc::class.qualifiedName!!) val actions = (symbols as Sequence).toList() @@ -77,6 +75,7 @@ class GrpcProcessor( .addStatement("import io.kritor.file.*") .addStatement("import io.kritor.message.*") .addStatement("import io.kritor.web.*") + .addStatement("import io.kritor.developer.*") .addFunction(funcBuilder.build()) .addImport("moe.fuqiuluo.symbols", "EMPTY_BYTE_ARRAY") runCatching { diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index c073ea8..990c7d4 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -28,6 +28,7 @@ class KritorServer( .addService(EventService) .addService(ForwardMessageService) .addService(WebService) + .addService(DeveloperService) .build()!! fun start(block: Boolean = false) { diff --git a/xposed/src/main/java/kritor/service/Authentication.kt b/xposed/src/main/java/kritor/service/Authentication.kt index d0def44..20aac7a 100644 --- a/xposed/src/main/java/kritor/service/Authentication.kt +++ b/xposed/src/main/java/kritor/service/Authentication.kt @@ -15,7 +15,7 @@ import moe.fuqiuluo.shamrock.config.ActiveTicket import moe.fuqiuluo.shamrock.config.ShamrockConfig import qq.service.QQInterfaces -object Authentication: AuthenticationGrpcKt.AuthenticationCoroutineImplBase() { +internal object Authentication: AuthenticationGrpcKt.AuthenticationCoroutineImplBase() { @Grpc("Authentication", "Auth") override suspend fun auth(request: AuthReq): AuthRsp { if (QQInterfaces.app.account != request.account) { diff --git a/xposed/src/main/java/kritor/service/ContactService.kt b/xposed/src/main/java/kritor/service/ContactService.kt index 267fd49..8dc8de7 100644 --- a/xposed/src/main/java/kritor/service/ContactService.kt +++ b/xposed/src/main/java/kritor/service/ContactService.kt @@ -33,7 +33,7 @@ import qq.service.contact.ContactHelper import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() { +internal object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() { @Grpc("ContactService", "VoteUser") override suspend fun voteUser(request: VoteUserRequest): VoteUserResponse { ContactHelper.voteUser(when(request.accountCase!!) { diff --git a/xposed/src/main/java/kritor/service/DeveloperService.kt b/xposed/src/main/java/kritor/service/DeveloperService.kt new file mode 100644 index 0000000..4577fc6 --- /dev/null +++ b/xposed/src/main/java/kritor/service/DeveloperService.kt @@ -0,0 +1,50 @@ +package kritor.service + +import com.google.protobuf.ByteString +import com.tencent.mobileqq.fe.FEKit +import com.tencent.mobileqq.qsec.qsecdandelionsdk.Dandelion +import io.kritor.developer.DeveloperServiceGrpcKt +import io.kritor.developer.EnergyRequest +import io.kritor.developer.EnergyResponse +import io.kritor.developer.SendPacketRequest +import io.kritor.developer.SendPacketResponse +import io.kritor.developer.SignRequest +import io.kritor.developer.SignResponse +import io.kritor.developer.energyResponse +import io.kritor.developer.sendPacketResponse +import io.kritor.developer.signResponse +import qq.service.QQInterfaces + +internal object DeveloperService: DeveloperServiceGrpcKt.DeveloperServiceCoroutineImplBase() { + @Grpc("DeveloperService", "Sign") + override suspend fun sign(request: SignRequest): SignResponse { + return signResponse { + val result = FEKit.getInstance().getSign(request.command, request.buffer.toByteArray(), request.seq, request.uin) + this.sign = ByteString.copyFrom(result.sign) + this.token = ByteString.copyFrom(result.token) + this.extra = ByteString.copyFrom(result.extra) + } + } + + @Grpc("DeveloperService", "Energy") + override suspend fun energy(request: EnergyRequest): EnergyResponse { + return energyResponse { + this.result = ByteString.copyFrom(Dandelion.getInstance().fly(request.data, request.salt.toByteArray())) + } + } + + + @Grpc("DeveloperService", "SendPacket") + override suspend fun sendPacket(request: SendPacketRequest): SendPacketResponse { + return sendPacketResponse { + val fromServiceMsg = QQInterfaces.sendBufferAW(request.command, request.isProtobuf, request.requestBuffer.toByteArray()) + if (fromServiceMsg?.wupBuffer == null) { + this.isSuccess = false + } else { + this.isSuccess = true + this.responseBuffer = ByteString.copyFrom(fromServiceMsg.wupBuffer) + } + } + } + +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/EventService.kt b/xposed/src/main/java/kritor/service/EventService.kt index 826e752..bededa2 100644 --- a/xposed/src/main/java/kritor/service/EventService.kt +++ b/xposed/src/main/java/kritor/service/EventService.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter -object EventService: EventServiceGrpcKt.EventServiceCoroutineImplBase() { +internal object EventService: EventServiceGrpcKt.EventServiceCoroutineImplBase() { override fun registerActiveListener(request: RequestPushEvent): Flow { return channelFlow { when(request.type!!) { diff --git a/xposed/src/main/java/kritor/service/FriendService.kt b/xposed/src/main/java/kritor/service/FriendService.kt index 6a2c862..bd45e4b 100644 --- a/xposed/src/main/java/kritor/service/FriendService.kt +++ b/xposed/src/main/java/kritor/service/FriendService.kt @@ -11,7 +11,7 @@ import io.kritor.friend.getFriendListResponse import qq.service.contact.ContactHelper import qq.service.friend.FriendHelper -object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() { +internal object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() { @Grpc("FriendService", "GetFriendList") override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse { val friendList = FriendHelper.getFriendList(if(request.hasRefresh()) request.refresh else false).onFailure { diff --git a/xposed/src/main/java/kritor/service/KritorService.kt b/xposed/src/main/java/kritor/service/KritorService.kt index 2190d3c..185bc6a 100644 --- a/xposed/src/main/java/kritor/service/KritorService.kt +++ b/xposed/src/main/java/kritor/service/KritorService.kt @@ -35,7 +35,7 @@ import qq.service.QQInterfaces.Companion.app import qq.service.contact.ContactHelper import java.io.File -object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() { +internal object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() { @Grpc("KritorService", "GetVersion") override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse { return getVersionResponse {