├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.md ├── pull_request_template.md └── workflows │ ├── dev.yml │ ├── publish.yml │ └── snapshot.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── LICENSE ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── groovy │ └── moe │ │ └── karla │ │ └── maven │ │ └── publishing │ │ ├── MavenPublishingExtension.groovy │ │ ├── MavenPublishingPlugin.groovy │ │ └── PublishingStubsSetupPlugin.groovy │ ├── java │ └── moe │ │ └── karla │ │ └── maven │ │ └── publishing │ │ └── advtask │ │ └── UploadToMavenCentral.java │ └── kotlin │ ├── Helper.kt │ └── PublicationHelper.kt ├── docs ├── UserManual.md ├── configuration.md ├── contributing │ └── README.md ├── dev │ ├── README.md │ └── progress.md └── install │ ├── MCL.md │ ├── MCLOverflow.md │ ├── MCLScript.md │ ├── README.md │ ├── Raw.md │ ├── install_overflow_mcl.cmd │ └── install_overflow_mcl.sh ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── overflow-core-all └── build.gradle.kts ├── overflow-core-api ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── top │ └── mrxiaom │ └── overflow │ ├── BotBuilder.kt │ ├── OverflowAPI.kt │ ├── action │ └── ActionContext.kt │ ├── contact │ ├── RemoteBot.kt │ ├── RemoteGroup.kt │ ├── RemoteUser.kt │ └── Updatable.kt │ ├── event │ ├── LegacyGuildMessageEvent.kt │ ├── MemberEssenceNoticeEvent.kt │ ├── MessageReactionEvent.kt │ └── UnsolvedOnebotEvent.kt │ ├── message │ └── data │ │ ├── ContactRecommend.kt │ │ ├── FileMessageWithUrl.kt │ │ ├── InlineKeyboard.kt │ │ ├── Location.kt │ │ └── Markdown.kt │ └── spi │ ├── ExtendedMessageSerializerService.kt │ ├── FileService.kt │ └── MediaURLService.kt ├── overflow-core ├── LICENSE ├── README.md ├── build.gradle.kts ├── runConsole.run.xml └── src │ ├── main │ ├── kotlin │ │ ├── cn │ │ │ └── evolvefield │ │ │ │ └── onebot │ │ │ │ ├── client │ │ │ │ ├── config │ │ │ │ │ └── BotConfig.kt │ │ │ │ ├── connection │ │ │ │ │ ├── ConnectFactory.kt │ │ │ │ │ ├── IAdapter.kt │ │ │ │ │ ├── WSClient.kt │ │ │ │ │ ├── WSServer.kt │ │ │ │ │ └── platform-connection.kt │ │ │ │ ├── core │ │ │ │ │ └── Bot.kt │ │ │ │ ├── handler │ │ │ │ │ ├── ActionHandler.kt │ │ │ │ │ ├── EventBus.kt │ │ │ │ │ ├── EventHandler.kt │ │ │ │ │ └── EventHolder.kt │ │ │ │ └── util │ │ │ │ │ ├── ActionSendRequest.kt │ │ │ │ │ ├── Exceptions.kt │ │ │ │ │ ├── Files.kt │ │ │ │ │ └── ListenerUtils.kt │ │ │ │ └── sdk │ │ │ │ ├── action │ │ │ │ ├── ActionPath.kt │ │ │ │ └── ActionResults.kt │ │ │ │ ├── entity │ │ │ │ ├── Anonymous.kt │ │ │ │ ├── File.kt │ │ │ │ ├── GroupSender.kt │ │ │ │ ├── MsgId.kt │ │ │ │ ├── PrivateSender.kt │ │ │ │ └── Sender.kt │ │ │ │ ├── enums │ │ │ │ └── ActionPathEnum.kt │ │ │ │ ├── event │ │ │ │ ├── Event.kt │ │ │ │ ├── UnsolvedEvent.kt │ │ │ │ ├── message │ │ │ │ │ ├── GroupMessageEvent.kt │ │ │ │ │ ├── MessageEvent.kt │ │ │ │ │ └── PrivateMessageEvent.kt │ │ │ │ ├── meta │ │ │ │ │ ├── HeartbeatMetaEvent.kt │ │ │ │ │ ├── LifecycleMetaEvent.kt │ │ │ │ │ └── MetaEvent.kt │ │ │ │ ├── notice │ │ │ │ │ ├── NoticeEvent.kt │ │ │ │ │ ├── NotifyNoticeEvent.kt │ │ │ │ │ ├── friend │ │ │ │ │ │ ├── FriendAddNoticeEvent.kt │ │ │ │ │ │ └── PrivateMsgDeleteNoticeEvent.kt │ │ │ │ │ ├── group │ │ │ │ │ │ ├── GroupAdminNoticeEvent.kt │ │ │ │ │ │ ├── GroupBanNoticeEvent.kt │ │ │ │ │ │ ├── GroupCardChangeNoticeEvent.kt │ │ │ │ │ │ ├── GroupDecreaseNoticeEvent.kt │ │ │ │ │ │ ├── GroupEssenceNoticeEvent.kt │ │ │ │ │ │ ├── GroupHonorChangeNoticeEvent.kt │ │ │ │ │ │ ├── GroupIncreaseNoticeEvent.kt │ │ │ │ │ │ ├── GroupLuckyKingNoticeEvent.kt │ │ │ │ │ │ ├── GroupMsgDeleteNoticeEvent.kt │ │ │ │ │ │ ├── GroupNameChangeNoticeEvent.kt │ │ │ │ │ │ └── GroupUploadNoticeEvent.kt │ │ │ │ │ └── misc │ │ │ │ │ │ ├── GroupMsgEmojiLikeNotice.kt │ │ │ │ │ │ ├── GroupReactionNotice.kt │ │ │ │ │ │ └── ReceiveOfflineFilesNoticeEvent.kt │ │ │ │ └── request │ │ │ │ │ ├── FriendAddRequestEvent.kt │ │ │ │ │ ├── GroupAddRequestEvent.kt │ │ │ │ │ └── RequestEvent.kt │ │ │ │ ├── response │ │ │ │ ├── contact │ │ │ │ │ ├── FriendInfoResp.kt │ │ │ │ │ ├── LoginInfoResp.kt │ │ │ │ │ └── StrangerInfoResp.kt │ │ │ │ ├── ext │ │ │ │ │ ├── CreateGroupFileFolderResp.kt │ │ │ │ │ ├── GetFileResp.kt │ │ │ │ │ ├── SetGroupReactionResp.kt │ │ │ │ │ └── UploadGroupFileResp.kt │ │ │ │ ├── group │ │ │ │ │ ├── EssenceMsgResp.kt │ │ │ │ │ ├── ForwardMsgResp.kt │ │ │ │ │ ├── GetHistoryMsgResp.kt │ │ │ │ │ ├── GetMsgResp.kt │ │ │ │ │ ├── GroupAtAllRemainResp.kt │ │ │ │ │ ├── GroupDataResp.kt │ │ │ │ │ ├── GroupFileUrlResp.kt │ │ │ │ │ ├── GroupFilesResp.kt │ │ │ │ │ ├── GroupHonorInfoResp.kt │ │ │ │ │ ├── GroupInfoResp.kt │ │ │ │ │ ├── GroupMemberInfoResp.kt │ │ │ │ │ └── GroupNoticeResp.kt │ │ │ │ └── misc │ │ │ │ │ ├── CSRFTokenResp.kt │ │ │ │ │ ├── ClientsResp.kt │ │ │ │ │ ├── CookiesResp.kt │ │ │ │ │ └── CredentialsResp.kt │ │ │ │ └── util │ │ │ │ ├── CQCode.kt │ │ │ │ ├── JsonDeserializerKt.kt │ │ │ │ ├── JsonHelper.kt │ │ │ │ └── json │ │ │ │ ├── ClientsAdapter.kt │ │ │ │ ├── ForwardMsgAdapter.kt │ │ │ │ ├── GroupFilesAdapter.kt │ │ │ │ ├── MessageEventAdapter.kt │ │ │ │ └── MsgAdapter.kt │ │ ├── net │ │ │ └── mamoe │ │ │ │ └── mirai │ │ │ │ └── internal │ │ │ │ ├── AbstractBot.kt │ │ │ │ ├── QQAndroidBot.kt │ │ │ │ ├── event │ │ │ │ ├── EventChannelImpl.kt │ │ │ │ ├── EventChannelToEventDispatcherAdapter.kt │ │ │ │ ├── EventListeners.kt │ │ │ │ ├── GlobalEventChannelProviderImpl.kt │ │ │ │ ├── SafeListener.kt │ │ │ │ └── package.kt │ │ │ │ ├── message │ │ │ │ ├── RefinableMessage.kt │ │ │ │ └── data │ │ │ │ │ └── MarketFaceImpl.kt │ │ │ │ ├── network │ │ │ │ ├── QQAndroidClient.kt │ │ │ │ ├── components │ │ │ │ │ ├── EventDispatcher.kt │ │ │ │ │ └── SsoProcessor.kt │ │ │ │ ├── keys.kt │ │ │ │ └── protocol │ │ │ │ │ └── data │ │ │ │ │ └── proto │ │ │ │ │ └── Msg.kt │ │ │ │ ├── spi │ │ │ │ └── EncryptService.kt │ │ │ │ └── utils │ │ │ │ └── MiraiProtocolInternal.kt │ │ └── top │ │ │ └── mrxiaom │ │ │ └── overflow │ │ │ └── internal │ │ │ ├── BotFactoryImpl.kt │ │ │ ├── Config.kt │ │ │ ├── Overflow.kt │ │ │ ├── cache │ │ │ └── MessageCache.kt │ │ │ ├── contact │ │ │ ├── BotWrapper.kt │ │ │ ├── FriendWrapper.kt │ │ │ ├── GroupWrapper.kt │ │ │ ├── MemberWrapper.kt │ │ │ ├── OtherClientWrapper.kt │ │ │ ├── StrangerWrapper.kt │ │ │ └── data │ │ │ │ ├── AnnouncementsWrapper.kt │ │ │ │ ├── EssencesWrapper.kt │ │ │ │ ├── FallbackFriendGroup.kt │ │ │ │ ├── FallbackFriendGroups.kt │ │ │ │ ├── GroupActiveProtocol.kt │ │ │ │ ├── GroupActiveWrapper.kt │ │ │ │ ├── GroupSettingsWrapper.kt │ │ │ │ ├── MemberActiveWrapper.kt │ │ │ │ └── RemoteFilesWrapper.kt │ │ │ ├── data │ │ │ ├── ContactInfoImpl.kt │ │ │ └── UserProfileImpl.kt │ │ │ ├── listener │ │ │ ├── bot.kt │ │ │ ├── friend.kt │ │ │ └── group.kt │ │ │ ├── message │ │ │ ├── OnebotMessages.kt │ │ │ └── data │ │ │ │ ├── MessageSourceImpl.kt │ │ │ │ ├── UnknownMessage.kt │ │ │ │ ├── WrappedAudio.kt │ │ │ │ ├── WrappedFileMessage.kt │ │ │ │ ├── WrappedImageProtocol.kt │ │ │ │ ├── WrappedMusicShare.kt │ │ │ │ └── WrappedVideo.kt │ │ │ ├── plugin │ │ │ ├── BuiltInCommands.kt │ │ │ └── OverflowCoreAsPlugin.kt │ │ │ └── utils │ │ │ ├── Base64FileService.kt │ │ │ ├── BotUtils.kt │ │ │ ├── LoggerUtils.kt │ │ │ ├── RequestManager.kt │ │ │ ├── ResourceUtils.kt │ │ │ └── TransformerUtils.kt │ └── resources │ │ └── META-INF │ │ └── services │ │ ├── net.mamoe.mirai.IMirai │ │ ├── net.mamoe.mirai.event.InternalGlobalEventChannelProvider │ │ ├── net.mamoe.mirai.message.data.InternalImageProtocol │ │ ├── net.mamoe.mirai.utils.InternalProtocolDataExchange │ │ └── top.mrxiaom.overflow.spi.FileService │ └── test │ ├── kotlin │ ├── ForwardMessageTest.kt │ └── RunConsole.kt │ └── resources │ └── logback.xml └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ["https://afdian.com/a/mrxiaom"] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 报告 2 | description: 提交一个 bug (若反馈漏洞不选择该模板,Issue 将被关闭) 3 | labels: 4 | - "question" 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | 感谢你来到这里 11 | 12 | 在反馈前, 请确认你已经做了下面这些事情 13 | - 阅读过 [「提问的智慧」](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md) 14 | - 阅读过 [「如何有效地报告 Bug」](https://www.chiark.greenend.org.uk/~sgtatham/bugs-cn.html) 15 | - 对照过 [Releases](https://github.com/MrXiaoM/Overflow/releases),相关问题未在近期更新中解决 16 | - 搜索了已有的 [issues](https://github.com/MrXiaoM/Overflow/issues?q=is%3Aissue) 列表中有没相关的信息 17 | - 阅读了 [Overflow 的相关文档](https://github.com/MrXiaoM/Overflow/tree/main/docs) 18 | - 已做过初步调查,确定该问题来自 Overflow,而不是来自 mirai、业务逻辑、插件,或 LLOnebot 等 Onebot 实现。 19 | 20 | - type: textarea 21 | id: issue-description 22 | attributes: 23 | label: 问题描述 24 | description: 在此详细描述你遇到的问题 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: reproduce 30 | attributes: 31 | label: 复现 32 | description: 在这里简略说明如何让这个问题再次发生 33 | placeholder: | 34 | 在这里简略说明如何让这个问题再次发生 35 | 可使用 1. 2. 3. 的列表格式,或其他任意恰当的格式 36 | 如果你不确定如何复现, 请尽量描述发生当时的情景 37 | 38 | 建议提供相关代码 39 | validations: 40 | required: true 41 | 42 | - type: input 43 | id: version-overflow 44 | attributes: 45 | label: Overflow 版本 46 | description: | 47 | 开发版本的版本号为 short commit hash,即启动信息显示的 x.x.x.xxx- 后面的7位字符,示例: fffffff 48 | 正式版本的版本号为 x.x.x,如 1.0.0 49 | validations: 50 | required: true 51 | 52 | - type: textarea 53 | id: version-others 54 | attributes: 55 | label: 其他组件版本 56 | description: | 57 | 如 mirai-console 版本 (如果有) 58 | 如 LLOnebot 等 Onebot 实现的版本 59 | 60 | - type: textarea 61 | id: journal-system 62 | attributes: 63 | label: 系统日志 64 | description: | 65 | 请提供全面的相关日志. 请不要截图. 66 | 如果日志过大, 可以在 `补充信息` 上传文件. 67 | 如果你遇到的问题是 "消息收不到", "收消息报错" 等与协议有关的问题, 请一定提交日志. 若不提交日志, 你的问题可能会被直接关闭. 68 | render: 'text' 69 | validations: 70 | required: false 71 | 72 | - type: textarea 73 | id: journal-network 74 | attributes: 75 | label: 网络日志 76 | description: | 77 | 请提供全面的网络日志(如果需要). 请不要截图. 78 | 网络日志一般在 `logs/onebot/*.log`. 79 | 如果日志过大, 可以在 `补充信息` 上传文件. 80 | render: text 81 | validations: 82 | required: true 83 | 84 | - type: textarea 85 | id: additional 86 | attributes: 87 | label: 补充信息 88 | description: 如有必要,你可以在下文继续添加其他信息 89 | 90 | - type: markdown 91 | attributes: 92 | value: | 93 | ---- 94 | 95 | 在发出 issue 前, 请确认 96 | 97 | - 全部信息已经填写完毕, 特别是「其他组件版本」、「**网络日志**」 98 | - 报告中没有令人反感的语言 99 | - 「复现」的描述是否足够详细准确 100 | - 若内容有你认为的敏感数据,请使用星号代替,但不要过度替换敏感数据造成阅读障碍 101 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 特性申请 3 | about: 申请 Overflow 添加新的特性 4 | title: '' 5 | labels: 'feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **在提出此拉取请求时,我确认了以下几点(请复选框):** 2 | 3 | - [ ] 我已阅读并理解[贡献文档](https://github.com/MrXiaoM/Overflow/tree/main/docs/contributing)。 4 | - [ ] 我已检查没有与此请求重复的 Pull Requests。 5 | - [ ] 我已经考虑过,并确认这份呈件对其他人很有价值。 6 | - [ ] 我接受此提交可能不会被使用,并根据维护人员的意愿关闭 Pull Requests。 7 | 8 | 9 | 10 | 11 | 12 | **填写PR内容:** 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Build Snapshots 2 | on: 3 | push: 4 | branches: [ "main" ] 5 | paths-ignore: 6 | - '*.md' 7 | pull_request: 8 | branches: [ "main" ] 9 | paths-ignore: 10 | - '*.md' 11 | permissions: 12 | contents: write 13 | jobs: 14 | build: 15 | permissions: 16 | contents: write 17 | packages: write 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - name: Validate Gradle wrapper 25 | uses: gradle/actions/wrapper-validation@v4 26 | - name: Setup Gradle 27 | uses: gradle/actions/setup-gradle@v4 28 | - name: Setup Java 11 29 | uses: actions/setup-java@v3 30 | with: 31 | distribution: temurin 32 | java-version: 11 33 | - name: Get short SHA 34 | run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV 35 | - name: Build Package 36 | run: ./gradlew :overflow-core-all:shadowJar 37 | env: 38 | # 加上 VERSION_OVERRIDE 将会设为 -SNAPSHOT 版本 39 | ORG_GRADLE_PROJECT_VERSION_OVERRIDE: ${{ env.SHORT_SHA }} 40 | - name: Upload Artifacts 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: overflow-core-all-${{ env.SHORT_SHA }} 44 | path: overflow-core-all/build/libs/*-all.jar 45 | if-no-files-found: error 46 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven Central 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | if: github.repository == 'MrXiaoM/Overflow' 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 0 14 | - name: Validate Gradle wrapper 15 | uses: gradle/actions/wrapper-validation@v4 16 | - name: Setup Gradle 17 | uses: gradle/actions/setup-gradle@v4 18 | - name: Set up Java 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: '11' 22 | distribution: 'adopt' 23 | - name: Publish Package to Stage Repository 24 | run: ./gradlew cleanMavenPublishingStage build publishAllPublicationsToMavenStageRepository 25 | env: 26 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_PRIVATE_KEY }} 27 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} 28 | - name: Upload Artifacts 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: tmp 32 | path: build/tmp/* 33 | if-no-files-found: error 34 | - name: Upload to Central Portal 35 | run: ./gradlew publishToMavenCentral 36 | env: 37 | MAVEN_PUBLISH_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PUBLISH_ACCOUNT }} 38 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Central Snapshots 2 | on: 3 | push: 4 | branches: [ "main" ] 5 | paths-ignore: 6 | - '*.md' 7 | 8 | permissions: 9 | contents: write 10 | jobs: 11 | build: 12 | permissions: 13 | contents: write 14 | packages: write 15 | runs-on: ubuntu-latest 16 | if: github.repository == 'MrXiaoM/Overflow' 17 | env: 18 | ORG_GRADLE_PROJECT_IS_SNAPSHOT: true 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - name: Validate Gradle wrapper 25 | uses: gradle/actions/wrapper-validation@v4 26 | - name: Setup Gradle 27 | uses: gradle/actions/setup-gradle@v4 28 | - name: Setup Java 11 29 | uses: actions/setup-java@v3 30 | with: 31 | distribution: temurin 32 | java-version: 11 33 | # 检查是否存在 secrets,避免未设置 secrets 时运行发布 34 | - name: Check Secrets 35 | run: | 36 | echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV 37 | echo "SECRETS_LENGTH=$(expr length "$SECRETS_TO_CHECK")" >> $GITHUB_ENV 38 | env: 39 | SECRETS_TO_CHECK: ${{ secrets.SIGNING_PRIVATE_KEY }} 40 | - name: Publish Package 41 | if: ${{ env.SECRETS_LENGTH > 0 }} 42 | run: ./gradlew build publishAllPublicationsToCentralSnapshotsRepository 43 | env: 44 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_PRIVATE_KEY }} 45 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} 46 | MAVEN_SNAPSHOTS_USERNAME: ${{ secrets.OSSRH_USERNAME }} 47 | MAVEN_SNAPSHOTS_TOKEN: ${{ secrets.OSSRH_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Overflow

5 | 6 | 欢迎回到 mirai 7 | 8 | Overflow 是 mirai-core-api 的实现,对接 OneBot 11 标准,实现 mirai 的无缝迁移。 9 | 10 | 形象图由 [人间工作](https://www.pixiv.net/artworks/114225110) 绘制 11 | 12 | [![project-tRNA](https://img.shields.io/badge/project--tRNA-a7a3ff?style=for-the-badge)](https://github.com/project-tRNA) [![星标](https://shields.io/github/stars/MrXiaoM/Overflow?logo=github&style=for-the-badge&label=%E6%98%9F%E6%A0%87)](https://github.com/MrXiaoM/Overflow/stargazers) [![mirai](https://img.shields.io/badge/mirai--core--api-2.16.0-blue?style=for-the-badge)](https://github.com/mamoe/mirai) [![Onebot 11](https://img.shields.io/badge/Onebot-11-313343?style=for-the-badge)](https://11.onebot.dev) [![论坛](https://img.shields.io/badge/MiraiForum-post-5094e4?style=for-the-badge)](https://mirai.mamoe.net/topic/2565) 13 |
14 | 15 | 当前 Overflow 基本稳定,欢迎各位使用。 16 | 17 | - **[用户手册: 快速开始](https://mirai.mrxiaom.top/docs/UserManual)** 18 | - **[开发文档](https://mirai.mrxiaom.top/docs/dev)** 19 | - **[开发进度/具体的接口支持情况](https://mirai.mrxiaom.top/docs/dev/progress)** 20 | 21 | ## 兼容性说明 22 | 23 | Overflow 支持且**仅支持**连接到大多数标准的 Onebot 或 go-cqhttp 协议,支持安装未使用 mirai 内部特性或 mirai 码的插件 24 | + [x] 使用正向(主动)或反向(被动) WebSocket 连接 25 | + [x] 在连接时使用 token 鉴权 26 | + [x] 在代码中调用自定义的 action 27 | + [x] 在消息中使用 CQ 码 *测试中* 28 | + [x] 将 mirai 消息段序列化/反序列化为 json 29 | + [ ] 使用 MiraiCode (Mirai 码) 处理消息 *不支持* 30 | 31 | 如果你的 Overflow 与 Onebot 实现 (如 LLOnebot、NapCat、Gensokyo 等) 可部署且已部署在了同一文件系统,推荐安装附属插件 [LocalFileService](https://github.com/MrXiaoM/LocalFileService)。 32 | 33 | # 鸣谢 34 | 35 | 本项目使用了 [onebot-client](https://github.com/cnlimiter/onebot-client) 进行快速开发。 36 | 感谢该项目为本项目的发起者节约了大量的阅读文档与设计接口时间。 37 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER") 2 | import org.ajoberstar.grgit.Grgit 3 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 4 | import moe.karla.maven.publishing.MavenPublishingExtension.PublishingType 5 | 6 | plugins { 7 | // kotlin("jvm") moved to `buildSrc/build.gradle.kts` 8 | kotlin("plugin.serialization") version prop("kotlin.version") apply false 9 | id("org.jetbrains.dokka") version "1.8.10" apply false 10 | id("com.github.johnrengelman.shadow") version "7.0.0" apply false 11 | id("com.github.gmazzo.buildconfig") version "3.1.0" apply false 12 | id("me.him188.kotlin-jvm-blocking-bridge") version "3.0.0-180.1" apply false 13 | id("org.ajoberstar.grgit") version "5.2.2" apply false 14 | 15 | id("moe.karla.maven-publishing") 16 | } 17 | 18 | Helper.proj = rootProject 19 | group = "top.mrxiaom.mirai" 20 | 21 | val overflowVersion = "1.0.5".ext("overflowVersion") 22 | val miraiVersion = "2.16.0".ext("miraiVersion") 23 | 24 | var commitHash = "local" 25 | var commitCount = 0 26 | if (File(rootProject.projectDir, ".git").exists()) { 27 | Grgit.open(mapOf("currentDir" to rootProject.projectDir)).use { repo -> 28 | commitHash = repo.head().abbreviatedId 29 | val log = repo.log() 30 | commitCount = log.size 31 | } 32 | } 33 | commitHash.ext("commitHash") 34 | commitCount.ext("commitCount") 35 | val commit = 36 | if (commitHash == "local") "9999-local" 37 | else "$commitCount-${commitHash.substring(0, 7)}" 38 | 39 | version = overflowVersion 40 | 41 | if (findProperty("IS_SNAPSHOT") == "true") { 42 | version = "$version.$commit-SNAPSHOT" 43 | } 44 | findProperty("OVERRIDE_VERSION")?.also { version = it } 45 | 46 | println("Mirai version: $miraiVersion") 47 | println("Overflow version: $overflowVersion") 48 | println("Commit: $commit") 49 | println("Version: $version") 50 | 51 | mavenPublishing { 52 | publishingType = PublishingType.AUTOMATIC 53 | url = "https://github.com/MrXiaoM/Overflow" 54 | } 55 | 56 | allprojects { 57 | group = rootProject.group 58 | version = rootProject.version 59 | 60 | val javaVersion = "1.8" 61 | tasks { 62 | withType { 63 | kotlinOptions.jvmTarget = javaVersion 64 | } 65 | withType { 66 | options.encoding = "UTF-8" 67 | sourceCompatibility = javaVersion 68 | targetCompatibility = javaVersion 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /buildSrc/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Karlatemp 4 | Copyright (c) 2025 MrXiaoM 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /buildSrc/README.md: -------------------------------------------------------------------------------- 1 | ## 构建逻辑 2 | 3 | 发布工具使用 [Karlatemp/maven-central-publish](https://github.com/Karlatemp/maven-central-publish) - MIT License 4 | 5 | 进行了一点修改 6 | + 不强制要求 `rootProject` 拥有 clean 任务 7 | + 移除 signing-setup 插件,自己配置签名就足够了 8 | + 添加 SNAPSHOT 仓库支持 9 | + 由于可能与 kotlin 插件冲突,将使用 `JavaExec` 隔离进程的发布任务改为直接执行,虽然有潜在的安全风险,但只要不乱装其它插件一般不会出事 10 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.* 2 | 3 | plugins { 4 | java 5 | `groovy-gradle-plugin` 6 | `kotlin-dsl` 7 | id("com.github.gmazzo.buildconfig") version "3.1.0" 8 | } 9 | val gradleProperties = rootDir.parentFile 10 | .resolve("gradle.properties") 11 | val prop = Properties().apply { 12 | gradleProperties.reader().use(::load) 13 | } 14 | java { 15 | sourceCompatibility = JavaVersion.VERSION_1_8 16 | targetCompatibility = JavaVersion.VERSION_1_8 17 | } 18 | buildConfig { 19 | className("BuildConstants") 20 | useKotlinOutput() 21 | buildConfigField("String", "PROPERTIES_PATH", "\"" + gradleProperties.absolutePath.replace('\\', '/') + "\"") 22 | buildConfigField("java.io.File", "PROPERTIES_FILE", "java.io.File(PROPERTIES_PATH)") 23 | } 24 | repositories { 25 | if (Locale.getDefault().country == "CN") { 26 | prop["mirror.repo"]?.also(::maven) 27 | } 28 | mavenCentral() 29 | gradlePluginPortal() 30 | } 31 | 32 | dependencies { 33 | api(gradleApi()) 34 | api(gradleKotlinDsl()) 35 | api("com.google.code.gson:gson:2.10.1") 36 | 37 | api("org.apache.httpcomponents:httpclient:4.5.13") 38 | api("org.apache.httpcomponents:httpmime:4.5.13") 39 | 40 | api("org.jetbrains.kotlin", "kotlin-gradle-plugin", prop["kotlin.version"].toString()) { 41 | exclude("org.jetbrains.kotlin", "kotlin-stdlib") 42 | exclude("org.jetbrains.kotlin", "kotlin-stdlib-common") 43 | exclude("org.jetbrains.kotlin", "kotlin-reflect") 44 | } 45 | } 46 | 47 | gradlePlugin { 48 | website.set("https://github.com/Karlatemp/maven-central-publish") 49 | vcsUrl.set("https://github.com/Karlatemp/maven-central-publish") 50 | 51 | testSourceSets(sourceSets.test.get()) 52 | 53 | plugins { 54 | register("maven-publishing") { 55 | id = "moe.karla.maven-publishing" 56 | implementationClass = "moe.karla.maven.publishing.MavenPublishingPlugin" 57 | 58 | displayName = "Maven Central Publishing" 59 | description = "Publishing your software to Maven Central" 60 | tags.set(listOf("signing", "publishing")) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/moe/karla/maven/publishing/MavenPublishingExtension.groovy: -------------------------------------------------------------------------------- 1 | package moe.karla.maven.publishing 2 | 3 | class MavenPublishingExtension { 4 | 5 | /////////////////////////// 6 | // BASIC 7 | /////////////////////////// 8 | 9 | /** 10 | * The URL of this project. 11 | * 12 | * Leave blank or null for auto guest. 13 | */ 14 | public String url 15 | 16 | static enum PublishingType { 17 | /** 18 | * (default) a deployment will go through validation and, if it passes, automatically proceed to publish to Maven Central 19 | */ 20 | AUTOMATIC, 21 | /** 22 | * a deployment will go through validation and require the user to manually publish it via the Portal UI 23 | */ 24 | USER_MANAGED, 25 | } 26 | 27 | public PublishingType publishingType = PublishingType.AUTOMATIC 28 | 29 | /** 30 | * Generate sources jar and stub javadoc jar automatic 31 | */ 32 | public boolean automaticSourcesAndJavadoc = true 33 | 34 | } 35 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/moe/karla/maven/publishing/PublishingStubsSetupPlugin.groovy: -------------------------------------------------------------------------------- 1 | package moe.karla.maven.publishing 2 | 3 | import org.gradle.api.NamedDomainObjectProvider 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | import org.gradle.api.UnknownDomainObjectException 7 | import org.gradle.api.artifacts.Configuration 8 | import org.gradle.api.attributes.Bundling 9 | import org.gradle.api.attributes.Category 10 | import org.gradle.api.attributes.DocsType 11 | import org.gradle.api.attributes.Usage 12 | import org.gradle.api.component.AdhocComponentWithVariants 13 | import org.gradle.api.plugins.JavaPluginExtension 14 | import org.gradle.jvm.tasks.Jar 15 | 16 | class PublishingStubsSetupPlugin implements Plugin { 17 | private static final String STUB_JAVADOC_CONFIGURATION_NAME = 'mavenPublishingDummyStubJavadoc' 18 | 19 | static NamedDomainObjectProvider createStubJavadocConfiguration(Project root) { 20 | def configurations = root.configurations 21 | try { 22 | return configurations.named(STUB_JAVADOC_CONFIGURATION_NAME) 23 | } catch (UnknownDomainObjectException ignored) { 24 | } 25 | 26 | root.apply plugin: 'java-base' 27 | def stubTask = root.tasks.register('createMavenPublishingDummyStubJavadoc', Jar.class) { Jar task -> 28 | task.group = 'publishing' 29 | task.archiveClassifier.set('javadoc') 30 | task.archiveBaseName.set('maven-publishing-stub') 31 | task.archiveVersion.set('') 32 | } 33 | 34 | return configurations.register(STUB_JAVADOC_CONFIGURATION_NAME) { Configuration configuration -> 35 | configuration.outgoing { 36 | artifact(stubTask) 37 | } 38 | configuration.attributes { 39 | attribute(Usage.USAGE_ATTRIBUTE, root.objects.named(Usage.class, "java-runtime")) 40 | attribute(Category.CATEGORY_ATTRIBUTE, root.objects.named(Category.class, "documentation")) 41 | attribute(Bundling.BUNDLING_ATTRIBUTE, root.objects.named(Bundling.class, "external")) 42 | attribute(DocsType.DOCS_TYPE_ATTRIBUTE, root.objects.named(DocsType.class, DocsType.JAVADOC)) 43 | } 44 | } 45 | } 46 | 47 | @Override 48 | void apply(Project target) { 49 | def stubConfig = createStubJavadocConfiguration(target.rootProject) 50 | 51 | 52 | target.pluginManager.withPlugin('java') { 53 | // setup sourcesJar & javadocs 54 | def javaExt = target.extensions.findByName('java') as JavaPluginExtension 55 | javaExt.withSourcesJar() 56 | 57 | AdhocComponentWithVariants javaComponent = (AdhocComponentWithVariants) target.components.findByName("java") 58 | javaComponent.addVariantsFromConfiguration(stubConfig.get()) { 59 | // dependencies for this variant are considered runtime dependencies 60 | it.mapToMavenScope("runtime") 61 | // and also optional dependencies, because we don't want them to leak 62 | it.mapToOptional() 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/moe/karla/maven/publishing/advtask/UploadToMavenCentral.java: -------------------------------------------------------------------------------- 1 | package moe.karla.maven.publishing.advtask; 2 | 3 | import org.apache.http.HttpResponse; 4 | import org.apache.http.client.methods.HttpPost; 5 | import org.apache.http.entity.mime.MultipartEntityBuilder; 6 | import org.apache.http.impl.client.CloseableHttpClient; 7 | import org.apache.http.impl.client.HttpClientBuilder; 8 | import org.apache.http.util.EntityUtils; 9 | 10 | import java.io.File; 11 | import java.io.InputStream; 12 | import java.net.URLEncoder; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.Base64; 15 | 16 | public class UploadToMavenCentral { 17 | private static byte[] getUserPassword() { 18 | String mavenPublishPassword = System.getenv("MAVEN_PUBLISH_PASSWORD"); 19 | if (mavenPublishPassword == null || mavenPublishPassword.isEmpty()) { 20 | throw new RuntimeException("Missing MAVEN_PUBLISH_PASSWORD"); 21 | } 22 | if (mavenPublishPassword.contains(":")) { 23 | return mavenPublishPassword.getBytes(StandardCharsets.UTF_8); 24 | } 25 | 26 | String mavenPublishUser = System.getenv("MAVEN_PUBLISH_USER"); 27 | if (mavenPublishUser == null || mavenPublishUser.isEmpty()) { 28 | throw new RuntimeException("Missing MAVEN_PUBLISH_USER and MAVEN_PUBLISH_PASSWORD not defined with user part"); 29 | } 30 | return (mavenPublishUser + ":" + mavenPublishPassword).getBytes(StandardCharsets.UTF_8); 31 | } 32 | 33 | public static void execute(String publishingName, String publishingType, File bundle) throws Throwable { 34 | 35 | CloseableHttpClient httpclient = HttpClientBuilder.create().build(); 36 | 37 | String url = "https://central.sonatype.com/api/v1/publisher/upload?publishingType=" + publishingType + "&name=" + URLEncoder.encode(publishingName, StandardCharsets.UTF_8); 38 | HttpPost httpPost = new HttpPost(url); 39 | System.out.println("POST " + url); 40 | httpPost.addHeader("Authorization", "Bearer " + Base64.getEncoder().encodeToString( 41 | getUserPassword() 42 | )); 43 | 44 | httpPost.setEntity( 45 | MultipartEntityBuilder.create() 46 | .addBinaryBody("bundle", bundle) 47 | .build() 48 | ); 49 | 50 | HttpResponse response = httpclient.execute(httpPost); 51 | int statusCode = response.getStatusLine().getStatusCode(); 52 | if (statusCode / 100 != 2) { 53 | System.err.println("HTTP Post Exited with " + statusCode); 54 | if (response.getEntity() != null) { 55 | InputStream content = response.getEntity().getContent(); 56 | if (content != null) { 57 | byte[] buffer = new byte[1024]; 58 | while (true) { 59 | int len = content.read(buffer); 60 | if (len == -1) break; 61 | 62 | System.err.write(buffer, 0, len); 63 | } 64 | } 65 | } 66 | throw new Exception("HTTP " + statusCode); 67 | } else { 68 | String deploymentId = EntityUtils.toString(response.getEntity()); 69 | System.out.println("========================================"); 70 | System.out.println("UPLOAD SUCCESS"); 71 | System.out.println("Deployment ID: " + deploymentId); 72 | System.out.println("========================================"); 73 | } 74 | httpclient.close(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Helper.kt: -------------------------------------------------------------------------------- 1 | import buildSrc.BuildConstants 2 | import org.gradle.api.Project 3 | import org.gradle.kotlin.dsl.* 4 | import org.jetbrains.kotlin.gradle.dsl.* 5 | import java.util.* 6 | 7 | fun T.ext(name: String): T { 8 | return also { Helper.rootProject.extra[name] = it } 9 | } 10 | 11 | inline fun Project.extra(name: String): T? { 12 | return rootProject.extra[name].run { 13 | if (this is T) this else null 14 | } 15 | } 16 | object Helper { 17 | @Suppress("MemberVisibilityCanBePrivate") 18 | internal lateinit var proj: Project 19 | val rootProject: Project 20 | get() = proj 21 | } 22 | 23 | val prop = Properties().apply { 24 | BuildConstants.PROPERTIES_FILE 25 | .reader().use(::load) 26 | } 27 | 28 | fun prop(name: String): String { 29 | return prop[name]?.toString() ?: "" 30 | } 31 | 32 | fun Project.optInForAllSourceSets(qualifiedClassname: String) { 33 | kotlinSourceSets!!.all { 34 | languageSettings { 35 | optIn(qualifiedClassname) 36 | } 37 | } 38 | } 39 | inline fun Any?.safeAs(): T? { 40 | return this as? T 41 | } 42 | val Project.kotlinSourceSets get() = extensions.findByName("kotlin").safeAs()?.sourceSets 43 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/PublicationHelper.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Action 2 | import org.gradle.api.Project 3 | import org.gradle.api.publish.PublishingExtension 4 | import org.gradle.api.publish.maven.MavenPom 5 | import org.gradle.api.publish.maven.MavenPublication 6 | import org.gradle.kotlin.dsl.apply 7 | import org.gradle.kotlin.dsl.configure 8 | import org.gradle.kotlin.dsl.create 9 | import org.gradle.kotlin.dsl.getByType 10 | import org.gradle.plugins.signing.SigningExtension 11 | 12 | fun Project.setupMavenCentralPublication(artifactsBlock: MavenPublication.() -> Unit) { 13 | apply(plugin = "signing") 14 | apply(plugin = "maven-publish") 15 | 16 | extensions.configure(PublishingExtension::class) { 17 | publications { 18 | create("maven") { 19 | from(components.getByName("kotlin")) 20 | groupId = rootProject.group.toString() 21 | artifactId = project.name 22 | version = rootProject.version.toString() 23 | 24 | artifactsBlock() 25 | pom(mavenPom(artifactId)) 26 | } 27 | } 28 | } 29 | extensions.configure(SigningExtension::class) { 30 | val signingKey = findProperty("signingKey")?.toString() 31 | val signingPassword = findProperty("signingPassword")?.toString() 32 | if (signingKey != null && signingPassword != null) { 33 | useInMemoryPgpKeys(signingKey, signingPassword) 34 | sign(extensions.getByType(PublishingExtension::class).publications.getByName("maven")) 35 | } else { 36 | logger.warn("子模块 ${project.name} 未找到签名配置") 37 | } 38 | } 39 | } 40 | fun mavenPom(artifactId: String): Action = action { 41 | name.set(artifactId) 42 | description.set("One of the Overflow project modules") 43 | url.set("https://github.com/MrXiaoM/Overflow") 44 | licenses { 45 | license { 46 | name.set("AGPL-3.0") 47 | url.set("https://github.com/MrXiaoM/Overflow/blob/master/LICENSE") 48 | } 49 | } 50 | developers { 51 | developer { 52 | name.set("MrXiaoM") 53 | email.set("mrxiaom@qq.com") 54 | } 55 | } 56 | scm { 57 | url.set("https://github.com/MrXiaoM/Overflow") 58 | connection.set("scm:git:https://github.com/MrXiaoM/Overflow.git") 59 | developerConnection.set("scm:git:https://github.com/MrXiaoM/Overflow.git") 60 | } 61 | } 62 | 63 | inline fun action( 64 | crossinline block: T.() -> Unit 65 | ): Action = Action { block() } 66 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # 配置文件说明 2 | 3 | 此处为各个配置文件的相关注释 4 | 5 | ## overflow.json 6 | 7 | | 键 | 默认值 | 注释 | 8 | |--------------------------------|-----------------------|------------------------------------------------------------------------------------------------------| 9 | | `ws_host` | `ws://127.0.0.1:3001` | 正向WebSocket连接地址 | 10 | | `reversed_ws_port` | `-1` | 反向WebSocket服务器端口,`-1` 为不开启反向WS。开启反向WS将会禁用正向WS连接 | 11 | | `token` | | 鉴权令牌,留空则不进行鉴权 | 12 | | `no_platform` | `false` | 是否禁止 Overflow 调用需要账号凭证的网络接口,如获取分享Key、群活跃等。在**非QQ平台**或**无法获取账号凭证**的平台 (如 Gensokyo) 使用 Overflow 请开启该选项 | 13 | | `use_cq_code` | `false` | 发送消息时,是否使用 CQ 码格式 | 14 | | `retry_times` | `5` | 正向WS断开连接后重连尝试次数,设为 `-1` 禁用自动重连 | 15 | | `retry_wait_mills` | `5000` | 每次重连之间的间隔时间 (毫秒) | 16 | | `retry_rest_mills` | `60000` | 重连次数用完后,等待多长时间 (毫秒) 后,重置次数再进行重连。设为 `-1` 禁用此功能 | 17 | | `heartbeat_check_seconds` | `60` | WebSocket 心跳策略检查时间 (秒),设为 `0` 关闭心跳策略 | 18 | | `resource_cache` | - | 媒体消息(图片、语音、短视频)自动下载缓存配置,更详细的信息请见下方 | 19 | | `drop_events_before_connected` | `true` | 是否抛弃所有在Bot成功连接之前传入的事件。比如 go-cqhttp 会在连接之后,在 Overflow 获取到 Bot 信息之前,推送以前未收到的消息。如果开启此项,则丢弃这些消息 | 20 | 21 | ## resource_cache 22 | 23 | | 键 | 默认值 | 注释 | 24 | |-----------------------|---------|--------------------------| 25 | | `enabled` | `false` | 是否启用媒体消息自动下载缓存功能 | 26 | | `keep_duration_hours` | `168` | 媒体缓存文件保留时间,超时自动删除,默认`7天` | 27 | -------------------------------------------------------------------------------- /docs/contributing/README.md: -------------------------------------------------------------------------------- 1 | # 贡献文档 2 | 3 | 本文为贡献者指南,Overflow 欢迎并感谢一切形式的贡献。 4 | 5 | 本仓库包含以下模块 6 | 7 | | 模块 | 说明 | 8 | |-------------------|-----------------| 9 | | onebot | Onebot SDK 和客户端 | 10 | | overflow-core-api | overflow 核心 api | 11 | | overflow-core | overflow 核心实现 | 12 | | overflow-core-all | 上述模块的集合,用于生产环境 | 13 | 14 | 以下为开发者须知,阅读以更快地进入开发状态,避免错误。 15 | 16 | ## JDK要求 17 | 18 | Overflow 的编译目标为 Java 1.8,在以下环境可以正常编译。 19 | 20 | | 操作系统 | JDK | 架构 | 21 | |----------------|-----------------|-------| 22 | | Windows 11 | OpenJDK 11 | amd64 | 23 | | Ubuntu 22.04.3 | AdoptOpenJDK 11 | amd64 | 24 | 25 | 若无法正常编译,可尝试使用以上环境。 26 | 27 | ## 编译 28 | 29 | 执行以下命令即可编译 Overflow 30 | ```shell 31 | ./gradlew shadowJar 32 | ``` 33 | 用于生产环境的包将会出现在 `overflow-core-all/build/libs/*-all.jar` 34 | 35 | ## 调试 36 | 37 | 使用 IntelliJ IDEA 打开此项目,在 Gradle 选项卡展开 `Overflow -> Tasks -> other`,双击执行 `runConsole` 即可运行 mirai-console。 38 | 39 | 或者在终端执行 `gradlew runConsole` 也可以达到同样的效果。 40 | 41 | 相应的 mirai 工作目录在 `./overflow-core/run`。 42 | 43 | ## mirai-console 兼容 44 | 45 | overflow-core 附有 mirai-console 兼容,在 IMirai 实现类 [Overflow](https://github.com/MrXiaoM/Overflow/blob/main/overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/Overflow.kt) 的构造函数中,若检测到存在 mirai-console 相关类,将注入插件 [OverflowCoreAsPlugin](https://github.com/MrXiaoM/Overflow/blob/main/overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/plugin/OverflowCoreAsPlugin.kt) 以帮助 Overflow 启动。 46 | 47 | ## 兼容其它 Onebot 实现 48 | 49 | 在连接成功后,Overflow 将会请求 `get_version_info`,并将 `app_name` 保存到 [RemoteBot](https://github.com/MrXiaoM/Overflow/blob/main/overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/contact/RemoteBot.kt).appName 中,如果有原版 Onebot 标准中没有的额外接口实现,可以判断 appName 再调取该接口,以兼容尽可能多的 Onebot 实现。 50 | 51 | `bot.asOnebot.impl` 即可获取当前 mirai bot 的 [Bot (Onebot)](https://github.com/MrXiaoM/Overflow/blob/main/overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/core/Bot.kt) 实现,在这里主动调取接口。 52 | 53 | 所有由服务端回复的结果,以及服务器发送的事件,均会在 onebot 模块转换为实体类,再传递给 overflow-core 模块使用,实体类均在 [cn.evolvefield.onebot.sdk](https://github.com/MrXiaoM/Overflow/tree/main/overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk) 包中。按需创建或修改。 54 | 55 | ## 添加新消息类型/格式支持 56 | 57 | mirai 中不存在的消息类型,请添加到包 [top.mrxiaom.overflow.message.data](https://github.com/MrXiaoM/overflow/tree/main/overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/message/data),参考包中原有的消息类型即可。 58 | 59 | 添加之后,到 [OnebotMessages](https://github.com/MrXiaoM/overflow/blob/dbd98d2b867356aa64b78d1a89e6cf8673337d0a/overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/message/OnebotMessages.kt) 60 | + 在 `registerSerializers` 注册序列化器,以便该消息能够在 mirai 这边被序列化与反序列化 61 | + 在 `serializeToOneBotJsonArray` 添加该消息转换为 Onebot 消息段的实现 62 | + 在 `deserializeFromOneBotJson` 添加 Onebot 消息段转换为该消息的实现 63 | + 在 `Message.messageType` 添加该消息的 Onebot 消息类型名 64 | 65 | 正如上方步骤所述,`serializeToOneBotJsonArray` 和 `deserializeFromOneBotJson` 是 mirai 消息与 Onebot 消息段互相转换的重要方法。 66 | 方法开头获取了当前 Bot 的 Onebot 实现名称,如果某些 Onebot 实现增加了扩展消息格式,可以添加到这两个方法中。 67 | 68 | ## 提交高质量的 commit 以及 PR 69 | 70 | 另请参见 [mirai 贡献](https://github.com/mamoe/mirai/tree/dev/docs/contributing#%E6%8F%90%E4%BA%A4%E9%AB%98%E8%B4%A8%E9%87%8F%E7%9A%84-commit-%E4%BB%A5%E5%8F%8A-pr) 71 | -------------------------------------------------------------------------------- /docs/install/MCL.md: -------------------------------------------------------------------------------- 1 | # 编辑 MCL 的 config.json 2 | 3 | > 开发版本需要 [该版本 MCL](https://github.com/iTXTech/mirai-console-loader/pull/192) 才可使用本方法。 4 | 5 | 首先,使用文本编辑器打开 `config.json` 大概是这样的 6 | 7 | ```json5 8 | { 9 | // ... 10 | "maven_repo": [ 11 | "https://maven.aliyun.com/repository/public" 12 | ], 13 | // ... 14 | "packages": { 15 | // ... 16 | "net.mamoe:mirai-core-all": { 17 | "channel": "maven-stable", 18 | "version": "2.16.0", 19 | "type": "libs", 20 | "versionLocked": false 21 | }, 22 | // ... 23 | } 24 | } 25 | ``` 26 | ## 第一步. 加仓库 27 | 在 `maven_repo` 中加入快照仓库地址,添加完成后如下 28 | 29 | ```json5 30 | { 31 | // ... 32 | "maven_repo": [ 33 | "https://maven.aliyun.com/repository/public", 34 | "https://s01.oss.sonatype.org/content/repositories/snapshots" 35 | ], 36 | // ... 37 | } 38 | ``` 39 | 40 | ## 第二步. 加依赖 41 | 42 | - 将其中的 `"net.mamoe:mirai-core-all"` 改为 `"top.mrxiaom.mirai:overflow-core-all"` 43 | - 将其中的 `"maven-stable"` 改为 `"maven-snapshots"` 44 | - 将 `version` 的值 `2.16.0` 改为 Overflow 版本号 45 | 46 | 快照仓库中 Overflow 版本号的格式为 `major.minor.patch.commits-shortHash-SNAPSHOT`, 47 | 例如:`0.9.9.481-d59fa60-SNAPSHOT`,详见 #92 48 | 49 | 你可以在 [这里](https://s01.oss.sonatype.org/content/repositories/snapshots/top/mrxiaom/mirai/overflow-core/) 查询已发布到快照仓库的开发版本列表。 50 | 你可以在 [这里](https://central.sonatype.com/search?q=g%3Atop.mrxiaom.mirai) 查询所有正式版本列表。 51 | -------------------------------------------------------------------------------- /docs/install/MCLOverflow.md: -------------------------------------------------------------------------------- 1 | # 通过 MCL 命令行安装 2 | 3 | # unix 安装教程 4 | 5 | 在 Linux/MacOS/Termux 下安装 Overflow。 6 | 7 | 在终端依次执行以下命令即可安装 8 | 9 | ```shell 10 | mkdir mcl 11 | cd mcl 12 | wget https://github.com/MrXiaoM/mirai-console-loader/releases/download/v2.1.2-patch1/with-overflow.zip 13 | unzip with-overflow.zip 14 | chmod +x mcl 15 | ./mcl -u --dry-run 16 | ``` 17 | 18 | 若网络环境不佳,可将 `https://github.com` 改为 `https://mirror.ghproxy.com/github.com` 再试。 19 | 20 | 等待安装完成后,以后只需执行 21 | 22 | ```shell 23 | ./mcl 24 | ``` 25 | 26 | 即可启动 Overflow。 27 | 28 | # Windows 安装教程 29 | 30 | 下载 [已修改的MCL](https://github.com/MrXiaoM/mirai-console-loader/releases/download/v2.1.2-patch1/with-overflow.zip) 并解压,若网络环境不佳,可将 https://github.com 改为 https://mirror.ghproxy.com/github.com 再试。 31 | 32 | 在解压的文件夹打开 CMD,执行 33 | 34 | ```shell 35 | mcl -u --dry-run 36 | ``` 37 | 38 | 等待安装完成后,以后只需打开 `mcl.cmd` 即可启动 Overflow。 39 | -------------------------------------------------------------------------------- /docs/install/MCLScript.md: -------------------------------------------------------------------------------- 1 | # MCL + 脚本安装 2 | 3 | > 开发版本需要 [该版本 MCL](https://github.com/iTXTech/mirai-console-loader/pull/192) 才可使用本方法。 4 | 5 | 如果需要开发版本,首先打开配置文件 config.json,在 `maven_repo` 中加入快照仓库地址,添加完成后如下 6 | 7 | > 如果不需要开发版本,直接打开下述的脚本即可。 8 | 9 | ```json5 10 | { 11 | // ... 12 | "maven_repo": [ 13 | "https://maven.aliyun.com/repository/public", 14 | "https://s01.oss.sonatype.org/content/repositories/snapshots" 15 | ], 16 | // ... 17 | } 18 | ``` 19 | 20 | 保存配置文件,然后 21 | 22 | - Windows 系统下载 [install_overflow_mcl.cmd](/docs/install/install_overflow_mcl.cmd) 23 | - Linux/MacOS 系统下载 [install_overflow_mcl.sh](/docs/install/install_overflow_mcl.sh) 24 | 25 | 将该脚本放到 MCL 所在目录,打开该脚本等待安装即可。 26 | 27 | 此脚本仅用于安装,升级请直接执行 `mcl -u` 28 | -------------------------------------------------------------------------------- /docs/install/README.md: -------------------------------------------------------------------------------- 1 | # 安装教程 2 | 3 | 本目录存放 Overflow 的安装教程,不含 Onebot 实现的部署与连接教程。 4 | 5 | 关于部分 Onebot 实现的部署教程,另请参见 [数据消散Wiki](https://wiki.mrxiaom.top/overflow)。 6 | -------------------------------------------------------------------------------- /docs/install/Raw.md: -------------------------------------------------------------------------------- 1 | # 替换 mirai-core 类库 2 | 3 | 需要准备: 4 | - overflow-core-all 的编译产物,可在 [Actions](https://github.com/MrXiaoM/Overflow/actions/workflows/dev.yml) 或 [快照仓库](https://s01.oss.sonatype.org/content/repositories/snapshots/top/mrxiaom/mirai/overflow-core-all/) 或 [Maven Central](https://repo.maven.apache.org/maven2/top/mrxiaom/mirai/overflow-core-all/) 下载。也可以拉取本项目运行 `./gradlew shadowJar` 后在 `overflow-core-all/build/libs` 中取得 (`-all.jar` 文件) 5 | - mirai-console 的编译产物,可[在此](https://mirrors.huaweicloud.com/repository/maven/net/mamoe/mirai-console/2.16.0/mirai-console-2.16.0-all.jar)下载 6 | - mirai-console-terminal 的编译产物,可[在此](https://mirrors.huaweicloud.com/repository/maven/net/mamoe/mirai-console-terminal/2.16.0/mirai-console-terminal-2.16.0-all.jar)下载 7 | 8 | 创建 `libs` 文件夹,将以上内容放入该文件夹。 9 | 10 | **注意,以上获取的文件均为 `-all.jar` 结尾的文件** 11 | 12 | 创建启动脚本,`start.cmd`(Windows) 或 `start.sh`(Linux/MacOS),按需写入以下内容。 13 | 14 | `start.cmd`(Windows) 如下 15 | ```shell 16 | java -cp ./libs/* net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader 17 | pause 18 | ``` 19 | `start.sh`(Linux/MacOS) 如下 20 | ```shell 21 | java -cp "$CLASSPATH:./libs/*" net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader 22 | ``` 23 | 24 | 打开启动脚本即可。 25 | -------------------------------------------------------------------------------- /docs/install/install_overflow_mcl.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cmd /c mcl --remove-package net.mamoe:mirai-core-all 3 | cmd /c mcl --update-package top.mrxiaom.mirai:overflow-core-all --channel maven-snapshots --type libs 4 | cmd /c mcl --update --dry-run 5 | pause 6 | -------------------------------------------------------------------------------- /docs/install/install_overflow_mcl.sh: -------------------------------------------------------------------------------- 1 | ./mcl --remove-package net.mamoe:mirai-core-all 2 | ./mcl --update-package top.mrxiaom.mirai:overflow-core-all --channel maven-snapshots --type libs 3 | ./mcl --update --dry-run 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.jvmargs=-Xmx7000m -Dfile.encoding=UTF-8 --illegal-access=permit -Dkotlin.daemon.jvm.options=--illegal-access=permit --add-opens java.base/java.util=ALL-UNNAMED 3 | org.gradle.parallel=true 4 | org.gradle.vfs.watch=true 5 | mirror.repo=https://mirrors.huaweicloud.com/repository/maven/ 6 | kotlin.version=1.8.0 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrXiaoM/Overflow/5f559bfb1cf58dc889f8d548446fc8ca18a021ee/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /overflow-core-all/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | id("com.github.johnrengelman.shadow") 4 | } 5 | 6 | setupMavenCentralPublication { 7 | artifact(tasks.shadowJar) 8 | artifact(tasks.kotlinSourcesJar) 9 | } 10 | 11 | tasks.test { 12 | useJUnitPlatform() 13 | } 14 | 15 | dependencies { 16 | api(project(":overflow-core-api")) 17 | api(project(":overflow-core")) 18 | } 19 | 20 | tasks { 21 | shadowJar { 22 | mapOf( 23 | "com.google.gson" to "gson", 24 | "org.java_websocket" to "websocket", 25 | "io.netty" to "netty" 26 | ).forEach { (original, target) -> 27 | relocate(original, "top.mrxiaom.overflow.internal.deps.$target") 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /overflow-core-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | kotlin("plugin.serialization") 4 | id("org.jetbrains.dokka") 5 | id("com.github.gmazzo.buildconfig") 6 | id("me.him188.kotlin-jvm-blocking-bridge") 7 | } 8 | 9 | var commitHash = extra("commitHash") ?: "local" 10 | var commitCount: Int? = extra("commitCount") 11 | val miraiVersion = extra("miraiVersion") ?: "2.16.0" 12 | 13 | kotlin { 14 | optInForAllSourceSets("net.mamoe.mirai.utils.MiraiExperimentalApi") 15 | optInForAllSourceSets("net.mamoe.mirai.utils.MiraiInternalApi") 16 | } 17 | 18 | buildConfig { 19 | className("BuildConstants") 20 | packageName("top.mrxiaom.overflow") 21 | useKotlinOutput() 22 | buildConfigField("String", "VERSION", "\"${project.version}\"") 23 | buildConfigField("String", "MIRAI_VERSION", "\"$miraiVersion\"") 24 | buildConfigField("String", "COMMIT_HASH", "\"$commitHash\"") 25 | buildConfigField("kotlin.Int?", "COMMIT_COUNT", commitCount?.toString() ?: "null") 26 | buildConfigField("java.time.Instant", "BUILD_TIME", "java.time.Instant.ofEpochSecond(${System.currentTimeMillis() / 1000L}L)") 27 | } 28 | 29 | tasks.register("dokkaJavadocJar") { 30 | group = "documentation" 31 | dependsOn(tasks.dokkaJavadoc) 32 | from(tasks.dokkaJavadoc.flatMap { it.outputDirectory }) 33 | archiveClassifier.set("javadoc") 34 | } 35 | 36 | setupMavenCentralPublication { 37 | artifact(tasks.kotlinSourcesJar) 38 | artifact(tasks.getByName("dokkaJavadocJar")) 39 | } 40 | 41 | dependencies { 42 | api("net.mamoe:mirai-core-api:$miraiVersion") 43 | api("net.mamoe:mirai-core-utils:$miraiVersion") 44 | 45 | api("me.him188:kotlin-jvm-blocking-bridge-runtime:3.0.0-180.1") 46 | 47 | api("org.slf4j:slf4j-api:2.0.5") 48 | } 49 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/OverflowAPI.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow 2 | 3 | import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge 4 | import net.mamoe.mirai.Bot 5 | import net.mamoe.mirai.Mirai 6 | import net.mamoe.mirai.message.data.* 7 | import net.mamoe.mirai.utils.MiraiLogger 8 | import top.mrxiaom.overflow.contact.RemoteBot 9 | import java.io.File 10 | import kotlin.time.Duration 11 | 12 | val Overflow: OverflowAPI 13 | get() = OverflowAPI.get() 14 | interface OverflowAPI { 15 | /** 16 | * Overflow 版本号 17 | */ 18 | val version: String 19 | val botStarter: IBotStarter 20 | /** 21 | * 以 Onebot 格式新建图片消息 22 | * 23 | * 另请参见 [Onebot file 参数说明](https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E5%9B%BE%E7%89%87) 24 | */ 25 | fun imageFromFile(file: String): Image 26 | /** 27 | * 以 Onebot 格式新建语音消息 28 | * 29 | * 另请参见 [Onebot file 参数说明](https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E5%9B%BE%E7%89%87) 30 | */ 31 | fun audioFromFile(file: String): Audio 32 | /** 33 | * 以 Onebot 格式新建短视频消息 34 | * 35 | * 另请参见 [Onebot file 参数说明](https://github.com/botuniverse/onebot-11/blob/master/message/segment.md#%E5%9B%BE%E7%89%87) 36 | */ 37 | fun videoFromFile(file: String): ShortVideo 38 | 39 | /** 40 | * 序列化 mirai 格式消息为 Onebot 格式 json 消息段 41 | */ 42 | @Deprecated( 43 | message = "Please use serializeMessage(bot, message)", 44 | ReplaceWith("serializeMessage(bot, message)") 45 | ) 46 | fun serializeMessage(message: Message): String = serializeMessage(null, message) 47 | 48 | /** 49 | * 序列化 mirai 格式消息为 Onebot 格式 json 消息段 50 | */ 51 | fun serializeMessage(bot: RemoteBot?, message: Message): String 52 | 53 | /** 54 | * 序列化 Onebot 格式 json 消息段为 CQ 码 55 | */ 56 | fun serializeJsonToCQCode(messageJson: String): String 57 | 58 | /** 59 | * 序列化 Onebot 格式 CQ 码 为 json 消息段 60 | */ 61 | fun serializeCQCodeToJson(messageCQCode: String): String 62 | 63 | /** 64 | * 反序列化 Onebot 格式 json 消息段或 CQ 码,反序列化为 mirai 格式消息 65 | */ 66 | @JvmBlockingBridge 67 | suspend fun deserializeMessage(bot: Bot, message: String): MessageChain 68 | 69 | /** 70 | * 反序列化 json 消息段为 mirai 格式消息 71 | */ 72 | @JvmBlockingBridge 73 | suspend fun deserializeMessageFromJson(bot: Bot, message: String): MessageChain? 74 | 75 | /** 76 | * 反序列化 CQ 码 为 mirai 格式消息 77 | */ 78 | @JvmBlockingBridge 79 | suspend fun deserializeMessageFromCQCode(bot: Bot, message: String): MessageChain? 80 | 81 | /** 82 | * 配置消息自动缓存选项,传入 null 的值不会被修改 83 | * @param enabled 是否启用媒体消息(图片、语音、视频)缓存。 84 | * @param saveDir 缓存文件夹路径。 85 | * @param keepDuration 缓存文件保存时间。在收到消息时,文件将会自动清理。 86 | */ 87 | fun configureMessageCache(enabled: Boolean? = null, saveDir: File? = null, keepDuration: Duration? = null) 88 | 89 | companion object { 90 | public val logger = MiraiLogger.Factory.create(OverflowAPI::class, "Overflow") 91 | @JvmStatic 92 | fun get(): OverflowAPI = Mirai as OverflowAPI 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/action/ActionContext.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.action 2 | 3 | import kotlinx.serialization.Serializable 4 | import java.util.function.Consumer 5 | 6 | /** 7 | * Onebot 主动操作上下文 8 | */ 9 | @Serializable 10 | data class ActionContext( 11 | val action: String, 12 | /** 13 | * 是否在调用失败时抛出异常。 14 | * 15 | * - 如果为 `false`(默认),则使用 warn 级别打印一条日志 16 | * - 如果为 `true`,则抛出异常 17 | * - 如果为 `null`,则使用 trace 级别打印日志,不抛出异常 18 | */ 19 | var throwExceptions: Boolean? = false, 20 | /** 21 | * 是否将返回结果没有 `"status"` 的情况,当作调用成功。 22 | * 23 | * 若开启此选项,明确出现网络问题或者明确指定 `"status": "failed"` 才会当作调用失败。 24 | */ 25 | var ignoreStatus: Boolean = false, 26 | ) { 27 | class Builder private constructor(action: String) { 28 | private val context = ActionContext(action) 29 | 30 | /** 31 | * @see ActionContext.throwExceptions 32 | */ 33 | fun throwExceptions(flag: Boolean?) { 34 | context.throwExceptions = flag 35 | } 36 | 37 | /** 38 | * (旧版方法) 是否在报错时显示警告日志 39 | * 40 | * 迁移说明: 41 | * - `null` 的效果,与 `showWarning=false` 一致 42 | * - `false` 的效果,与 `showWarning=true` 一致 43 | * @see ActionContext.throwExceptions 44 | */ 45 | fun showWarning(flag: Boolean) { 46 | if (flag) { 47 | context.throwExceptions = false 48 | } else { 49 | context.throwExceptions = null 50 | } 51 | } 52 | 53 | /** 54 | * @see ActionContext.ignoreStatus 55 | */ 56 | fun ignoreStatus(flag: Boolean) { 57 | context.ignoreStatus = flag 58 | } 59 | 60 | /** 61 | * 构建一个新的 ActionContext 62 | */ 63 | fun build(): ActionContext { 64 | return ActionContext( 65 | action = context.action, 66 | throwExceptions = context.throwExceptions, 67 | ignoreStatus = context.ignoreStatus 68 | ) 69 | } 70 | 71 | companion object { 72 | fun create(action: String): Builder = Builder(action) 73 | } 74 | } 75 | companion object { 76 | @JvmStatic 77 | fun builder(action: String): Builder = Builder.create(action) 78 | 79 | fun builder(action: String, block: Builder.() -> Unit): Builder = builder(action).apply(block) 80 | 81 | fun build(action: String, block: Builder.() -> Unit): ActionContext = builder(action).apply(block).build() 82 | 83 | @JvmStatic 84 | fun builder(action: String, block: Consumer): Builder { 85 | val builder = builder(action) 86 | block.accept(builder) 87 | return builder 88 | } 89 | 90 | @JvmStatic 91 | fun build(action: String, block: Consumer): ActionContext { 92 | val builder = builder(action) 93 | block.accept(builder) 94 | return builder.build() 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/contact/RemoteBot.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.contact 2 | 3 | import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge 4 | import net.mamoe.mirai.Bot 5 | import net.mamoe.mirai.message.data.MessageChain 6 | import top.mrxiaom.overflow.action.ActionContext 7 | import kotlin.jvm.Throws 8 | 9 | interface RemoteBot { 10 | /** 11 | * Onebot 实现名称 12 | */ 13 | val appName: String 14 | 15 | /** 16 | * Onebot 实现版本 17 | */ 18 | val appVersion: String 19 | 20 | /** 21 | * 配置中是否已禁止 Overflow 调用网络接口 22 | */ 23 | val noPlatform: Boolean 24 | 25 | /** 26 | * 执行自定义动作 27 | * 28 | * 详见 [Onebot 11 公开 API](https://github.com/botuniverse/onebot-11/blob/master/api/public.md)。 29 | * @param actionPath API请求路径 30 | * @param params 请求参数,可为空 31 | * @return 服务端返回信息,格式为 json 32 | */ 33 | @JvmBlockingBridge 34 | suspend fun executeAction(actionPath: String, params: String?): String 35 | 36 | /** 37 | * 执行自定义动作 38 | * 39 | * 详见 [Onebot 11 公开 API](https://github.com/botuniverse/onebot-11/blob/master/api/public.md) 和 [go-cqhttp 公开 API](https://docs.go-cqhttp.org/api)。 40 | * 41 | * 用例: 42 | * ```kotlin 43 | * // kotlin 44 | * val result = onebot.executeAction( 45 | * context = ActionContext.build("get_guild_list") { 46 | * throwExceptions(true) 47 | * }, 48 | * params = null 49 | * ) 50 | * ``` 51 | * ```java 52 | * // java 53 | * String result = onebot.executeAction( 54 | * ActionContext.build("get_guild_list", builder -> { 55 | * builder.throwExceptions(true); 56 | * }), 57 | * null 58 | * ); 59 | * ``` 60 | * 61 | * @param context 自定义请求上下文,可控制抛出异常、忽略标准格式等选项 62 | * @param params 请求参数,可为空 63 | * @return 服务端返回信息,格式为 json 64 | */ 65 | @JvmBlockingBridge 66 | suspend fun executeAction(context: ActionContext, params: String?): String 67 | 68 | /** 69 | * 发送自定义 WebSocket 消息 70 | * @param message 消息内容 71 | */ 72 | fun sendRawWebSocketMessage(message: String) 73 | 74 | /** 75 | * 发送自定义 WebSocket 消息 76 | * @param message 消息内容 77 | */ 78 | fun sendRawWebSocketMessage(message: ByteArray) 79 | 80 | /** 81 | * 通过消息id获取消息,并反序列化为 mirai 格式消息 82 | * @param messageId 消息id 83 | */ 84 | @JvmBlockingBridge 85 | suspend fun getMsg(messageId: Int): MessageChain? 86 | 87 | companion object { 88 | /** 89 | * 尝试将 mirai Bot 转换为 Overflow RemoteBot 90 | * 91 | * 当类型不正确时,或没有 Overflow 实现时,将会抛出异常 92 | */ 93 | @JvmStatic 94 | @get:Throws(ClassNotFoundException::class, ClassCastException::class) 95 | val Bot.asRemoteBot 96 | get() = this as RemoteBot 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/contact/RemoteGroup.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.contact 2 | 3 | import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge 4 | import net.mamoe.mirai.contact.ContactList 5 | import net.mamoe.mirai.contact.Group 6 | import net.mamoe.mirai.contact.NormalMember 7 | import net.mamoe.mirai.contact.announcement.Announcements 8 | 9 | interface RemoteGroup { 10 | @JvmBlockingBridge 11 | suspend fun updateGroupMemberList(): ContactList 12 | @JvmBlockingBridge 13 | suspend fun updateAnnouncements(): Announcements 14 | 15 | /** 16 | * 设置群消息反应表情 17 | * 18 | * @param messageId 消息ID,可通过 `MessageSource.ids[0]` 取得 19 | * @param icon 图标ID,详见 Overflow 开发文档 20 | * @param enable 添加或取消反应表情 21 | */ 22 | @JvmBlockingBridge 23 | suspend fun setMsgReaction(messageId: Int, icon: String, enable: Boolean) 24 | 25 | companion object { 26 | /** 27 | * 尝试将 mirai Group 转换为 Overflow RemoteGroup 28 | * 29 | * 当类型不正确时,或没有 Overflow 实现时,将会抛出异常 30 | */ 31 | @JvmStatic 32 | @get:Throws(ClassNotFoundException::class, ClassCastException::class) 33 | val Group.asRemoteGroup 34 | get() = this as RemoteGroup 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/contact/RemoteUser.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.contact 2 | 3 | import net.mamoe.mirai.contact.User 4 | 5 | interface RemoteUser { 6 | val onebotData: String 7 | 8 | companion object { 9 | /** 10 | * 尝试将 mirai User 转换为 Overflow RemoteUser 11 | * 12 | * 当类型不正确时,或没有 Overflow 实现时,将会抛出异常 13 | */ 14 | @JvmStatic 15 | @get:Throws(ClassNotFoundException::class, ClassCastException::class) 16 | val User.asRemoteUser 17 | get() = this as RemoteUser 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/contact/Updatable.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.contact 2 | 3 | import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge 4 | 5 | interface Updatable { 6 | /** 7 | * 请求更新信息 8 | */ 9 | @JvmBlockingBridge 10 | suspend fun queryUpdate() 11 | } 12 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/event/LegacyGuildMessageEvent.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.event 2 | 3 | import net.mamoe.mirai.Bot 4 | import net.mamoe.mirai.contact.MemberPermission 5 | import net.mamoe.mirai.event.AbstractEvent 6 | import net.mamoe.mirai.message.data.MessageChain 7 | 8 | /** 9 | * 临时的频道消息事件 10 | * 11 | * 在正式确定频道接口之前,可使用此事件接收频道消息 12 | */ 13 | @Deprecated("腾讯自己都不积极维护频道了,没必要支持了") 14 | public class LegacyGuildMessageEvent( 15 | /** 16 | * 接收消息的机器人 17 | */ 18 | public val bot: Bot, 19 | /** 20 | * 频道ID 21 | */ 22 | public val guildId: String, 23 | /** 24 | * 子频道ID 25 | */ 26 | public val channelId: String, 27 | /** 28 | * 消息ID 29 | */ 30 | public val messageId: String, 31 | /** 32 | * 收到的消息 33 | */ 34 | public val message: MessageChain, 35 | /** 36 | * 消息发送者ID 37 | */ 38 | public val senderId: Long, 39 | /** 40 | * 消息发送者TinyID 41 | */ 42 | public val senderTinyId: String, 43 | /** 44 | * 消息发送者昵称 45 | */ 46 | public val senderNick: String, 47 | /** 48 | * 消息发送者名片 49 | */ 50 | public val senderNameCard: String, 51 | /** 52 | * 消息发送者头衔 53 | */ 54 | public val senderTitle: String, 55 | /** 56 | * 消息发送者等级 57 | */ 58 | public val senderLevel: String, 59 | /** 60 | * 消息发送者权限 61 | */ 62 | public val senderRole: MemberPermission, 63 | /** 64 | * 消息发送时间 65 | */ 66 | public val time: Int 67 | ): AbstractEvent() { 68 | public override fun toString(): String = 69 | "LegacyGuildMessageEvent(guildId=$guildId, channelId=$channelId, senderNameCard=$senderNameCard, sender=${senderId}, permission=${senderRole}, message=$message)" 70 | } -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/event/MemberEssenceNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.event 2 | 3 | import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge 4 | import net.mamoe.mirai.Bot 5 | import net.mamoe.mirai.contact.Group 6 | import net.mamoe.mirai.contact.NormalMember 7 | import net.mamoe.mirai.event.AbstractEvent 8 | import net.mamoe.mirai.message.data.MessageChain 9 | import top.mrxiaom.overflow.contact.RemoteBot.Companion.asRemoteBot 10 | 11 | public abstract class MemberEssenceNoticeEvent private constructor( 12 | /** 13 | * 机器人 14 | */ 15 | public val bot: Bot, 16 | /** 17 | * 群聊 18 | */ 19 | public val group: Group, 20 | /** 21 | * 消息发送者 22 | */ 23 | public val member: NormalMember, 24 | /** 25 | * 精华消息添加者 26 | */ 27 | public val operator: NormalMember, 28 | /** 29 | * 精华消息ID 30 | */ 31 | public val messageId: Int 32 | ): AbstractEvent() { 33 | 34 | /** 35 | * 获取精华消息内容 36 | */ 37 | @JvmBlockingBridge 38 | suspend fun queryMessage(): MessageChain? { 39 | return bot.asRemoteBot.getMsg(messageId) 40 | } 41 | 42 | protected open val type: String = "" 43 | 44 | public class Add( 45 | bot: Bot, 46 | group: Group, 47 | member: NormalMember, 48 | operator: NormalMember, 49 | messageId: Int 50 | ) : MemberEssenceNoticeEvent(bot, group, member, operator, messageId) { 51 | override val type: String = ".Add" 52 | } 53 | 54 | public class Delete( 55 | bot: Bot, 56 | group: Group, 57 | member: NormalMember, 58 | operator: NormalMember, 59 | messageId: Int 60 | ) : MemberEssenceNoticeEvent(bot, group, member, operator, messageId) { 61 | override val type: String = ".Delete" 62 | } 63 | 64 | public override fun toString(): String = "MemberEssenceNoticeEvent$type(bot=${bot.id}, group=${group.id}, operator=${operator.id}, member=${member.id}, messageId=$messageId)" 65 | } 66 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/event/MessageReactionEvent.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.event 2 | 3 | import net.mamoe.mirai.Bot 4 | import net.mamoe.mirai.contact.Group 5 | import net.mamoe.mirai.contact.NormalMember 6 | import net.mamoe.mirai.event.AbstractEvent 7 | import net.mamoe.mirai.event.events.BotEvent 8 | 9 | public class MessageReactionEvent( 10 | /** 11 | * 机器人 12 | */ 13 | public override val bot: Bot, 14 | /** 15 | * 群聊 16 | */ 17 | public val group: Group, 18 | /** 19 | * 操作人,null 时为机器人 20 | */ 21 | public val operator: NormalMember?, 22 | /** 23 | * 被操作的消息ID 24 | */ 25 | public val messageId: Int, 26 | /** 27 | * 表情ID,详见 Overflow 开发文档 28 | */ 29 | public val reaction: String, 30 | /** 31 | * 进行此操作后的表情总数 32 | */ 33 | public val count: Int, 34 | /** 35 | * 表情被添加还是移除 36 | * 37 | * `true` 为添加,`false` 为移除 38 | */ 39 | public val operation: Boolean, 40 | ): BotEvent, AbstractEvent() 41 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/event/UnsolvedOnebotEvent.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.event 2 | 3 | import kotlinx.serialization.json.Json 4 | import kotlinx.serialization.json.JsonObject 5 | import kotlinx.serialization.json.jsonObject 6 | import net.mamoe.mirai.Bot 7 | import net.mamoe.mirai.event.AbstractEvent 8 | import net.mamoe.mirai.event.CancellableEvent 9 | 10 | public class UnsolvedOnebotEvent( 11 | /** 12 | * 机器人QQ号 13 | */ 14 | public val botId: Long, 15 | /** 16 | * 收到的原事件内容 17 | */ 18 | public val messageRaw: String, 19 | /** 20 | * 收到事件的时间 21 | */ 22 | public val time: Long, 23 | ): AbstractEvent(), CancellableEvent { 24 | /** 25 | * 获取机器人实例,可能会失败 26 | */ 27 | public val bot: Bot 28 | get() = Bot.getInstance(botId) 29 | 30 | /** 31 | * 获取机器人实例,可能会失败 32 | */ 33 | public val botOrNull: Bot? 34 | get() = Bot.getInstanceOrNull(botId) 35 | 36 | /** 37 | * 读取事件内容为 json 38 | */ 39 | public val json: JsonObject 40 | get() = Json.parseToJsonElement(messageRaw).jsonObject 41 | 42 | public override fun toString(): String = "UnsolvedOnebotEvent(bot=$botId, time=$time, message=$messageRaw)" 43 | } 44 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/message/data/ContactRecommend.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") 2 | package top.mrxiaom.overflow.message.data 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | import net.mamoe.mirai.message.code.CodableMessage 7 | import net.mamoe.mirai.message.data.AbstractPolymorphicMessageKey 8 | import net.mamoe.mirai.message.data.ConstrainSingle 9 | import net.mamoe.mirai.message.data.MessageContent 10 | import net.mamoe.mirai.message.data.MessageKey 11 | import net.mamoe.mirai.utils.safeCast 12 | 13 | /** 14 | * 推荐联系人 15 | */ 16 | @Serializable 17 | @SerialName(ContactRecommend.SERIAL_NAME) 18 | public data class ContactRecommend( 19 | /** 20 | * 联系人类型 21 | */ 22 | public val contactType: ContactType, 23 | public val id: Long 24 | ) : MessageContent, ConstrainSingle, CodableMessage { 25 | private val _contentValue: String by lazy(LazyThreadSafetyMode.NONE) { 26 | val typeString = when(contactType) { 27 | ContactType.Group -> "群聊" 28 | ContactType.Private -> "好友" 29 | } 30 | "[推荐$typeString]$id" 31 | } 32 | public override fun toString(): String = "[overflow:contact,$contactType,$id]" 33 | public override fun contentToString(): String = _contentValue 34 | 35 | override fun appendMiraiCodeTo(builder: StringBuilder) { 36 | builder.append("[mirai:contact:").append(contactType.name.lowercase()).append(",").append(id).append("]") 37 | } 38 | 39 | /** 40 | * 推荐联系人类型 41 | */ 42 | public enum class ContactType { 43 | /** 44 | * 群聊 45 | */ 46 | Group, 47 | 48 | /** 49 | * 私聊 50 | */ 51 | Private 52 | } 53 | 54 | override val key: MessageKey get() = Key 55 | public companion object Key : 56 | AbstractPolymorphicMessageKey(MessageContent, { it.safeCast() }) { 57 | public const val SERIAL_NAME: String = "Contact" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/message/data/FileMessageWithUrl.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.message.data 2 | 3 | import net.mamoe.mirai.message.data.FileMessage 4 | 5 | /** 6 | * 带链接的文件消息 7 | * 8 | * 注意,这个消息类型提供的文件链接并不可靠,可能为空字符串。 9 | * 10 | * 基本上,仅在开启 use_group_upload_event_for_file_message 的情况下,从消息收到文件消息时可提供链接。 11 | */ 12 | interface FileMessageWithUrl : FileMessage { 13 | val url: String 14 | } 15 | 16 | val FileMessage.withUrl: FileMessageWithUrl 17 | get() = this as FileMessageWithUrl 18 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/message/data/InlineKeyboard.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.message.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import net.mamoe.mirai.message.data.* 6 | import net.mamoe.mirai.utils.safeCast 7 | 8 | /** 9 | * 内联键盘消息. 常见于 Markdown 消息下方 10 | */ 11 | @Serializable 12 | @SerialName(InlineKeyboard.SERIAL_NAME) 13 | public data class InlineKeyboard( 14 | /** 15 | * 官方机器人 appId 16 | */ 17 | public val botAppId: Long, 18 | /** 19 | * 按键行列表 20 | */ 21 | public val rows: List 22 | ) : MessageContent { 23 | private val string by lazy { 24 | "[overflow:inlinekeyboard,$botAppId,${rows.joinToString(",")}]" 25 | } 26 | private val content by lazy { 27 | "\n[内联键盘]\n" + 28 | rows.joinToString("\n") { row -> row.buttons.joinToString(", ") { it.label } } 29 | } 30 | public override fun toString(): String = string 31 | public override fun contentToString(): String = content 32 | 33 | public companion object Key : 34 | AbstractPolymorphicMessageKey(MessageContent, { it.safeCast() }) { 35 | public const val SERIAL_NAME: String = "InlineKeyboard" 36 | } 37 | } 38 | 39 | /** 40 | * 内联键盘按键行 41 | */ 42 | @Serializable 43 | public data class InlineKeyboardRow( 44 | /** 45 | * 这一行的按钮列表 46 | */ 47 | val buttons: List 48 | ) { 49 | private val stringContent by lazy { 50 | "{row:[${buttons.joinToString(",")}]}" 51 | } 52 | override fun toString(): String = stringContent 53 | } 54 | /** 55 | * 内联键盘按钮 56 | */ 57 | @Serializable 58 | public data class InlineKeyboardButton( 59 | val id: String, 60 | val label: String, 61 | val visitedLabel: String, 62 | val style: Int, 63 | val type: Int, 64 | val clickLimit: Int, 65 | val unsupportTips: String, 66 | val data: String, 67 | val atBotShowChannelList: Boolean, 68 | val permissionType: Int, 69 | val specifyRoleIds: List, 70 | val specifyTinyIds: List 71 | ) { 72 | override fun toString(): String { 73 | return "[button:$label]" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/message/data/Location.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") 2 | package top.mrxiaom.overflow.message.data 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | import net.mamoe.mirai.message.code.CodableMessage 7 | import net.mamoe.mirai.message.data.AbstractPolymorphicMessageKey 8 | import net.mamoe.mirai.message.data.ConstrainSingle 9 | import net.mamoe.mirai.message.data.MessageContent 10 | import net.mamoe.mirai.message.data.MessageKey 11 | import net.mamoe.mirai.utils.safeCast 12 | 13 | /** 14 | * 定位分享 15 | */ 16 | @Serializable 17 | @SerialName(Location.SERIAL_NAME) 18 | public data class Location( 19 | /** 20 | * 纬度 21 | */ 22 | public val lat: Float, 23 | /** 24 | * 经度 25 | */ 26 | public val lon: Float, 27 | /** 28 | * 定位消息标题 (可能为空),通常为地名 29 | */ 30 | public val title: String = "", 31 | 32 | /** 33 | * 定位消息内容 (可能为空),通常为详细地址 34 | */ 35 | public val content: String = "" 36 | ) : MessageContent, ConstrainSingle, CodableMessage { 37 | private val _contentValue: String by lazy(LazyThreadSafetyMode.NONE) { 38 | "[位置]$title $content(经度:$lon 纬度:$lat)" 39 | } 40 | public override fun toString(): String = "[overflow:location,lon=$lat,lat=$lon,title=$title,content=$content]" 41 | public override fun contentToString(): String = _contentValue 42 | 43 | override fun appendMiraiCodeTo(builder: StringBuilder) { 44 | builder.append("[mirai:location:").append(lat).append(",") 45 | .append(lon).append(",") 46 | .append(title).append(",") 47 | .append(content).append("]") 48 | } 49 | 50 | override val key: MessageKey get() = Key 51 | public companion object Key : 52 | AbstractPolymorphicMessageKey(MessageContent, { it.safeCast() }) { 53 | public const val SERIAL_NAME: String = "Location" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/message/data/Markdown.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") 2 | package top.mrxiaom.overflow.message.data 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | import net.mamoe.mirai.message.code.CodableMessage 7 | import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode 8 | import net.mamoe.mirai.message.data.* 9 | import net.mamoe.mirai.utils.safeCast 10 | 11 | /** 12 | * Markdown消息. 用于官方 Bot 13 | * 14 | * 使用时直接构造即可. 15 | */ 16 | @Serializable 17 | @SerialName(Markdown.SERIAL_NAME) 18 | public data class Markdown( 19 | /** 20 | * Markdown 内容 21 | */ 22 | public val content: String 23 | ) : MessageContent, CodableMessage { 24 | public override fun toString(): String = "[overflow:markdown,$content]" 25 | public override fun contentToString(): String = content 26 | 27 | override fun appendMiraiCodeTo(builder: StringBuilder) { 28 | builder.append("[mirai:markdown:").appendStringAsMiraiCode(content).append("]") 29 | } 30 | 31 | public companion object Key : 32 | AbstractPolymorphicMessageKey(MessageContent, { it.safeCast() }) { 33 | public const val SERIAL_NAME: String = "Markdown" 34 | } 35 | } 36 | /** 37 | * 构造 [Markdown] 38 | */ 39 | @JvmSynthetic 40 | @Suppress("NOTHING_TO_INLINE") 41 | public inline fun String.toMarkdown(): Markdown = Markdown(this) 42 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/spi/ExtendedMessageSerializerService.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 2 | package top.mrxiaom.overflow.spi 3 | 4 | import kotlinx.serialization.json.JsonObject 5 | import net.mamoe.mirai.message.data.Message 6 | import net.mamoe.mirai.spi.BaseService 7 | import net.mamoe.mirai.spi.SpiServiceLoader 8 | import top.mrxiaom.overflow.contact.RemoteBot 9 | 10 | /** 11 | * 额外的消息序列化器,用于处理不同 Onebot 平台的特殊情况 12 | */ 13 | interface ExtendedMessageSerializerService : BaseService { 14 | 15 | /** 16 | * 反序列化 Onebot 消息段为 Mirai 消息 17 | * CQ 码会先转为消息段再进行处理,无需担心CQ码兼容问题 18 | * 19 | * @param bot 机器人实例 20 | * @param type 消息类型 21 | * @param data 消息数据 22 | * @return Mirai 消息,返回 null 代表不进行额外处理,交给其它序列化器或 Overflow 自带序列化器处理 23 | */ 24 | suspend fun deserialize(bot: RemoteBot, type: String, data: JsonObject): Message? 25 | 26 | /** 27 | * 序列化 Mirai 消息为 Onebot 消息段 28 | * 29 | * @param bot 机器人实例 30 | * @param message Mirai 消息 31 | * @return Onebot 消息段的类型与数据,返回 null 代表不进行额外处理,交给其它序列化器或 Overflow 自带序列化器处理 32 | */ 33 | fun serialize(bot: RemoteBot?, message: Message): Pair? 34 | 35 | companion object { 36 | private val loader = SpiServiceLoader(ExtendedMessageSerializerService::class) 37 | 38 | internal val instances: List 39 | get() = loader.allServices.sortedBy { it.priority } 40 | 41 | suspend fun List.deserialize( 42 | bot: RemoteBot, 43 | type: String, 44 | data: JsonObject 45 | ): Message? { 46 | for (ext in this) { 47 | val message = ext.deserialize(bot, type, data) ?: continue 48 | return message 49 | } 50 | return null 51 | } 52 | 53 | fun List.serialize( 54 | bot: RemoteBot?, 55 | message: Message 56 | ): Pair? { 57 | for (ext in this) { 58 | val pair = ext.serialize(bot, message) ?: continue 59 | return pair 60 | } 61 | return null 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/spi/FileService.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 2 | package top.mrxiaom.overflow.spi 3 | 4 | import net.mamoe.mirai.spi.BaseService 5 | import net.mamoe.mirai.spi.SpiServiceLoader 6 | import net.mamoe.mirai.utils.ExternalResource 7 | 8 | /** 9 | * 文件服务,用于图片、语音、端视频文件上传 10 | */ 11 | interface FileService : BaseService { 12 | /** 13 | * 上传文件并返回链接 14 | * 15 | * @return Onebot 格式链接,以 `file:///`、`http(s)://` 或 `base64://` 开头 16 | */ 17 | suspend fun upload(res: ExternalResource): String 18 | 19 | companion object { 20 | private val loader = SpiServiceLoader(FileService::class) 21 | 22 | internal val instance: FileService? 23 | get() = loader.service 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /overflow-core-api/src/main/kotlin/top/mrxiaom/overflow/spi/MediaURLService.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 2 | package top.mrxiaom.overflow.spi 3 | 4 | import net.mamoe.mirai.message.data.Audio 5 | import net.mamoe.mirai.message.data.Image 6 | import net.mamoe.mirai.message.data.ShortVideo 7 | import net.mamoe.mirai.spi.BaseService 8 | import net.mamoe.mirai.spi.SpiServiceLoader 9 | import top.mrxiaom.overflow.contact.RemoteBot 10 | 11 | /** 12 | * 媒体消息下载链接获取服务 13 | */ 14 | interface MediaURLService : BaseService { 15 | 16 | /** 17 | * 获取图片链接 18 | * 19 | * @return 图片链接,返回 null 代表不进行处理,交给其它服务或 Overflow 自带服务处理 20 | */ 21 | suspend fun queryImageUrl(bot: RemoteBot, image: Image): String? 22 | 23 | /** 24 | * 获取语音链接 25 | * 26 | * @return 语音链接,返回 null 代表不进行处理,交给其它服务或 Overflow 自带服务处理 27 | */ 28 | fun queryAudioUrl(audio: Audio): String? 29 | 30 | /** 31 | * 获取视频链接 32 | * 33 | * @return 视频链接,返回 null 代表不进行处理,交给其它服务或 Overflow 自带服务处理 34 | */ 35 | fun queryVideoUrl(video: ShortVideo): String? 36 | 37 | companion object { 38 | private val loader = SpiServiceLoader(MediaURLService::class) 39 | 40 | internal val instances: List 41 | get() = loader.allServices.sortedBy { it.priority } 42 | 43 | suspend fun List.queryImageUrl(bot: RemoteBot, image: Image): String? { 44 | for (ext in this) { 45 | val url = ext.queryImageUrl(bot, image) ?: continue 46 | return url 47 | } 48 | return null 49 | } 50 | fun List.queryAudioUrl(audio: Audio): String? { 51 | for (ext in this) { 52 | val url = ext.queryAudioUrl(audio) ?: continue 53 | return url 54 | } 55 | return null 56 | } 57 | fun List.queryVideoUrl(video: ShortVideo): String? { 58 | for (ext in this) { 59 | val url = ext.queryVideoUrl(video) ?: continue 60 | return url 61 | } 62 | return null 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /overflow-core/README.md: -------------------------------------------------------------------------------- 1 | # Overflow-Core 2 | 3 | 本模块为 mirai-core-api 的实现,用于对接 mirai 的接口。 4 | 5 | # Onebot 6 | 7 | 本模块同时包含 [onebot-sdk](https://github.com/cnlimiter/onebot-sdk) 和 [onebot-client](https://github.com/cnlimiter/onebot-client) 的 fork, 8 | 适配 kotlin 协程特性,增加来自 go-cqhttp、LLOnebot、NapCatQQ 等实现的额外接口调用。 9 | -------------------------------------------------------------------------------- /overflow-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | kotlin("plugin.serialization") 4 | id("org.jetbrains.dokka") 5 | id("me.him188.kotlin-jvm-blocking-bridge") 6 | id("org.ajoberstar.grgit") 7 | } 8 | 9 | kotlin { 10 | optInForAllSourceSets("net.mamoe.mirai.utils.MiraiExperimentalApi") 11 | optInForAllSourceSets("net.mamoe.mirai.utils.MiraiInternalApi") 12 | optInForAllSourceSets("net.mamoe.mirai.LowLevelApi") 13 | optInForAllSourceSets("net.mamoe.mirai.console.ConsoleFrontEndImplementation") 14 | optInForAllSourceSets("net.mamoe.mirai.console.util.ConsoleExperimentalApi") 15 | } 16 | 17 | tasks.register("dokkaJavadocJar") { 18 | group = "documentation" 19 | dependsOn(tasks.dokkaJavadoc) 20 | from(tasks.dokkaJavadoc.flatMap { it.outputDirectory }) 21 | archiveClassifier.set("javadoc") 22 | } 23 | 24 | setupMavenCentralPublication { 25 | artifact(tasks.kotlinSourcesJar) 26 | artifact(tasks.getByName("dokkaJavadocJar")) 27 | } 28 | 29 | val miraiVersion = extra("miraiVersion") ?: "2.16.0" 30 | 31 | dependencies { 32 | compileOnly("net.mamoe:mirai-console:$miraiVersion") 33 | api("net.mamoe:mirai-core-api:$miraiVersion") 34 | api("net.mamoe:mirai-core-utils:$miraiVersion") 35 | api("com.google.code.gson:gson:2.10.1") 36 | api("org.slf4j:slf4j-api:2.0.5") 37 | api("org.java-websocket:Java-WebSocket:1.5.7") 38 | api("com.google.code.gson:gson:2.10.1") 39 | api("me.him188:kotlin-jvm-blocking-bridge-runtime:3.0.0-180.1") 40 | 41 | fun netty(s: String): Dependency? = api("io.netty:netty-$s:4.1.90.Final") 42 | netty("codec-http") 43 | netty("codec-socks") 44 | netty("transport") 45 | 46 | api(project(":overflow-core-api")) 47 | 48 | testImplementation("com.google.code.gson:gson:2.10.1") 49 | testImplementation("org.java-websocket:Java-WebSocket:1.5.7") 50 | testImplementation("net.mamoe:mirai-console:$miraiVersion") 51 | testImplementation("net.mamoe:mirai-console-terminal:$miraiVersion") 52 | testImplementation("ch.qos.logback:logback-classic:1.4.14") 53 | testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") 54 | testImplementation(kotlin("test")) 55 | } 56 | 57 | tasks.test { 58 | useJUnitPlatform() 59 | } 60 | 61 | tasks { 62 | register("runConsole") { 63 | mainClass.set("RunConsoleKt") 64 | workingDir = File(project.projectDir, "run") 65 | classpath = sourceSets.test.get().runtimeClasspath.filter { 66 | val path = it.absolutePath.replace("\\", "/") 67 | !path.contains("ch.qos.logback/logback-") 68 | } 69 | standardInput = System.`in` 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /overflow-core/runConsole.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/config/BotConfig.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.client.config 2 | 3 | import kotlinx.coroutines.Job 4 | 5 | /** 6 | * Description: 7 | * Author: cnlimiter 8 | * Date: 2022/10/1 17:05 9 | * Version: 1.0 10 | */ 11 | class BotConfig( 12 | /** 13 | * websocket地址 14 | */ 15 | val url: String = "ws://127.0.0.1:8080", 16 | /** 17 | * 反向 websocket 端口 18 | */ 19 | val reversedPort: Int = -1, 20 | /** 21 | * token或者verifyKey鉴权 22 | */ 23 | val token: String = "", 24 | /** 25 | * 是否开启鉴权 26 | */ 27 | val isAccessToken: Boolean = false, 28 | /** 29 | * 是否禁止 Overflow 通过 get_credentials 获取凭证调用网络接口 30 | * 31 | * 该选项无法禁止其它插件自行调用网络接口 32 | */ 33 | val noPlatform: Boolean = false, 34 | /** 35 | * 发送消息时,是否使用 CQ 码 36 | */ 37 | val useCQCode: Boolean = false, 38 | /** 39 | * 重连尝试次数 40 | */ 41 | val retryTimes: Int = 5, 42 | /** 43 | * 重连间隔 (毫秒) 44 | */ 45 | val retryWaitMills: Long = 5_000L, 46 | /** 47 | * 重连休息时间 (毫秒) 48 | */ 49 | val retryRestMills: Long = 60_000L, 50 | /** 51 | * 心跳包检测时间 (秒),设为0关闭检测 52 | */ 53 | val heartbeatCheckSeconds: Int = 60, 54 | /** 55 | * 是否不接收群聊的 file 消息,使用 group_upload 事件作为群文件消息 56 | */ 57 | val useGroupUploadEventForFileMessage: Boolean = false, 58 | /** 59 | * 是否抛弃所有在Bot成功连接之前传入的事件。 60 | * 61 | * 比如 go-cqhttp 会在连接之后,在 Overflow 获取到 Bot 信息之前,推送以前未收到的消息。如果开启此项,则丢弃这些消息 62 | */ 63 | val dropEventsBeforeConnected: Boolean = true, 64 | val parentJob: Job? = null 65 | ) { 66 | val isInReverseMode get() = reversedPort in 1..65535 67 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/connection/IAdapter.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.client.connection 2 | 3 | import cn.evolvefield.onebot.client.handler.ActionHandler 4 | import cn.evolvefield.onebot.client.handler.EventBus 5 | import cn.evolvefield.onebot.client.handler.EventHolder 6 | import cn.evolvefield.onebot.client.util.ActionSendRequest 7 | import cn.evolvefield.onebot.client.util.OnebotException 8 | import cn.evolvefield.onebot.sdk.util.ignorable 9 | import com.google.gson.JsonParser 10 | import com.google.gson.JsonSyntaxException 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.sync.Mutex 14 | import kotlinx.coroutines.sync.withLock 15 | import kotlinx.coroutines.withTimeoutOrNull 16 | import net.mamoe.mirai.Bot 17 | import org.slf4j.Logger 18 | 19 | internal interface IAdapter { 20 | val scope: CoroutineScope 21 | val actionHandler: ActionHandler 22 | val logger: Logger 23 | val eventsHolder: MutableMap> 24 | 25 | fun onReceiveMessage(message: String) { 26 | try { 27 | val json = JsonParser.parseString(message).asJsonObject 28 | if (json.ignorable(META_EVENT, "") != HEART_BEAT) { // 过滤心跳 29 | logger.debug("[Recv] <-- {}", json.toString()) 30 | 31 | if (json.has(API_RESULT_KEY)) { // 接口回调 32 | actionHandler.onReceiveActionResp(json) 33 | } else { 34 | val holder = EventBus.matchHandlers(message) 35 | val botId = holder.event.selfId 36 | if (Bot.findInstance(botId) == null) { 37 | val list = eventsHolder[botId] ?: run { 38 | mutableListOf().also { 39 | eventsHolder[botId] = it 40 | } 41 | } 42 | list.add(holder) 43 | return 44 | } else scope.launch { // 处理事件 45 | handleEvent(holder) 46 | } 47 | } 48 | } 49 | } catch (e: JsonSyntaxException) { 50 | logger.error("Json语法错误: {}", message) 51 | } catch (e: OnebotException) { 52 | logger.error("解析异常: {}", e.info) 53 | } 54 | } 55 | 56 | fun afterLoginInfoFetch(bot: cn.evolvefield.onebot.client.core.Bot) { 57 | val events = eventsHolder.remove(bot.id) ?: return 58 | if (bot.config.dropEventsBeforeConnected) return 59 | for (holder in events) scope.launch { 60 | handleEvent(holder) 61 | } 62 | } 63 | 64 | private suspend fun handleEvent(holder: EventHolder) { // 处理事件 65 | mutex.withLock { 66 | withTimeoutOrNull(processTimeout) { 67 | runCatching { 68 | holder.onReceive() 69 | }.onFailure { 70 | logger.error("处理 Onebot 事件时出现异常: ", it) 71 | } 72 | } ?: throw IllegalStateException("事件处理超时: ${holder.event.json}") 73 | } 74 | } 75 | 76 | fun unlockMutex() { 77 | runCatching { 78 | if (mutex.isLocked) mutex.unlock() 79 | if (ActionSendRequest.mutex.isLocked) ActionSendRequest.mutex.unlock() 80 | } 81 | } 82 | 83 | companion object { 84 | private const val META_EVENT = "meta_event_type" 85 | private const val API_RESULT_KEY = "echo" 86 | private const val HEART_BEAT = "heartbeat" 87 | 88 | val processTimeout by lazy { 89 | System.getProperty("overflow.timeout-process")?.toLongOrNull() ?: 20000L 90 | } 91 | val mutex = Mutex() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/connection/platform-connection.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.client.connection 2 | 3 | import cn.evolvefield.onebot.client.core.Bot 4 | import kotlinx.coroutines.withTimeout 5 | import kotlin.time.Duration 6 | 7 | internal interface OneBotProducer { 8 | fun invokeOnClose(block: () -> Unit) 9 | fun close() 10 | fun setBotConsumer(consumer: suspend (Bot) -> Unit) 11 | suspend fun awaitNewBotConnection(duration: Duration = Duration.INFINITE): Bot? 12 | } 13 | 14 | internal class PositiveOneBotProducer( 15 | private val client: WSClient 16 | ): OneBotProducer { 17 | override fun invokeOnClose(block: () -> Unit) = TODO("客户端暂不支持断线重连") 18 | 19 | override fun close() = client.close() 20 | 21 | override fun setBotConsumer(consumer: suspend (Bot) -> Unit) { 22 | client.botConsumer = consumer 23 | } 24 | 25 | override suspend fun awaitNewBotConnection(duration: Duration): Bot? { 26 | return kotlin.runCatching { 27 | withTimeout(duration) { 28 | if (client.connectSuspend()) client.createBot() else null 29 | } 30 | }.getOrNull() 31 | } 32 | } 33 | 34 | internal class ReversedOneBotProducer( 35 | private val server: WSServer 36 | ): OneBotProducer { 37 | override fun invokeOnClose(block: () -> Unit) { 38 | server.closeHandler.add(block) 39 | } 40 | 41 | override fun close() = server.stop() 42 | 43 | override fun setBotConsumer(consumer: suspend (Bot) -> Unit) { 44 | server.botConsumer = consumer 45 | } 46 | 47 | override suspend fun awaitNewBotConnection(duration: Duration): Bot? { 48 | return kotlin.runCatching { 49 | withTimeout(duration) { 50 | server.awaitNewBot() 51 | } 52 | }.getOrNull() 53 | } 54 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/handler/EventBus.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.client.handler 2 | 3 | import cn.evolvefield.onebot.client.util.ListenerUtils 4 | import cn.evolvefield.onebot.sdk.event.Event 5 | import cn.evolvefield.onebot.sdk.event.UnsolvedEvent 6 | import cn.evolvefield.onebot.sdk.util.JsonHelper.applyJson 7 | import cn.evolvefield.onebot.sdk.util.JsonHelper.gson 8 | import cn.evolvefield.onebot.sdk.util.ignorable 9 | import com.google.gson.JsonParser 10 | import net.mamoe.mirai.utils.currentTimeSeconds 11 | import org.slf4j.LoggerFactory 12 | import top.mrxiaom.overflow.internal.contact.BotWrapper 13 | import kotlin.reflect.KClass 14 | import kotlin.reflect.KProperty1 15 | import kotlin.reflect.full.declaredMemberProperties 16 | 17 | object EventBus { 18 | private val logger = LoggerFactory.getLogger(EventBus::class.java) 19 | private val handlers: MutableMap, MutableList>> = hashMapOf() 20 | 21 | internal fun addListener(handler: Handler) { 22 | val list = handlers.getOrPut(handler.type) { mutableListOf() } 23 | list.add(handler) 24 | } 25 | internal inline fun listen(noinline block: suspend BotWrapper.(T) -> Unit) { 26 | addListener(BotHandler(T::class, null, block)) 27 | } 28 | internal inline fun listenNormally(noinline block: suspend (T) -> Unit) { 29 | addListener(NormalHandler(T::class, null, block)) 30 | } 31 | internal inline fun listen(subType: String, noinline block: suspend BotWrapper.(T) -> Unit) { 32 | val field = T::class.property("subType") ?: return 33 | addListener(BotHandler(T::class, field.check(subType), block)) 34 | } 35 | internal inline fun listenNormally(subType: String, noinline block: suspend (T) -> Unit) { 36 | val field = T::class.property("subType") ?: return 37 | addListener(NormalHandler(T::class, field.check(subType), block)) 38 | } 39 | private fun KClass.property(name: String): KProperty1? { 40 | val field = declaredMemberProperties 41 | .firstOrNull { it.name == name } 42 | if (field == null) { 43 | logger.warn("无法注册 ${java.name}: 找不到 $name") 44 | return null 45 | } 46 | return field 47 | } 48 | private fun KProperty1.check(subType: String): (T) -> Boolean { 49 | return { get(it)?.toString() == subType } 50 | } 51 | 52 | fun clear() { 53 | handlers.clear() 54 | } 55 | 56 | internal fun matchHandlers( 57 | message: String 58 | ): EventHolder { 59 | val messageType = ListenerUtils[message] // 获取消息对应的实体类型 60 | val json = JsonParser.parseString(message).asJsonObject 61 | if (messageType != null) { 62 | val bean = gson.fromJson(message, messageType.java) // 将消息反序列化为对象 63 | bean.applyJson(json) 64 | logger.debug(String.format("接收到上报消息内容:%s", bean.toString())) 65 | val executes = handlers[messageType] ?: emptyList() 66 | if (executes.isNotEmpty()) { 67 | return EventHolder(bean, executes) 68 | } 69 | } 70 | // 如果该事件未被监听,或者未定义事件类,将其定为 UnsolvedEvent 71 | val bean = UnsolvedEvent().also { it.jsonString = message } 72 | val executes = handlers[UnsolvedEvent::class] ?: emptyList() 73 | bean.postType = json.ignorable("post_type", "") 74 | bean.time = json.ignorable("time", currentTimeSeconds()) 75 | bean.selfId = json.ignorable("self_id", 0L) 76 | bean.applyJson(json) 77 | return EventHolder(bean, executes) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/handler/EventHandler.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.client.handler 2 | 3 | import cn.evolvefield.onebot.sdk.event.Event 4 | import top.mrxiaom.overflow.internal.contact.BotWrapper 5 | import top.mrxiaom.overflow.internal.utils.bot 6 | import kotlin.reflect.KClass 7 | import kotlin.reflect.safeCast 8 | 9 | internal abstract class Handler( 10 | val type: KClass, 11 | ) { 12 | suspend fun onReceive(event: Event) { 13 | val e = type.safeCast(event) 14 | if (e != null) { 15 | receive(e) 16 | } 17 | } 18 | 19 | abstract suspend fun receive(e: T) 20 | } 21 | internal class BotHandler( 22 | type: KClass, 23 | private val condition: ((T) -> Boolean)?, 24 | private val block: suspend BotWrapper.(T) -> Unit 25 | ): Handler(type) { 26 | override suspend fun receive(e: T) { 27 | val bot = e.bot ?: return 28 | if (condition?.invoke(e) != false) { 29 | block.invoke(bot, e) 30 | } 31 | } 32 | } 33 | internal class NormalHandler( 34 | type: KClass, 35 | private val condition: ((T) -> Boolean)?, 36 | private val block: suspend (T) -> Unit 37 | ): Handler(type) { 38 | override suspend fun receive(e: T) { 39 | if (condition?.invoke(e) != false) { 40 | block.invoke(e) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/handler/EventHolder.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.client.handler 2 | 3 | import cn.evolvefield.onebot.sdk.event.Event 4 | 5 | internal class EventHolder( 6 | val event: Event, 7 | val handlers: List>, 8 | ) { 9 | suspend fun onReceive() { 10 | for (handler in handlers) { 11 | handler.onReceive(event) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/util/ActionSendRequest.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.client.util 2 | 3 | import cn.evolvefield.onebot.client.core.Bot 4 | import cn.evolvefield.onebot.sdk.util.ignorable 5 | import cn.evolvefield.onebot.sdk.util.ignorableArray 6 | import cn.evolvefield.onebot.sdk.util.ignorableObject 7 | import com.google.gson.JsonArray 8 | import com.google.gson.JsonObject 9 | import kotlinx.coroutines.CompletableDeferred 10 | import kotlinx.coroutines.Job 11 | import kotlinx.coroutines.TimeoutCancellationException 12 | import kotlinx.coroutines.sync.Mutex 13 | import kotlinx.coroutines.sync.withLock 14 | import kotlinx.coroutines.withTimeout 15 | import org.java_websocket.WebSocket 16 | import org.slf4j.Logger 17 | import top.mrxiaom.overflow.action.ActionContext 18 | import java.util.* 19 | 20 | /** 21 | * Description: 22 | * Author: cnlimiter 23 | * Date: 2022/9/14 15:06 24 | * Version: 1.0 25 | */ 26 | /** 27 | * @param channel [WebSocket] 28 | * @param requestTimeout Request Timeout 29 | */ 30 | internal class ActionSendRequest( 31 | private val bot: Bot, 32 | private val context: ActionContext, 33 | parent: Job?, 34 | private val logger: Logger, 35 | private val channel: WebSocket, 36 | private val requestTimeout: Long 37 | ) { 38 | private val resp = CompletableDeferred(parent) 39 | /** 40 | * @param req Request json data 41 | * @return Response json data 42 | */ 43 | @Throws(TimeoutCancellationException::class, ActionFailedException::class) 44 | suspend fun send(req: JsonObject): JsonObject { 45 | val resp = mutex.withLock { 46 | kotlin.runCatching { 47 | withTimeout(requestTimeout) { 48 | logger.debug("[Send] --> {}", req.toString()) 49 | channel.send(req.toString()) 50 | resp.await() 51 | } 52 | }.onFailure { resp.cancel() }.getOrThrow() 53 | } 54 | if (resp.ignorable("status", if (context.ignoreStatus) "" else "failed") == "failed") { 55 | val extra = runCatching { 56 | val params = req.ignorableObject("params") { JsonObject() } 57 | val messages = params.ignorableArray("message") { JsonArray() } 58 | val extraFileTypes = messages.filter { 59 | listOf("image", "record", "video").contains(it.asJsonObject?.get("type")?.asString) 60 | && it.asJsonObject?.has("file") == true 61 | }.mapNotNull { 62 | val file = it.asJsonObject!!["file"].asString 63 | if (file.startsWith("base64://")) { 64 | val bytes = Base64.getDecoder().decode(file.replace("base64://", "")) 65 | "msgFileType=${bytes.fileType}" 66 | } else null 67 | } 68 | return@runCatching if (extraFileTypes.isNotEmpty()) { 69 | "; " + extraFileTypes.joinToString(", ") 70 | } else "" 71 | }.getOrElse { "" } 72 | throw ActionFailedException( 73 | action = context.action, 74 | app = "${bot.appName} v${bot.appVersion}", 75 | msg = "${resp.ignorable("message", "")}$extra", 76 | json = resp 77 | ) 78 | } 79 | return resp 80 | } 81 | 82 | /** 83 | * @param resp Response json data 84 | */ 85 | fun onCallback(resp: JsonObject) { 86 | this.resp.complete(resp) 87 | } 88 | 89 | companion object { 90 | val mutex = Mutex() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/util/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.client.util 2 | 3 | import com.google.gson.JsonObject 4 | 5 | class ActionFailedException( 6 | val action: String, 7 | val app: String, 8 | val msg: String, 9 | val json: JsonObject 10 | ): OnebotException("[$action] 请求失败: app=$app, message=$msg,\n retJson=$json") 11 | 12 | open class OnebotException( 13 | val info: String 14 | ): IllegalStateException(info) 15 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/client/util/Files.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.ink/mamoe/mirai/blob/dev/LICENSE 8 | */ 9 | 10 | @file:JvmMultifileClass 11 | @file:JvmName("MiraiUtils") 12 | 13 | package cn.evolvefield.onebot.client.util 14 | 15 | private class FileType( 16 | signature: String, 17 | val requiredHeaderSize: Int, 18 | val formatName: String 19 | ) { 20 | val signatureRegex = Regex(signature, RegexOption.IGNORE_CASE) 21 | } 22 | 23 | /** 24 | * 文件头和文件类型列表 25 | */ 26 | private val FILE_TYPES: List = listOf( 27 | FileType("^FFD8FF", 3, "jpg"), 28 | FileType("^89504E47", 4, "png"), 29 | FileType("^47494638", 4, "gif"), 30 | FileType("^424D", 3, "bmp"), 31 | FileType("^2321414D52", 5, "amr"), 32 | FileType("^02232153494C4B5F5633", 10, "silk"), 33 | FileType("^([a-zA-Z0-9]{8})66747970", 8, "mp4"), 34 | 35 | FileType("49492A00", 8, "tif"), // client doesn't support 36 | FileType("52494646", 8, "webp"), // pc client doesn't support 37 | FileType("57415645", 8, "wav"), // server doesn't support 38 | ) 39 | 40 | /** 41 | * 在 [getFileType] 需要的 [ByteArray] 长度 42 | */ 43 | val COUNT_BYTES_USED_FOR_DETECTING_FILE_TYPE: Int by lazy { FILE_TYPES.maxOf { it.requiredHeaderSize } } 44 | 45 | /* 46 | 47 | startsWith("FFD8") -> "jpg" 48 | startsWith("89504E47") -> "png" 49 | startsWith("47494638") -> "gif" 50 | startsWith("424D") -> "bmp" 51 | */ 52 | 53 | val ByteArray.fileType: String? 54 | get() = getFileTypeByHeader(copyOf(COUNT_BYTES_USED_FOR_DETECTING_FILE_TYPE)) 55 | 56 | /** 57 | * 根据文件头获取文件类型 58 | */ 59 | fun getFileTypeByHeader(fileHeader: ByteArray): String? { 60 | val hex = fileHeader.toUHexString( 61 | "", 62 | length = COUNT_BYTES_USED_FOR_DETECTING_FILE_TYPE.coerceAtMost(fileHeader.size) 63 | ) 64 | FILE_TYPES.forEach { t -> 65 | if (hex.contains(t.signatureRegex)) { 66 | return t.formatName 67 | } 68 | } 69 | return null 70 | } 71 | 72 | fun ByteArray.toUHexString( 73 | separator: String = " ", 74 | offset: Int = 0, 75 | length: Int = this.size - offset 76 | ): String { 77 | this.checkOffsetAndLength(offset, length) 78 | if (length == 0) { 79 | return "" 80 | } 81 | val lastIndex = offset + length 82 | return buildString(length * 2) { 83 | this@toUHexString.forEachIndexed { index, it -> 84 | if (index in offset until lastIndex) { 85 | val ret = it.toUByte().toString(16).uppercase() 86 | if (ret.length == 1) append('0') 87 | append(ret) 88 | if (index < lastIndex - 1) append(separator) 89 | } 90 | } 91 | } 92 | } 93 | 94 | fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) { 95 | require(offset >= 0) { "offset shouldn't be negative: $offset" } 96 | require(length >= 0) { "length shouldn't be negative: $length" } 97 | require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" } 98 | } 99 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/action/ActionPath.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.action 2 | 3 | interface ActionPath { 4 | val path: String 5 | } 6 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/action/ActionResults.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.action 2 | 3 | import com.google.gson.JsonElement 4 | import com.google.gson.annotations.SerializedName 5 | import java.util.* 6 | 7 | open class JsonContainer { 8 | internal lateinit var element: JsonElement 9 | val json: JsonElement 10 | get() = element 11 | } 12 | 13 | data class ActionRaw( 14 | @SerializedName("status") 15 | val status: String, 16 | @SerializedName("retCode") 17 | val retCode: Int, 18 | @SerializedName("echo") 19 | val echo: String? = null, 20 | ): JsonContainer() 21 | 22 | data class ActionData ( 23 | @SerializedName("status") 24 | val status: String, 25 | @SerializedName("retcode") 26 | val retCode: Int, 27 | @SerializedName("data") 28 | val data: T?, 29 | @SerializedName("echo") 30 | val echo: String? = null 31 | ): JsonContainer() 32 | 33 | data class ActionList( 34 | @SerializedName("status") 35 | val status: String, 36 | @SerializedName("retcode") 37 | val retCode: Int, 38 | @SerializedName("data") 39 | val data: LinkedList, 40 | @SerializedName("echo") 41 | val echo: String? = null 42 | ): JsonContainer() 43 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/entity/Anonymous.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class Anonymous { 6 | @SerializedName("id") 7 | var id = 0L 8 | @SerializedName("name") 9 | var name: String = "" 10 | @SerializedName("flag") 11 | var flag: String = "" 12 | } 13 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/entity/File.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * 文件实体 7 | */ 8 | class File { 9 | @SerializedName("id") 10 | var id = "" 11 | @SerializedName("name") 12 | var name = "" 13 | @SerializedName("size") 14 | var size = 0L 15 | @SerializedName("busid") 16 | var busId = 0L 17 | @SerializedName("url") 18 | var url = "" 19 | } 20 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/entity/GroupSender.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GroupSender { 6 | @SerializedName("user_id") 7 | var userId = "" 8 | @SerializedName("nickname") 9 | var nickname = "" 10 | @SerializedName("card") 11 | var card = "" 12 | @SerializedName("role") 13 | var role = "member" 14 | } 15 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/entity/MsgId.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class MsgId { 6 | @SerializedName("message_id") 7 | var messageId: Int? = null 8 | } 9 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/entity/PrivateSender.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.entity 2 | 3 | import cn.evolvefield.onebot.sdk.util.json.MessageEventAdapter 4 | import com.google.gson.annotations.JsonAdapter 5 | 6 | @JsonAdapter(MessageEventAdapter.PrivateSenderAdapter::class) 7 | class PrivateSender { 8 | var userId = 0L 9 | var groupId = 0L // go-cqhttp 10 | var nickname = "" 11 | var sex = "" 12 | var age = 0 13 | } 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/entity/Sender.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.entity 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class Sender { 6 | @SerializedName("user_id") 7 | var userId = "" 8 | @SerializedName("nickname") 9 | var nickname = "" 10 | @SerializedName("card") 11 | var card = "" 12 | @SerializedName("sex") 13 | var sex = "" 14 | @SerializedName("age") 15 | var age = 0 16 | @SerializedName("area") 17 | var area = "" 18 | @SerializedName("level") 19 | var level = "" 20 | @SerializedName("role") 21 | var role = "" 22 | @SerializedName("title") 23 | var title = "" 24 | } 25 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/Event.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event 2 | 3 | import cn.evolvefield.onebot.sdk.action.JsonContainer 4 | import com.google.gson.annotations.SerializedName 5 | 6 | /** 7 | * 事件上报 8 | * 9 | * @author cnlimiter 10 | */ 11 | open class Event : JsonContainer() { 12 | @SerializedName("post_type") 13 | var postType: String = "" 14 | @SerializedName("time") 15 | var time: Long = 0 16 | @SerializedName("self_id") 17 | var selfId: Long = 0 18 | 19 | /** 20 | * 自动转换毫秒时间戳(如果是)为秒时间戳, 21 | * 仅支持2000年以后的毫秒时间戳,2000年以前的认定为秒时间戳 22 | */ 23 | fun timeInSecond(): Long { 24 | return if (time < 946656000000L) time 25 | else time / 1000L 26 | } 27 | 28 | fun time(): Int = timeInSecond().toInt() 29 | } 30 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/UnsolvedEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event 2 | 3 | /** 4 | * 未成功解析的事件 5 | * 6 | * @author MrXiaoM 7 | */ 8 | class UnsolvedEvent : Event() { 9 | var jsonString: String = "{}" 10 | } 11 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/message/GroupMessageEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.message 2 | 3 | import cn.evolvefield.onebot.sdk.entity.Anonymous 4 | import cn.evolvefield.onebot.sdk.entity.GroupSender 5 | import cn.evolvefield.onebot.sdk.util.json.MessageEventAdapter 6 | import com.google.gson.annotations.JsonAdapter 7 | 8 | @JsonAdapter(MessageEventAdapter::class) 9 | class GroupMessageEvent : MessageEvent() { 10 | var messageId: Int = 0 11 | var subType: String = "" 12 | var groupId = 0L 13 | var anonymous: Anonymous? = null 14 | var sender: GroupSender? = null 15 | } 16 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/message/MessageEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.message 2 | 3 | import cn.evolvefield.onebot.sdk.event.Event 4 | 5 | open class MessageEvent : Event() { 6 | var messageType = "" 7 | var userId = 0L 8 | var isJsonMessage = false 9 | var message = "" 10 | var rawMessage = "" 11 | var font = 0 12 | } 13 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/message/PrivateMessageEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.message 2 | 3 | import cn.evolvefield.onebot.sdk.entity.PrivateSender 4 | import cn.evolvefield.onebot.sdk.util.json.MessageEventAdapter 5 | import com.google.gson.annotations.JsonAdapter 6 | 7 | /** 8 | * 私聊消息 9 | * 10 | * @author cnlimiter 11 | */ 12 | @JsonAdapter(MessageEventAdapter::class) 13 | class PrivateMessageEvent : MessageEvent() { 14 | var messageId = 0 15 | var subType = "" 16 | lateinit var sender: PrivateSender 17 | var groupId = 0L // OpenShamrock 18 | 19 | /** 20 | * go-cqhttp: [docs](https://docs.go-cqhttp.org/reference/data_struct.html#post-message-tempsource)

21 | * Group(0), 22 | * Consultation(1), 23 | * Seek(2), 24 | * QQMovie(3), 25 | * HotChat(4), 26 | * VerifyMsg(6), 27 | * Discussion(7), 28 | * Dating(8), 29 | * Contact(9), 30 | * Unknown(-1) 31 | */ 32 | var tempSource = 0 33 | var fromNick = "" // OpenShamrock 34 | } 35 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/meta/HeartbeatMetaEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.meta 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class HeartbeatMetaEvent : MetaEvent() { 6 | init { 7 | metaEventType = "heartbeat" 8 | } 9 | @SerializedName("status") 10 | val status: Any = "" 11 | @SerializedName("interval") 12 | val interval: Long = 0 13 | } 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/meta/LifecycleMetaEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.meta 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class LifecycleMetaEvent : MetaEvent() { 6 | init { 7 | metaEventType = "lifecycle" 8 | } 9 | @SerializedName("sub_type") 10 | var subType = "" // enable、disable、connect 11 | } 12 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/meta/MetaEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.meta 2 | 3 | import cn.evolvefield.onebot.sdk.event.Event 4 | import com.google.gson.annotations.SerializedName 5 | 6 | open class MetaEvent : Event() { 7 | init { 8 | postType = "meta_event" 9 | } 10 | @SerializedName("meta_event_type") 11 | var metaEventType = "" 12 | } 13 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/NoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice 2 | 3 | import cn.evolvefield.onebot.sdk.event.Event 4 | import com.google.gson.annotations.SerializedName 5 | 6 | open class NoticeEvent : Event() { 7 | @SerializedName("notice_type") 8 | var noticeType = "" 9 | @SerializedName("user_id") 10 | var userId = 0L 11 | } 12 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/NotifyNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class NotifyNoticeEvent : NoticeEvent() { 6 | @SerializedName("sub_type") 7 | var subType = "" 8 | @SerializedName("operator_id") 9 | var operatorId = 0L 10 | @SerializedName("target_id") 11 | var targetId = 0L 12 | @SerializedName("group_id") 13 | var groupId = 0L 14 | @SerializedName("title") 15 | val title = "" 16 | val realOperatorId: Long 17 | get() = operatorId.takeIf { it > 0 } ?: userId 18 | } 19 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/friend/FriendAddNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.friend 2 | 3 | import cn.evolvefield.onebot.sdk.event.request.RequestEvent 4 | 5 | class FriendAddNoticeEvent : RequestEvent() 6 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/friend/PrivateMsgDeleteNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.friend 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class PrivateMsgDeleteNoticeEvent : NoticeEvent() { 7 | @SerializedName("operator_id") 8 | var operatorId = 0L 9 | @SerializedName("message_id") 10 | var msgId = 0L 11 | } 12 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupAdminNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupAdminNoticeEvent : NoticeEvent() { 7 | /** 8 | * set、unset 9 | * 事件子类型, 分别表示设置和取消管理 10 | */ 11 | @SerializedName( "sub_type") 12 | var subType = "" 13 | 14 | /** 15 | * 群号 16 | */ 17 | @SerializedName( "group_id") 18 | var groupId = 0L 19 | 20 | // 管理员QQ号 userId 已在 NoticeEvent 中实现 21 | } 22 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupBanNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupBanNoticeEvent : NoticeEvent() { 7 | @SerializedName("sub_type") 8 | var subType = "" 9 | @SerializedName("group_id") 10 | var groupId = 0L 11 | @SerializedName("operator_id") 12 | var operatorId = 0L 13 | @SerializedName("duration") 14 | var duration = 0L 15 | } 16 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupCardChangeNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupCardChangeNoticeEvent : NoticeEvent() { 7 | @SerializedName("card_new") 8 | var cardNew = "" 9 | @SerializedName("card_old") 10 | var cardOld = "" 11 | @SerializedName("group_id") 12 | var groupId = 0L 13 | } 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupDecreaseNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupDecreaseNoticeEvent : NoticeEvent() { 7 | @SerializedName("sub_type") 8 | var subType = "" 9 | @SerializedName("group_id") 10 | var groupId = 0L 11 | @SerializedName("operator_id") 12 | var operatorId = 0L 13 | } 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupEssenceNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupEssenceNoticeEvent : NoticeEvent() { 7 | /** 8 | * add,delete 添加为add,移出为delete 9 | */ 10 | @SerializedName("sub_type") 11 | var subType = "" 12 | 13 | /** 14 | * 消息发送者ID 15 | */ 16 | @SerializedName("sender_id") 17 | var senderId: Long = 0L 18 | 19 | /** 20 | * 操作者ID 21 | */ 22 | @SerializedName("operator_id") 23 | var operatorId: Long = 0L 24 | 25 | /** 26 | * 消息ID 27 | */ 28 | @SerializedName("message_id") 29 | var messageId: Int = 0 30 | 31 | /** 32 | * 群号 33 | */ 34 | @SerializedName( "group_id") 35 | var groupId = 0L 36 | } 37 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupHonorChangeNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupHonorChangeNoticeEvent : NoticeEvent() { 7 | @SerializedName("sub_type") 8 | var subType = "" 9 | @SerializedName("group_id") 10 | var groupId = 0L 11 | @SerializedName("honor_type") 12 | var honorType = "" 13 | } 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupIncreaseNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupIncreaseNoticeEvent : NoticeEvent() { 7 | @SerializedName("sub_type") 8 | var subType = "" 9 | @SerializedName("group_id") 10 | var groupId = 0L 11 | @SerializedName("operator_id") 12 | var operatorId = 0L 13 | } 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupLuckyKingNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupLuckyKingNoticeEvent : NoticeEvent() { 7 | @SerializedName("sub_type") 8 | var subType = "" 9 | @SerializedName("group_id") 10 | var groupId = 0L 11 | @SerializedName("target_id") 12 | var targetId = 0L 13 | } 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupMsgDeleteNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupMsgDeleteNoticeEvent : NoticeEvent() { 7 | @SerializedName("group_id") 8 | var groupId = 0L 9 | @SerializedName("operator_id") 10 | var operatorId = 0L 11 | @SerializedName("message_id") 12 | var msgId = 0L 13 | } 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupNameChangeNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupNameChangeNoticeEvent : NoticeEvent() { 7 | @SerializedName("name") 8 | var name = "" 9 | @SerializedName("group_id") 10 | var groupId = 0L 11 | @SerializedName("operator_id") 12 | var operatorId = 0L 13 | } 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/group/GroupUploadNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.group 2 | 3 | import cn.evolvefield.onebot.sdk.entity.File 4 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class GroupUploadNoticeEvent : NoticeEvent() { 8 | @SerializedName("group_id") 9 | var groupId = 0L 10 | @SerializedName("file") 11 | var file: File? = null 12 | } 13 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/misc/GroupMsgEmojiLikeNotice.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.misc 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | import java.util.* 6 | 7 | class GroupMsgEmojiLikeNotice : NoticeEvent() { 8 | @SerializedName("group_id") 9 | var groupId = 0L 10 | @SerializedName("message_id") 11 | var messageId = 0 12 | @SerializedName("likes") 13 | var likes = LinkedList() 14 | 15 | class Like { 16 | @SerializedName("emoji_id") 17 | var emojiId = "" 18 | @SerializedName("count") 19 | var count = 0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/misc/GroupReactionNotice.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.misc 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class GroupReactionNotice : NoticeEvent() { 7 | @SerializedName("sub_type") // add, remove 8 | var subType = "" 9 | @SerializedName("group_id") 10 | var groupId = 0L 11 | @SerializedName("message_id") 12 | var messageId = 0 13 | @SerializedName("operator_id") 14 | var operatorId = 0L 15 | @SerializedName("code", alternate = ["icon", "icon_id"]) 16 | var code = "" 17 | @SerializedName("count") 18 | var count = 0 19 | } 20 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/notice/misc/ReceiveOfflineFilesNoticeEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.notice.misc 2 | 3 | import cn.evolvefield.onebot.sdk.event.notice.NoticeEvent 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class ReceiveOfflineFilesNoticeEvent : NoticeEvent() { 7 | @SerializedName("file") 8 | var file: File? = null 9 | 10 | /** 11 | * 文件对象 12 | */ 13 | class File { 14 | @SerializedName("name") 15 | var name = "" 16 | @SerializedName("size") 17 | var size = 0L 18 | @SerializedName("url") 19 | var url = "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/request/FriendAddRequestEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.request 2 | 3 | class FriendAddRequestEvent : RequestEvent() 4 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/request/GroupAddRequestEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.request 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GroupAddRequestEvent : RequestEvent() { 6 | @SerializedName("sub_type") 7 | var subType = "" 8 | @SerializedName("group_id") 9 | var groupId = 0L 10 | @SerializedName("invitor_id") 11 | var invitorId = 0L 12 | } 13 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/event/request/RequestEvent.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.event.request 2 | 3 | import cn.evolvefield.onebot.sdk.event.Event 4 | import com.google.gson.annotations.SerializedName 5 | 6 | open class RequestEvent : Event() { 7 | @SerializedName("request_type") 8 | var requestType = "" 9 | @SerializedName("user_id") 10 | var userId = 0L 11 | @SerializedName(value = "comment", alternate = ["message"]) 12 | var comment = "" 13 | @SerializedName("flag") 14 | var flag = "" 15 | } 16 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/contact/FriendInfoResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.contact 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class FriendInfoResp { 6 | @SerializedName("user_id") 7 | var userId = 0L 8 | @SerializedName(value = "nickname", alternate = ["user_name"]) 9 | var nickname = "" 10 | @SerializedName(value = "remark", alternate = ["user_remark"]) 11 | var remark = "" 12 | @SerializedName("sex") 13 | var sex = "unknown" 14 | @SerializedName("age") 15 | var age = 0 16 | @SerializedName("email") 17 | var email = "" 18 | @SerializedName("level") 19 | var level = 0 20 | } 21 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/contact/LoginInfoResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.contact 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class LoginInfoResp { 6 | @SerializedName("user_id") 7 | var userId = 0L 8 | @SerializedName("nickname") 9 | var nickname = "" 10 | } 11 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/contact/StrangerInfoResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.contact 2 | 3 | import com.google.gson.JsonObject 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class StrangerInfoResp { 7 | /** 8 | * QQ 号 9 | */ 10 | @SerializedName("user_id") 11 | var userId = 0L 12 | /** 13 | * 昵称 14 | */ 15 | @SerializedName("nickname") 16 | var nickname = "" 17 | /** 18 | * 性别 male 或 female 或 unknown 19 | */ 20 | @SerializedName("sex") 21 | var sex = "" 22 | /** 23 | * 年龄 24 | */ 25 | @SerializedName("age") 26 | var age = 0 27 | /** 28 | * qid 身份卡 29 | */ 30 | @SerializedName("qid") 31 | var qid = "" 32 | /** 33 | * 等级 34 | */ 35 | @SerializedName("level", alternate = ["qqLevel", "qq_level"]) 36 | var level = 0 37 | /** 38 | * 在线天数?我猜的( 39 | */ 40 | @SerializedName("login_days") 41 | var loginDays = 0 42 | 43 | /** 44 | * 好友分组ID(NapCat) 45 | */ 46 | @SerializedName("categoryId") 47 | var friendGroupId = 0 48 | /** 49 | * 个性签名(NapCat、LLOnebot) 50 | */ 51 | @SerializedName("longNick", alternate = ["long_nick"]) 52 | var sign = "" 53 | /** 54 | * 电子邮箱(NapCat) 55 | */ 56 | @SerializedName("eMail") 57 | var email = "" 58 | 59 | @SerializedName("ext") 60 | var ext = JsonObject() 61 | } 62 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/ext/CreateGroupFileFolderResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.ext 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * LLOnebot 7 | */ 8 | class CreateGroupFileFolderResp { 9 | @SerializedName("folder_id") 10 | var folderId = "" 11 | } 12 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/ext/GetFileResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.ext 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GetFileResp { 6 | @SerializedName("file") 7 | var file: String = "" 8 | @SerializedName("file_name") 9 | var fileName: String = "" 10 | @SerializedName("file_size") 11 | var fileSize: Long = 0 12 | @SerializedName("base64") 13 | var base64: String? = null 14 | } 15 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/ext/SetGroupReactionResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.ext 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * AstralGocq 7 | */ 8 | class SetGroupReactionResp { 9 | @SerializedName("message_id") 10 | var messageId = 0L 11 | @SerializedName("group_id") 12 | var groupId = 0L 13 | @SerializedName("message_seq") 14 | var messageSequence = 0L 15 | } 16 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/ext/UploadGroupFileResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.ext 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * LLOnebot 7 | */ 8 | class UploadGroupFileResp { 9 | @SerializedName("file_id") 10 | var fileId = "" 11 | } 12 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/EssenceMsgResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class EssenceMsgResp { 6 | @SerializedName("sender_id") 7 | var senderId = 0L 8 | @SerializedName("sender_nick") 9 | var senderNick = "" 10 | @SerializedName("sender_time") 11 | var senderTime = 0L 12 | @SerializedName("operator_id") 13 | var operatorId = 0L 14 | @SerializedName("operator_nick") 15 | var operatorNick = "" 16 | @SerializedName("operator_time") 17 | var operatorTime = "" 18 | @SerializedName("message_id") 19 | var messageId = 0 20 | } 21 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/ForwardMsgResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import cn.evolvefield.onebot.sdk.util.json.ForwardMsgAdapter 4 | import com.google.gson.annotations.JsonAdapter 5 | import com.google.gson.annotations.SerializedName 6 | 7 | /** 8 | * @author MrXiaoM 9 | */ 10 | @JsonAdapter(ForwardMsgAdapter::class) 11 | class ForwardMsgResp { 12 | /** 13 | * 消息节点列表 14 | */ 15 | var message: List = mutableListOf() 16 | /** 17 | * 消息节点 18 | */ 19 | class Node { 20 | var time = 0 21 | var messageType = "" 22 | var messageId = 0 23 | var realId = 0 24 | var peerId = 0L 25 | var targetId = 0L 26 | var sender: Sender? = null 27 | var message = "" 28 | } 29 | 30 | /** 31 | * 消息发送者 32 | */ 33 | class Sender { 34 | @SerializedName("user_id") 35 | var userId = 0L 36 | @SerializedName("nickname") 37 | var nickname = "" 38 | @SerializedName("sex") 39 | var sex = "" 40 | @SerializedName("age") 41 | var age = 0 42 | @SerializedName("uid") 43 | var uid = "" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GetHistoryMsgResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GetHistoryMsgResp { 6 | /** 7 | * 发送时间 8 | */ 9 | @SerializedName("time") 10 | var time = 0 11 | /** 12 | * 消息类型 13 | */ 14 | @SerializedName("message_type") 15 | var messageType = 0 16 | /** 17 | * 消息id 18 | */ 19 | @SerializedName("message_id") 20 | var messageId = 0 21 | /** 22 | * 消息真实id 23 | */ 24 | @SerializedName("real_id") 25 | var realId = 0 26 | /** 27 | * 发送者 28 | */ 29 | @SerializedName("sender") 30 | var sender: Sender? = null 31 | /** 32 | * 消息内容 33 | */ 34 | @SerializedName("message") 35 | var message = "" 36 | /** 37 | * 群号 38 | */ 39 | @SerializedName("group_id") 40 | var groupId = 0L 41 | /** 42 | * 消息目标(私聊) 43 | */ 44 | @SerializedName("target_id") 45 | var targetId = 0L 46 | /** 47 | * 消息接收者,群聊是群号,私聊时是目标QQ 48 | */ 49 | @SerializedName("peer_id") 50 | var peerId = 0L 51 | 52 | /** 53 | * sender信息 54 | */ 55 | class Sender { 56 | @SerializedName("user_id") 57 | var userId = 0L 58 | @SerializedName("nickname") 59 | var nickname = "" 60 | @SerializedName("sex") 61 | var sex = "" 62 | @SerializedName("age") 63 | var age = 0 64 | @SerializedName("uid") 65 | var uid = "" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GetMsgResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import cn.evolvefield.onebot.sdk.entity.Sender 4 | import cn.evolvefield.onebot.sdk.util.json.MsgAdapter 5 | import com.google.gson.annotations.JsonAdapter 6 | 7 | @JsonAdapter(MsgAdapter::class) 8 | class GetMsgResp { 9 | var messageId = 0 10 | var realId = 0 11 | lateinit var sender: Sender 12 | var time = 0 13 | var isJsonMessage = false 14 | var message = "" 15 | var rawMessage = "" 16 | var peerId = 0L 17 | var groupId = 0L 18 | var targetId = 0L 19 | } 20 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GroupAtAllRemainResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GroupAtAllRemainResp { 6 | /** 7 | * 是否可以 @全体成员 8 | */ 9 | @SerializedName("can_at_all") 10 | var canAtAll = false 11 | /** 12 | * 群内所有管理当天剩余 @全体成员 次数 13 | */ 14 | @SerializedName("remain_at_all_count_for_group") 15 | var remainAtAllCountForGroup = 0 16 | /** 17 | * Bot 当天剩余 @全体成员 次数 18 | */ 19 | @SerializedName("remain_at_all_count_for_uin") 20 | var remainAtAllCountForUin = 0 21 | } 22 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GroupDataResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | open class GroupDataResp { 6 | @SerializedName("group_id") 7 | var groupId = 0L 8 | @SerializedName("group_name") 9 | var groupName = "" 10 | } 11 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GroupFileUrlResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GroupFileUrlResp { 6 | @SerializedName("url") 7 | var url = "" 8 | } 9 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GroupFilesResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import cn.evolvefield.onebot.sdk.util.json.GroupFilesAdapter 4 | import com.google.gson.annotations.JsonAdapter 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class GroupFilesResp { 8 | 9 | var files: List? = null 10 | 11 | var folders: List? = null 12 | 13 | /** 14 | * 群文件 15 | */ 16 | @JsonAdapter(GroupFilesAdapter.Files::class) 17 | class Files { 18 | var fileId = "" 19 | var fileName = "" 20 | var busid = 0 21 | var fileSize = 0L 22 | var uploadTime = 0L 23 | var deadTime = 0L 24 | var modifyTime = 0L 25 | var downloadTimes = 0 26 | var uploader = 0L 27 | var uploaderName = "" 28 | var md5: String? = null 29 | var sha1: String? = null 30 | var sha3: String? = null 31 | } 32 | 33 | /** 34 | * 群文件夹 35 | */ 36 | class Folders { 37 | @SerializedName("folder_id") 38 | var folderId = "" 39 | @SerializedName("folder_name") 40 | var folderName = "" 41 | @SerializedName("create_time") 42 | var createTime = 0L 43 | @SerializedName("creator") 44 | var creator = 0L 45 | @SerializedName("creator_name") 46 | var creatorName = "" 47 | @SerializedName("total_file_count") 48 | var totalFileCount = 0 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GroupHonorInfoResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GroupHonorInfoResp { 6 | @SerializedName("group_id") 7 | var groupId = 0L 8 | @SerializedName("current_talkative") 9 | private val currentTalkative: CurrentTalkative? = null 10 | @SerializedName("talkative_list") 11 | var talkativeList: List = mutableListOf() 12 | @SerializedName("performer_list") 13 | var performerList: List = mutableListOf() 14 | @SerializedName("legend_list") 15 | var legendList: List = mutableListOf() 16 | @SerializedName("strong_newbie_list") 17 | var strongNewbieList: List = mutableListOf() 18 | @SerializedName("emotion_list") 19 | var emotionList: List = mutableListOf() 20 | 21 | /** 22 | * 活跃天数 23 | */ 24 | class CurrentTalkative { 25 | @SerializedName("user_id") 26 | var userId = 0L 27 | 28 | @SerializedName("nickname") 29 | var nickname = "" 30 | 31 | @SerializedName("avatar") 32 | var avatar = "" 33 | 34 | @SerializedName("day_count") 35 | var dayCount = 0 36 | } 37 | 38 | /** 39 | * 其它荣耀 40 | */ 41 | class OtherHonor { 42 | @SerializedName("user_id") 43 | var userId = 0L 44 | 45 | @SerializedName("nickname") 46 | var nickname = "" 47 | 48 | @SerializedName("avatar") 49 | var avatar = "" 50 | 51 | @SerializedName("description") 52 | var description = "" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GroupInfoResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GroupInfoResp : GroupDataResp() { 6 | @SerializedName("group_all_shut") // NapCat,全员禁言,-1 开启,0 关闭 7 | var groupAllShut = 0 8 | @SerializedName("group_memo") 9 | var groupMemo = "" 10 | @SerializedName("group_create_time") 11 | var groupCreateTime = 0 12 | @SerializedName("group_level") 13 | var groupLevel = 0 14 | @SerializedName("member_count") 15 | var memberCount: Int? = null 16 | @SerializedName("max_member_count") 17 | var maxMemberCount: Int? = null 18 | } 19 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GroupMemberInfoResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GroupMemberInfoResp { 6 | @SerializedName("group_id") 7 | var groupId = 0L 8 | @SerializedName("user_id") 9 | var userId = 0L 10 | @SerializedName("nickname") 11 | var nickname = "" 12 | @SerializedName("card") 13 | var card = "" 14 | @SerializedName("sex") 15 | var sex = "" 16 | @SerializedName("age") 17 | var age = 0 18 | @SerializedName("area") 19 | var area = "" 20 | @SerializedName("join_time") 21 | var joinTime = 0 22 | @SerializedName("last_sent_time") 23 | var lastSentTime = 0 24 | @SerializedName("level") 25 | var level = "0" 26 | @SerializedName("qq_level") 27 | var qqLevel = 0 28 | @SerializedName("role") 29 | var role = "member" 30 | @SerializedName("unfriendly") 31 | var unfriendly = false 32 | @SerializedName("title") 33 | var title = "" 34 | @SerializedName("title_expire_time") 35 | var titleExpireTime = 0L 36 | @SerializedName("card_changeable") 37 | var cardChangeable = true 38 | @SerializedName("shut_up_timestamp") 39 | var shutUpTimestamp = 0L 40 | 41 | val levelInt: Int 42 | get() = level.toIntOrNull() ?: 0 43 | } 44 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/group/GroupNoticeResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.group 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class GroupNoticeResp { 6 | @SerializedName("sender_id") 7 | var senderId = 0L 8 | 9 | @SerializedName("publish_time") 10 | var publishTime = 0L 11 | 12 | @SerializedName("message") 13 | var message: Message? = null 14 | 15 | class Message { 16 | @SerializedName("text") 17 | var text: String = "" 18 | 19 | @SerializedName("images") 20 | var images: List = mutableListOf() 21 | } 22 | 23 | class Image { 24 | @SerializedName("id") 25 | var id = "" 26 | 27 | @SerializedName("width") 28 | var width = "0" 29 | 30 | @SerializedName("height") 31 | var height = "0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/misc/CSRFTokenResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.misc 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class CSRFTokenResp { 6 | @SerializedName("token") 7 | var token = "" 8 | } 9 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/misc/ClientsResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.misc 2 | 3 | import cn.evolvefield.onebot.sdk.util.json.ClientsAdapter 4 | import com.google.gson.annotations.JsonAdapter 5 | import com.google.gson.annotations.SerializedName 6 | 7 | class ClientsResp { 8 | @SerializedName("clients") 9 | var clients: List = mutableListOf() 10 | 11 | @JsonAdapter(ClientsAdapter::class) 12 | class Clients { 13 | var appId = 0L 14 | var deviceName = "" 15 | var deviceKind = "" 16 | var loginTime = 0L 17 | var loginPlatform = 0L 18 | var location = "" 19 | var guid = "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/misc/CookiesResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.misc 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class CookiesResp { 6 | @SerializedName("cookies") 7 | var cookies = "" 8 | } 9 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/response/misc/CredentialsResp.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.response.misc 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class CredentialsResp { 6 | @SerializedName("cookies") 7 | var cookies = "" 8 | @SerializedName("csrf_token", alternate = ["token"]) 9 | var token = "" 10 | } 11 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/util/CQCode.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.util 2 | 3 | import com.google.gson.JsonArray 4 | import com.google.gson.JsonObject 5 | import com.google.gson.JsonPrimitive 6 | 7 | object CQCode { 8 | fun fromJson(json: JsonArray): String { 9 | return buildString { 10 | for (ele in json) { 11 | val obj = ele.asJsonObject 12 | val type = obj["type"].asString 13 | val data = obj["data"] 14 | if (data is JsonObject) { 15 | // 纯文本 16 | if (type == "text") { 17 | append(data["text"].asString 18 | .replace("&", "&") 19 | .replace("[", "[") 20 | .replace("]", "]")) 21 | continue 22 | } 23 | append("[CQ:$type") 24 | for ((key, value) in data.asMap()) { 25 | // 约定: key 不会出现需要转义的字符 26 | append(",$key=") 27 | append( 28 | if (value is JsonPrimitive) { 29 | value.asString 30 | } else { 31 | value.toString() 32 | }.replace("&", "&") 33 | .replace("[", "[") 34 | .replace("]", "]") 35 | .replace(",", ",") 36 | ) 37 | } 38 | } 39 | append("]") 40 | } 41 | } 42 | } 43 | 44 | fun toJson(s: String): JsonArray { 45 | val array = JsonArray() 46 | fun add(type: String, data: JsonObject.() -> Unit) { 47 | array.add(JsonObject().also { 48 | it.addProperty("type", type) 49 | it.add("data", JsonObject().apply(data)) 50 | }) 51 | } 52 | fun String.decode(): String { 53 | return replace("&", "&") 54 | .replace("[", "[") 55 | .replace("]", "]") 56 | } 57 | fun String.decodeValue(): String { 58 | return decode() 59 | .replace(",", ",") 60 | } 61 | fun StringBuilder.decode(): String { 62 | return toString() 63 | .decode() 64 | .also { clear() } 65 | } 66 | val temp = StringBuilder() 67 | var flag = false 68 | for (c in s) { 69 | if (c == '[' && temp.isNotEmpty()) { 70 | add("text") { 71 | addProperty("text", temp.decode()) 72 | } 73 | } 74 | temp.append(c) 75 | if (!flag && temp.startsWith("[CQ:")) flag = true 76 | if (flag && c == ']') { 77 | val cq = temp.substring(4, temp.length - 1) 78 | if (cq.contains(',')) { 79 | val split = cq.split(',') 80 | add(split[0]) { 81 | for (s1 in split.drop(1)) { 82 | if (!s1.contains('=')) continue 83 | val (key, value) = s1 84 | .split('=', limit = 2) 85 | .run { this[0] to this[1].decodeValue() } 86 | addProperty(key, value) 87 | } 88 | } 89 | } else add(cq) { } 90 | temp.clear() 91 | flag = false 92 | } 93 | } 94 | if (temp.isNotEmpty()) { 95 | add("text") { 96 | addProperty("text", temp.decode()) 97 | } 98 | } 99 | return array 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/util/JsonDeserializerKt.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.util 2 | 3 | import com.google.gson.JsonDeserializationContext 4 | import com.google.gson.JsonDeserializer 5 | import com.google.gson.JsonElement 6 | import com.google.gson.JsonParseException 7 | import java.lang.reflect.Type 8 | 9 | interface JsonDeserializerKt : JsonDeserializer { 10 | override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T? { 11 | return runCatching { 12 | deserializeFromJson(json, typeOfT, context) 13 | }.getOrElse { 14 | throw JsonParseException("An error has occurred while parsing json $json to ${typeOfT.typeName}", it) 15 | } 16 | } 17 | 18 | fun deserializeFromJson(json: JsonElement, type: Type, ctx: JsonDeserializationContext): T? 19 | } 20 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/util/json/ClientsAdapter.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.util.json 2 | 3 | import cn.evolvefield.onebot.sdk.response.misc.ClientsResp 4 | import cn.evolvefield.onebot.sdk.util.JsonDeserializerKt 5 | import cn.evolvefield.onebot.sdk.util.ignorable 6 | import cn.evolvefield.onebot.sdk.util.long 7 | import cn.evolvefield.onebot.sdk.util.string 8 | import com.google.gson.JsonDeserializationContext 9 | import com.google.gson.JsonElement 10 | import java.lang.reflect.Type 11 | 12 | class ClientsAdapter : JsonDeserializerKt { 13 | override fun deserializeFromJson( 14 | json: JsonElement, 15 | type: Type, 16 | ctx: JsonDeserializationContext 17 | ): ClientsResp.Clients { 18 | val obj = json.asJsonObject 19 | return ClientsResp.Clients().apply { 20 | appId = obj.long("app_id") 21 | deviceName = obj.string("device_name") 22 | deviceKind = obj.string("device_kind") 23 | loginTime = obj.ignorable("login_time", 0L) 24 | loginPlatform = obj.ignorable("login_platform", 0L) 25 | location = obj.ignorable("location", "") 26 | guid = obj.ignorable("guid", "") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/util/json/ForwardMsgAdapter.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.util.json 2 | 3 | import cn.evolvefield.onebot.sdk.response.group.ForwardMsgResp 4 | import cn.evolvefield.onebot.sdk.util.JsonDeserializerKt 5 | import cn.evolvefield.onebot.sdk.util.forceString 6 | import cn.evolvefield.onebot.sdk.util.fromJson 7 | import cn.evolvefield.onebot.sdk.util.ignorable 8 | import com.google.gson.JsonDeserializationContext 9 | import com.google.gson.JsonElement 10 | import java.lang.reflect.Type 11 | 12 | class ForwardMsgAdapter : JsonDeserializerKt { 13 | override fun deserializeFromJson( 14 | json: JsonElement, 15 | type: Type, 16 | ctx: JsonDeserializationContext 17 | ): ForwardMsgResp { 18 | val nodes = mutableListOf() 19 | val jsonObj = json.asJsonObject 20 | val msgArray = when { 21 | jsonObj.has("messages") -> jsonObj.get("messages") //Lagrange 22 | jsonObj.has("message") -> jsonObj.get("message") // go-cqhttp 23 | jsonObj.has("content") -> jsonObj.get("content") //old nap-cat 24 | else -> throw IllegalArgumentException("不受支持的合并转发消息格式!请携带日志前往对应实现反馈。") 25 | } 26 | for (element in msgArray.asJsonArray) { 27 | val obj = element.asJsonObject.run { 28 | if (has("data")) get("data").asJsonObject 29 | else this 30 | } 31 | nodes.add(ForwardMsgResp.Node().apply { 32 | time = obj.ignorable("time", 0) // OpenShamrock 33 | messageType = obj.ignorable("message_type", "") // OpenShamrock 34 | realId = obj.ignorable("real_id", 0) // OpenShamrock 35 | sender = obj.fromJson("sender") // OpenShamrock 36 | if (sender == null) { // Lagrange 37 | sender = ForwardMsgResp.Sender().apply { 38 | userId = obj.ignorable("user_id", 0L) 39 | nickname = obj.ignorable("nickname", "") 40 | sex = "unknown" 41 | } 42 | } 43 | message = if (obj.has("content")) { 44 | obj.forceString("content") // Lagrange 45 | } else { 46 | obj.forceString("message") // go-cqhttp, OpenShamrock 47 | } 48 | peerId = obj.ignorable("peer_id", 0L) // OpenShamrock 49 | targetId = obj.ignorable("target_id", 0L) // OpenShamrock 50 | }) 51 | } 52 | return ForwardMsgResp().apply { message = nodes } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/util/json/GroupFilesAdapter.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.util.json 2 | 3 | import cn.evolvefield.onebot.sdk.response.group.GroupFilesResp 4 | import cn.evolvefield.onebot.sdk.util.* 5 | import com.google.gson.JsonDeserializationContext 6 | import com.google.gson.JsonElement 7 | import java.lang.reflect.Type 8 | 9 | class GroupFilesAdapter { 10 | class Files : JsonDeserializerKt { 11 | override fun deserializeFromJson( 12 | json: JsonElement, 13 | type: Type, 14 | ctx: JsonDeserializationContext 15 | ): GroupFilesResp.Files { 16 | val obj = json.asJsonObject 17 | return GroupFilesResp.Files().apply { 18 | fileId = obj.string("file_id") 19 | fileName = obj.string("file_name") 20 | busid = obj.int("busid") 21 | fileSize = obj.long("file_size", "size") // NapCat: size 22 | uploadTime = obj.long("upload_time") 23 | deadTime = obj.long("dead_time") 24 | modifyTime = obj.long("modify_time") 25 | downloadTimes = obj.int("download_times") 26 | uploader = obj.long("uploader") 27 | uploaderName = obj.string("uploader_name") 28 | md5 = obj.nullableString("md5", null) 29 | sha1 = obj.nullableString("sha1", null) 30 | sha3 = obj.nullableString("sha3", null) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/util/json/MessageEventAdapter.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.util.json 2 | 3 | import cn.evolvefield.onebot.sdk.entity.PrivateSender 4 | import cn.evolvefield.onebot.sdk.event.message.GroupMessageEvent 5 | import cn.evolvefield.onebot.sdk.event.message.MessageEvent 6 | import cn.evolvefield.onebot.sdk.event.message.PrivateMessageEvent 7 | import cn.evolvefield.onebot.sdk.util.* 8 | import cn.evolvefield.onebot.sdk.util.json.MsgAdapter.Companion.messageId 9 | import com.google.gson.JsonDeserializationContext 10 | import com.google.gson.JsonDeserializer 11 | import com.google.gson.JsonElement 12 | import com.google.gson.JsonObject 13 | import java.lang.reflect.Type 14 | 15 | class MessageEventAdapter : JsonDeserializerKt { 16 | override fun deserializeFromJson( 17 | json: JsonElement, 18 | type: Type, 19 | ctx: JsonDeserializationContext 20 | ): MessageEvent? { 21 | val obj = json.asJsonObject 22 | val subType = obj.string("sub_type") 23 | val messageType = obj.string("message_type") 24 | return when (messageType) { 25 | "group" -> obj.groupMessage(subType) 26 | "private" -> obj.privateMessage(subType) 27 | else -> null 28 | }?.apply { 29 | postType = obj.string("post_type") 30 | time = obj.long("time") 31 | selfId = obj.long("self_id") 32 | userId = obj.long("user_id") 33 | isJsonMessage = obj.has("message") && obj["message"].isJsonArray 34 | message = obj.forceString("message") 35 | rawMessage = obj.ignorable("raw_message", "") 36 | font = obj.ignorable("font", 0) 37 | } 38 | } 39 | 40 | private fun JsonObject.groupMessage(subType: String): MessageEvent = GroupMessageEvent().apply { 41 | this.subType = subType 42 | messageId = messageId() 43 | groupId = long("group_id") 44 | anonymous = fromJson("anonymous") 45 | sender = fromJson("sender") 46 | } 47 | private fun JsonObject.privateMessage(subType: String): MessageEvent = PrivateMessageEvent().apply { 48 | this.subType = subType 49 | messageId = messageId() 50 | tempSource = ignorable("temp_source", Int.MIN_VALUE) 51 | sender = fromJson("sender")!! 52 | groupId = ignorable("group_id", sender.groupId) 53 | fromNick = ignorable("from_nick", sender.nickname) 54 | } 55 | 56 | class PrivateSenderAdapter : JsonDeserializer { 57 | override fun deserialize( 58 | json: JsonElement, 59 | typeOfT: Type, 60 | context: JsonDeserializationContext 61 | ): PrivateSender { 62 | val obj = json.asJsonObject 63 | return PrivateSender().apply { 64 | userId = obj["user_id"].asLong 65 | groupId = obj.ignorable("group_id", 0L) 66 | nickname = obj["nickname"].asString 67 | sex = obj.ignorable("sex", "unknown") 68 | age = obj.ignorable("age", 0) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/cn/evolvefield/onebot/sdk/util/json/MsgAdapter.kt: -------------------------------------------------------------------------------- 1 | package cn.evolvefield.onebot.sdk.util.json 2 | 3 | import cn.evolvefield.onebot.sdk.response.group.GetMsgResp 4 | import cn.evolvefield.onebot.sdk.util.* 5 | import com.google.gson.JsonDeserializationContext 6 | import com.google.gson.JsonElement 7 | import com.google.gson.JsonObject 8 | import java.lang.reflect.Type 9 | 10 | class MsgAdapter : JsonDeserializerKt { 11 | override fun deserializeFromJson( 12 | json: JsonElement, 13 | type: Type, 14 | ctx: JsonDeserializationContext 15 | ): GetMsgResp { 16 | val obj = json.asJsonObject 17 | return GetMsgResp().apply { 18 | messageId = obj.messageId() 19 | realId = obj.ignorable("real_id", 0) 20 | sender = obj.fromJson("sender")!! 21 | time = obj.int("time") 22 | isJsonMessage = obj.has("message") && obj["message"].isJsonArray 23 | message = obj.forceString("message") 24 | rawMessage = obj.ignorable("raw_message", "") 25 | peerId = obj.ignorable("peer_id", 0L) 26 | groupId = obj.ignorable("group_id", 0L) 27 | targetId = obj.ignorable("target_id", 0L) 28 | } 29 | } 30 | 31 | companion object { 32 | fun JsonObject.messageId(): Int { 33 | val msgId: String = this["message_id"]?.asString ?: throw IllegalStateException(""" 34 | 收到消息ID为空的消息事件。type=${this["message_type"]}, sub_type=${this["sub_type"]} 35 | 请向你所使用的 Onebot 实现维护者报告该问题, 36 | 不要将该问题反馈到 Overflow。 37 | """.trimIndent() 38 | ) 39 | try { 40 | return msgId.toInt() 41 | } catch (ignored: NumberFormatException) { 42 | throw NumberFormatException( 43 | """ 44 | 无法将消息ID $msgId 的类型转换为 int32, 45 | 请向你所使用的 Onebot 实现维护者报告该问题, 46 | 不要将该问题反馈到 Overflow。 47 | """.trimIndent() 48 | ) 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/AbstractBot.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/master/LICENSE 8 | */ 9 | package net.mamoe.mirai.internal 10 | 11 | import kotlinx.coroutines.CoroutineScope 12 | import net.mamoe.mirai.Bot 13 | 14 | abstract class AbstractBot : Bot, CoroutineScope 15 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/QQAndroidBot.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/master/LICENSE 8 | */ 9 | package net.mamoe.mirai.internal 10 | 11 | import net.mamoe.mirai.Bot 12 | import net.mamoe.mirai.internal.network.QQAndroidClient 13 | import kotlin.contracts.ExperimentalContracts 14 | import kotlin.contracts.contract 15 | 16 | @OptIn(ExperimentalContracts::class) 17 | internal fun Bot.asQQAndroidBot(): QQAndroidBot { 18 | contract { 19 | returns() implies (this@asQQAndroidBot is QQAndroidBot) 20 | } 21 | 22 | return this as QQAndroidBot 23 | } 24 | 25 | internal abstract class QQAndroidBot : AbstractBot() { 26 | abstract val implGetter: () -> cn.evolvefield.onebot.client.core.Bot 27 | val client: QQAndroidClient by lazy { 28 | QQAndroidClient(implGetter) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/event/EventChannelToEventDispatcherAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/dev/LICENSE 8 | */ 9 | 10 | package net.mamoe.mirai.internal.event 11 | 12 | import net.mamoe.mirai.event.Event 13 | import kotlin.coroutines.CoroutineContext 14 | import kotlin.coroutines.EmptyCoroutineContext 15 | import kotlin.reflect.KClass 16 | 17 | internal class EventChannelToEventDispatcherAdapter private constructor( 18 | baseEventClass: KClass, defaultCoroutineContext: CoroutineContext = EmptyCoroutineContext 19 | ) : EventChannelImpl(baseEventClass, defaultCoroutineContext) { 20 | companion object { 21 | @InternalEventMechanism 22 | val instance by lazy { EventChannelToEventDispatcherAdapter(Event::class, EmptyCoroutineContext) } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/event/GlobalEventChannelProviderImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/dev/LICENSE 8 | */ 9 | 10 | package net.mamoe.mirai.internal.event 11 | 12 | import net.mamoe.mirai.event.Event 13 | import net.mamoe.mirai.event.EventChannel 14 | import net.mamoe.mirai.event.InternalGlobalEventChannelProvider 15 | import net.mamoe.mirai.utils.MiraiInternalApi 16 | 17 | @OptIn(MiraiInternalApi::class) 18 | @InternalEventMechanism 19 | internal class GlobalEventChannelProviderImpl : InternalGlobalEventChannelProvider { 20 | @InternalEventMechanism 21 | val instance = EventChannelToEventDispatcherAdapter.instance 22 | 23 | @OptIn(InternalEventMechanism::class) 24 | override fun getInstance(): EventChannel = instance 25 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/event/package.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/dev/LICENSE 8 | */ 9 | 10 | package net.mamoe.mirai.internal.event 11 | 12 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/message/RefinableMessage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/dev/LICENSE 8 | */ 9 | package net.mamoe.mirai.internal.message 10 | 11 | import net.mamoe.mirai.message.data.* 12 | 13 | internal sealed class MessageRefiner 14 | 15 | /** 16 | * 执行不需要 `suspend` 的 refine. 用于 [MessageSource.originalMessage]. 17 | * 18 | * 兼容 cssxsh/mirai-hibernate-plugin 19 | * 20 | * https://github.com/cssxsh/mirai-hibernate-plugin/blob/8f425db01629ff84900b53b1bf86c741ef7f81be/src/main/kotlin/xyz/cssxsh/mirai/hibernate/MiraiHibernateRecorder.kt 21 | */ 22 | @Suppress("unused") 23 | internal object LightMessageRefiner : MessageRefiner() { 24 | /** 25 | * 去除 [MessageChain] 携带的内部标识 26 | * 27 | * 用于 [createMessageReceipt] <- `RemoteFile.uploadAndSend` (文件操作API v1) 28 | */ 29 | fun MessageChain.dropMiraiInternalFlags(): MessageChain = this 30 | } 31 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/message/data/MarketFaceImpl.kt: -------------------------------------------------------------------------------- 1 | package net.mamoe.mirai.internal.message.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.Transient 6 | import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody 7 | import net.mamoe.mirai.message.data.MarketFace 8 | import net.mamoe.mirai.utils.MiraiExperimentalApi 9 | 10 | @SerialName(MarketFaceImpl.SERIAL_NAME) 11 | @Serializable 12 | internal data class MarketFaceImpl internal constructor( 13 | internal val delegate: ImMsgBody.MarketFace, 14 | ) : MarketFace { 15 | 16 | override val name: String get() = delegate.faceName.decodeToString() 17 | 18 | @Transient 19 | @MiraiExperimentalApi 20 | override val id: Int = delegate.tabId 21 | 22 | @OptIn(MiraiExperimentalApi::class) 23 | override fun toString() = "[mirai:marketface:$id,$name]" 24 | 25 | companion object { 26 | const val SERIAL_NAME = MarketFace.SERIAL_NAME 27 | } 28 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/network/QQAndroidClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/master/LICENSE 8 | */ 9 | package net.mamoe.mirai.internal.network 10 | 11 | import net.mamoe.mirai.internal.network.components.SsoSession 12 | 13 | internal open class QQAndroidClient( 14 | implGetter: () -> cn.evolvefield.onebot.client.core.Bot 15 | ) : SsoSession { 16 | override var loginState: Int = 0 17 | override var wLoginSigInfo: WLoginSigInfo = WLoginSigInfo(implGetter) 18 | } 19 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/network/components/SsoProcessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/master/LICENSE 8 | */ 9 | package net.mamoe.mirai.internal.network.components 10 | 11 | import net.mamoe.mirai.internal.network.WLoginSigInfo 12 | 13 | /** 14 | * Contains secrets for encryption and decryption during a session created by [SsoProcessor] and [PacketCodec]. 15 | * 16 | * @see AccountSecrets 17 | */ 18 | internal interface SsoSession { 19 | //var outgoingPacketSessionId: ByteArray 20 | 21 | /** 22 | * always 0 for now. 23 | */ 24 | var loginState: Int 25 | 26 | // also present in AccountSecrets 27 | var wLoginSigInfo: WLoginSigInfo 28 | //val randomKey: ByteArray 29 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/network/keys.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/master/LICENSE 8 | */ 9 | @file:Suppress("unused") 10 | package net.mamoe.mirai.internal.network 11 | 12 | import kotlinx.coroutines.runBlocking 13 | import net.mamoe.mirai.internal.AbstractBot 14 | import net.mamoe.mirai.internal.QQAndroidBot 15 | 16 | /** 17 | * 兼容 cssxsh/meme-helper 中请求网络接口所需的 bkn、sKey、psKey 等参数 18 | * 19 | * https://github.com/cssxsh/meme-helper/blob/61fbfd967a5e45dd9923c470622cd90d5bce1ce4/src/main/kotlin/face/MarketFaceHelper.kt 20 | */ 21 | internal data class WLoginSigInfo( 22 | val impl: () -> cn.evolvefield.onebot.client.core.Bot 23 | ) { 24 | val bkn: Int 25 | get() = sKey.encodeToByteArray() 26 | .fold(5381) { acc: Int, b: Byte -> acc + acc.shl(5) + b.toInt() } 27 | .and(Int.MAX_VALUE) 28 | 29 | val sKey: String 30 | get() = runBlocking { 31 | val data = impl().getCredentials("qun.qq.com").data ?: throw IllegalStateException("Onebot 获取 Credentials (sKey) 失败") 32 | val matches = Regex("[^_]skey=([^;]+);?").find(data.cookies) ?: throw IllegalStateException("Onebot 获取 Credentials 返回的 cookie 中没有 skey") 33 | return@runBlocking matches.groupValues[1] 34 | } 35 | 36 | fun getPsKey(name: String): String { 37 | return runBlocking { 38 | val data = impl().getCredentials(name).data ?: throw IllegalStateException("Onebot 获取 Credentials (psKey) 失败") 39 | val matches = Regex("p_skey=([^;]+);?").find(data.cookies) ?: throw IllegalStateException("Onebot 获取 Credentials 返回的 cookie 中没有 p_skey") 40 | return@runBlocking matches.groupValues[1] 41 | } 42 | } 43 | } 44 | 45 | internal val AbstractBot.sKey get() = client.wLoginSigInfo.sKey 46 | internal fun AbstractBot.psKey(name: String) = client.wLoginSigInfo.getPsKey(name) 47 | internal val AbstractBot.client get() = (this as QQAndroidBot).client 48 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/net/mamoe/mirai/internal/network/protocol/data/proto/Msg.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 Mamoe Technologies and contributors. 3 | * 4 | * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. 5 | * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. 6 | * 7 | * https://github.com/mamoe/mirai/blob/dev/LICENSE 8 | */ 9 | package net.mamoe.mirai.internal.network.protocol.data.proto 10 | 11 | import kotlinx.serialization.Serializable 12 | import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY 13 | import net.mamoe.mirai.utils.isSameType 14 | 15 | @Serializable 16 | internal class ImMsgBody /*: ProtoBuf*/ { 17 | 18 | @Serializable 19 | internal data class MarketFace( 20 | /*@ProtoNumber(1) */@JvmField var faceName: ByteArray = EMPTY_BYTE_ARRAY, 21 | /*@ProtoNumber(2) */@JvmField val itemType: Int = 0, 22 | /*@ProtoNumber(3) */@JvmField val faceInfo: Int = 0, 23 | /*@ProtoNumber(4) */@JvmField val faceId: ByteArray = EMPTY_BYTE_ARRAY, 24 | /*@ProtoNumber(5) */@JvmField val tabId: Int = 0, 25 | /*@ProtoNumber(6) */@JvmField val subType: Int = 0, 26 | /*@ProtoNumber(7) */@JvmField val key: ByteArray = EMPTY_BYTE_ARRAY, 27 | /*@ProtoNumber(8) */@JvmField val param: ByteArray = EMPTY_BYTE_ARRAY, 28 | /*@ProtoNumber(9) */@JvmField val mediaType: Int = 0, 29 | /*@ProtoNumber(10) */@JvmField val imageWidth: Int = 0, 30 | /*@ProtoNumber(11) */@JvmField val imageHeight: Int = 0, 31 | /*@ProtoNumber(12) */@JvmField val mobileParam: ByteArray = EMPTY_BYTE_ARRAY, 32 | /*@ProtoNumber(13) */@JvmField val pbReserve: ByteArray = EMPTY_BYTE_ARRAY, 33 | ) /*: ProtoBuf*/ { 34 | @Suppress("DuplicatedCode") 35 | override fun equals(other: Any?): Boolean { 36 | if (this === other) return true 37 | if (!isSameType(this, other)) return false 38 | 39 | if (!faceName.contentEquals(other.faceName)) return false 40 | if (itemType != other.itemType) return false 41 | if (faceInfo != other.faceInfo) return false 42 | if (!faceId.contentEquals(other.faceId)) return false 43 | if (tabId != other.tabId) return false 44 | if (subType != other.subType) return false 45 | if (!key.contentEquals(other.key)) return false 46 | if (!param.contentEquals(other.param)) return false 47 | if (mediaType != other.mediaType) return false 48 | if (imageWidth != other.imageWidth) return false 49 | if (imageHeight != other.imageHeight) return false 50 | if (!mobileParam.contentEquals(other.mobileParam)) return false 51 | if (!pbReserve.contentEquals(other.pbReserve)) return false 52 | 53 | return true 54 | } 55 | 56 | override fun hashCode(): Int { 57 | var result = faceName.contentHashCode() 58 | result = 31 * result + itemType 59 | result = 31 * result + faceInfo 60 | result = 31 * result + faceId.contentHashCode() 61 | result = 31 * result + tabId 62 | result = 31 * result + subType 63 | result = 31 * result + key.contentHashCode() 64 | result = 31 * result + param.contentHashCode() 65 | result = 31 * result + mediaType 66 | result = 31 * result + imageWidth 67 | result = 31 * result + imageHeight 68 | result = 31 * result + mobileParam.contentHashCode() 69 | result = 31 * result + pbReserve.contentHashCode() 70 | return result 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/BotFactoryImpl.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal 2 | 3 | import net.mamoe.mirai.Bot 4 | import net.mamoe.mirai.BotFactory 5 | import net.mamoe.mirai.auth.BotAuthorization 6 | import net.mamoe.mirai.utils.BotConfiguration 7 | 8 | internal object BotFactoryImpl : BotFactory { 9 | override fun newBot(qq: Long, passwordMd5: ByteArray, configuration: BotConfiguration): Bot = end(qq) 10 | 11 | override fun newBot(qq: Long, password: String, configuration: BotConfiguration): Bot = end(qq) 12 | 13 | override fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot = end(qq) 14 | 15 | private fun end(qq: Long): Bot { 16 | Bot.getInstanceOrNull(qq)?.also { return it } 17 | throw UnsupportedOperationException("溢出核心已委托远程实现接管了账户管理,mirai 框架端没有登录机器人的职责") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/Config.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Config( 8 | @SerialName("no_log___DO_NOT_REPORT_IF_YOU_SWITCH_THIS_ON___开启此选项时不接受漏洞反馈") 9 | var noLogDoNotReportIfYouSwitchThisOn: Boolean = false, 10 | @SerialName("ws_host") 11 | var wsHost: String = "ws://127.0.0.1:3001", 12 | @SerialName("reversed_ws_port") 13 | var reversedWSPort: Int = -1, 14 | @SerialName("token") 15 | var token: String = "", 16 | @SerialName("no_platform") 17 | val noPlatform: Boolean = false, 18 | @SerialName("use_cq_code") 19 | val useCQCode: Boolean = false, 20 | @SerialName("retry_times") 21 | var retryTimes: Int = 5, 22 | @SerialName("retry_wait_mills") 23 | var retryWaitMills: Long = 5_000L, 24 | @SerialName("retry_rest_mills") 25 | var retryRestMills: Long = 60_000L, 26 | @SerialName("heartbeat_check_seconds") 27 | var heartbeatCheckSeconds: Int = 60, 28 | @SerialName("use_group_upload_event_for_file_message") 29 | var useGroupUploadEventForFileMessage: Boolean = false, 30 | @SerialName("resource_cache") 31 | var resourceCache: CacheConfig = CacheConfig(), 32 | @SerialName("drop_events_before_connected") 33 | var dropEventsBeforeConnected: Boolean = true, 34 | ) 35 | @Serializable 36 | data class CacheConfig ( 37 | @SerialName("enabled") 38 | var enabled: Boolean = false, 39 | @SerialName("keep_duration_hours") 40 | var keepDurationHours: Long = 168L, 41 | ) 42 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/contact/OtherClientWrapper.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 2 | package top.mrxiaom.overflow.internal.contact 3 | 4 | import kotlinx.coroutines.CoroutineName 5 | import net.mamoe.mirai.contact.OtherClient 6 | import net.mamoe.mirai.contact.OtherClientInfo 7 | import net.mamoe.mirai.message.data.ShortVideo 8 | import net.mamoe.mirai.utils.ExternalResource 9 | import top.mrxiaom.overflow.internal.message.OnebotMessages 10 | import top.mrxiaom.overflow.spi.FileService 11 | import kotlin.coroutines.CoroutineContext 12 | 13 | internal class OtherClientWrapper( 14 | override val bot: BotWrapper, 15 | override var info: OtherClientInfo, 16 | ) : OtherClient { 17 | override val coroutineContext: CoroutineContext = CoroutineName("(Bot/${bot.id})OtherClient/${info.deviceKind}") 18 | 19 | override suspend fun uploadShortVideo( 20 | thumbnail: ExternalResource, 21 | video: ExternalResource, 22 | fileName: String? 23 | ): ShortVideo { 24 | return OnebotMessages.videoFromFile(FileService.instance!!.upload(video)) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/contact/data/AnnouncementsWrapper.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 2 | package top.mrxiaom.overflow.internal.contact.data 3 | 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.asFlow 6 | import net.mamoe.mirai.contact.Group 7 | import net.mamoe.mirai.contact.NormalMember 8 | import net.mamoe.mirai.contact.announcement.* 9 | import net.mamoe.mirai.utils.ExternalResource 10 | import net.mamoe.mirai.utils.currentTimeSeconds 11 | import top.mrxiaom.overflow.internal.contact.GroupWrapper 12 | import top.mrxiaom.overflow.internal.utils.FastImageInfo 13 | import top.mrxiaom.overflow.spi.FileService 14 | 15 | internal class AnnouncementsWrapper( 16 | val impl: GroupWrapper, 17 | internal var list: List 18 | ) : Announcements { 19 | override fun asFlow(): Flow = list.asFlow() 20 | 21 | override suspend fun delete(fid: String): Boolean { 22 | TODO("Not yet implemented") 23 | } 24 | 25 | override suspend fun get(fid: String): OnlineAnnouncement? { 26 | TODO("Not yet implemented") 27 | } 28 | 29 | override suspend fun members(fid: String, confirmed: Boolean): List { 30 | TODO("Not yet implemented") 31 | } 32 | 33 | override suspend fun remind(fid: String) { 34 | TODO("Not yet implemented") 35 | } 36 | 37 | override suspend fun publish(announcement: Announcement): OnlineAnnouncement { 38 | impl.bot.impl.sendGroupNotice( 39 | impl.id, 40 | announcement.content, 41 | announcement.parameters.image?.file 42 | ) 43 | return OnlineAnnouncementWrapper( 44 | content = announcement.content, 45 | group = impl, 46 | senderId = impl.bot.id, 47 | parameters = announcement.parameters 48 | ) 49 | } 50 | 51 | override suspend fun uploadImage(resource: ExternalResource): AnnouncementImage { 52 | val size = FastImageInfo(resource.inputStream()) 53 | return AnnouncementImage.create("\n${FileService.instance!!.upload(resource)}\n", size?.height ?: 0, size?.width ?: 0) 54 | } 55 | 56 | suspend fun update() { 57 | list = impl.fetchAnnouncements().list 58 | } 59 | 60 | companion object { 61 | suspend fun GroupWrapper.fetchAnnouncements(): AnnouncementsWrapper { 62 | val list = bot.impl.getGroupNotice(id).data.map { 63 | val message = it.message ?: return@map null 64 | OnlineAnnouncementWrapper( 65 | content = message.text, 66 | group = this, 67 | senderId = it.senderId, 68 | publicationTime = it.publishTime, 69 | parameters = AnnouncementParametersBuilder().apply { 70 | for (image in message.images) { 71 | image(AnnouncementImage.create( 72 | image.id, image.height.toIntOrNull() ?: 0, image.width.toIntOrNull() ?: 0 73 | )) 74 | } 75 | }.build() 76 | ) 77 | } 78 | return AnnouncementsWrapper(this, list.filterNotNull()) 79 | } 80 | } 81 | } 82 | 83 | internal class OnlineAnnouncementWrapper( 84 | override val content: String, 85 | override val group: Group, 86 | override val senderId: Long, 87 | override val parameters: AnnouncementParameters = AnnouncementParameters.DEFAULT, 88 | override val allConfirmed: Boolean = false, 89 | override val confirmedMembersCount: Int = 0, 90 | override val fid: String = "", 91 | override val publicationTime: Long = currentTimeSeconds(), 92 | override val sender: NormalMember? = group[senderId], 93 | ) : OnlineAnnouncement 94 | 95 | internal val AnnouncementImage.file: String 96 | get() = if (url.contains("\n")) url.split("\n")[1] else url 97 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/contact/data/EssencesWrapper.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.contact.data 2 | 3 | import kotlinx.coroutines.currentCoroutineContext 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.flow 6 | import kotlinx.coroutines.isActive 7 | import net.mamoe.mirai.contact.MemberPermission 8 | import net.mamoe.mirai.contact.checkBotPermission 9 | import net.mamoe.mirai.contact.essence.EssenceMessageRecord 10 | import net.mamoe.mirai.contact.essence.Essences 11 | import net.mamoe.mirai.message.data.MessageSource 12 | import net.mamoe.mirai.message.data.MessageSourceBuilder 13 | import net.mamoe.mirai.message.data.MessageSourceKind 14 | import net.mamoe.mirai.message.data.messageChainOf 15 | import top.mrxiaom.overflow.internal.contact.GroupWrapper 16 | import top.mrxiaom.overflow.internal.utils.shareDigest 17 | 18 | internal class EssencesWrapper( 19 | val impl: GroupWrapper, 20 | private var list: List 21 | ) : Essences { 22 | override fun asFlow(): Flow { 23 | return flow { 24 | var offset = 0 25 | while (currentCoroutineContext().isActive) { 26 | val list = impl.fetchEssencesList(offset) 27 | for (message in list) { 28 | emit(message) 29 | } 30 | if (list.isEmpty() || list.size < 20) break 31 | offset++ 32 | } 33 | } 34 | } 35 | 36 | override suspend fun getPage(start: Int, limit: Int): List { 37 | if (impl.bot.noPlatform) return listOf() 38 | return impl.fetchEssencesList().also { list = it } 39 | } 40 | 41 | override suspend fun remove(source: MessageSource) { 42 | impl.checkBotPermission(MemberPermission.ADMINISTRATOR) 43 | impl.bot.impl.deleteEssenceMsg(source.ids[0]) 44 | } 45 | 46 | override suspend fun share(source: MessageSource): String { 47 | if (impl.bot.noPlatform) return "" 48 | val shareKey = impl.bot.shareDigest( 49 | groupCode = impl.id, 50 | msgSeq = source.ids.first().toLong().and(0xFFFF_FFFF), 51 | msgRandom = source.internalIds.first().toLong().and(0xFFFF_FFFF), 52 | targetGroupCode = 0 53 | ) 54 | return "https://qun.qq.com/essence/share?_wv=3&_wwv=128&_wvx=2&sharekey=$shareKey" 55 | } 56 | 57 | companion object { 58 | internal suspend fun GroupWrapper.fetchEssencesList(page: Int = 0): List { 59 | if (bot.noPlatform) return listOf() 60 | return bot.impl.getEssenceMsgList(id, page).data.map { 61 | EssenceMessageRecord( 62 | this, queryMember(it.senderId), it.senderId, it.senderNick, it.senderTime.toInt(), 63 | queryMember(it.operatorId), it.operatorId, it.operatorNick, it.operatorTime.toInt() 64 | ) { parse -> 65 | MessageSourceBuilder().apply { 66 | id(it.messageId) 67 | internalId(it.messageId) 68 | time(it.senderTime.toInt()) 69 | target(this@fetchEssencesList) 70 | sender(it.senderId) 71 | if (parse) { 72 | bot.getMsg(it.messageId)?.also { msg -> messageChainOf(msg) } 73 | } 74 | }.build(bot.id, MessageSourceKind.GROUP) 75 | } 76 | } 77 | } 78 | internal suspend fun GroupWrapper.fetchEssences(): EssencesWrapper { 79 | return EssencesWrapper(this, fetchEssencesList()) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/contact/data/FallbackFriendGroup.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.contact.data 2 | 3 | import net.mamoe.mirai.Bot 4 | import net.mamoe.mirai.contact.Friend 5 | import net.mamoe.mirai.contact.friendgroup.FriendGroup 6 | 7 | class FallbackFriendGroup( 8 | val bot: Bot 9 | ): FriendGroup { 10 | override val count: Int 11 | get() = friends.size 12 | override val friends: Collection 13 | get() = bot.friends 14 | override val id: Int = 0 15 | override val name: String = "我的好友" 16 | 17 | override suspend fun delete(): Boolean = throw IllegalStateException("Onebot 未提供好友分组接口") 18 | override suspend fun moveIn(friend: Friend): Boolean = throw IllegalStateException("Onebot 未提供好友分组接口") 19 | override suspend fun renameTo(newName: String): Boolean = throw IllegalStateException("Onebot 未提供好友分组接口") 20 | } 21 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/contact/data/FallbackFriendGroups.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.contact.data 2 | 3 | import net.mamoe.mirai.Bot 4 | import net.mamoe.mirai.contact.friendgroup.FriendGroup 5 | import net.mamoe.mirai.contact.friendgroup.FriendGroups 6 | 7 | class FallbackFriendGroups( 8 | val bot: Bot 9 | ) : FriendGroups { 10 | 11 | internal val fallbackFriendGroup = FallbackFriendGroup(bot) 12 | 13 | override fun asCollection(): Collection = listOf(fallbackFriendGroup) 14 | override suspend fun create(name: String): FriendGroup = fallbackFriendGroup 15 | override fun get(id: Int): FriendGroup? = fallbackFriendGroup 16 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/contact/data/GroupSettingsWrapper.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.contact.data 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import net.mamoe.mirai.contact.GroupSettings 5 | import net.mamoe.mirai.contact.MemberPermission 6 | import net.mamoe.mirai.contact.checkBotPermission 7 | import top.mrxiaom.overflow.internal.contact.GroupWrapper 8 | 9 | internal class GroupSettingsWrapper( 10 | val group: GroupWrapper 11 | ) : GroupSettings { 12 | @Deprecated( 13 | "group.announcements.asFlow().filter { it.parameters.sendToNewMember }.firstOrNull()", 14 | level = DeprecationLevel.HIDDEN 15 | ) 16 | override var entranceAnnouncement: String 17 | get() = group.impl.groupMemo 18 | set(_) {} 19 | 20 | override var isAllowMemberInvite: Boolean 21 | get() = false // TODO: Not yet implemented 22 | set(_) {} 23 | 24 | override var isAnonymousChatEnabled: Boolean 25 | get() = false // TODO: Not yet implemented 26 | set(value) = runBlocking { 27 | group.bot.impl.setGroupAnonymous(group.id, value) 28 | } 29 | 30 | override val isAutoApproveEnabled: Boolean 31 | get() = false // TODO: Not yet implemented 32 | 33 | internal var muteAll: Boolean 34 | get() = group.impl.groupAllShut == -1 35 | set(value) { 36 | group.impl.groupAllShut = if (value) -1 else 0 37 | } 38 | override var isMuteAll: Boolean 39 | get() = muteAll 40 | set(value) = runBlocking { 41 | group.checkBotPermission(MemberPermission.ADMINISTRATOR) 42 | muteAll = value 43 | group.bot.impl.setGroupWholeBan(group.id, value) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/contact/data/MemberActiveWrapper.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.contact.data 2 | 3 | import net.mamoe.mirai.contact.active.MemberActive 4 | import net.mamoe.mirai.contact.active.MemberMedalInfo 5 | import net.mamoe.mirai.contact.active.MemberMedalType 6 | import net.mamoe.mirai.data.GroupHonorType 7 | import top.mrxiaom.overflow.internal.contact.MemberWrapper 8 | 9 | internal class MemberActiveWrapper( 10 | val member: MemberWrapper 11 | ) : MemberActive { 12 | internal var pointInternal = 0 13 | internal var rankInternal = 1 14 | internal val honorsInternal: HashSet = hashSetOf() 15 | override val honors: Set 16 | get() { 17 | member.group.active 18 | return honorsInternal 19 | } 20 | override val point: Int 21 | get() { 22 | member.group.active 23 | return pointInternal 24 | } 25 | override val rank: Int 26 | get() { 27 | member.group.active 28 | return rankInternal 29 | } 30 | override val temperature: Int 31 | get() = member.impl.levelInt 32 | override suspend fun queryMedal(): MemberMedalInfo { 33 | return member.group.active.queryMemberMedal(uid = member.id) 34 | } 35 | } 36 | internal object EmptyMemberActive : MemberActive { 37 | override val honors: Set = setOf() 38 | override val point: Int = 0 39 | override val rank: Int = 1 40 | override val temperature: Int = 0 41 | override suspend fun queryMedal(): MemberMedalInfo = MemberMedalInfo("", "", MemberMedalType.ACTIVE, setOf()) 42 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/data/ContactInfoImpl.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(LowLevelApi::class) 2 | package top.mrxiaom.overflow.internal.data 3 | 4 | import cn.evolvefield.onebot.sdk.response.contact.FriendInfoResp 5 | import cn.evolvefield.onebot.sdk.response.contact.StrangerInfoResp 6 | import cn.evolvefield.onebot.sdk.response.group.GroupMemberInfoResp 7 | import cn.evolvefield.onebot.sdk.util.ignorable 8 | import com.google.gson.JsonObject 9 | import net.mamoe.mirai.LowLevelApi 10 | import net.mamoe.mirai.contact.MemberPermission 11 | import net.mamoe.mirai.data.FriendInfo 12 | import net.mamoe.mirai.data.GroupHonorType 13 | import net.mamoe.mirai.data.MemberInfo 14 | import net.mamoe.mirai.data.StrangerInfo 15 | 16 | internal class FriendInfoImpl( 17 | override val uin: Long, 18 | override val nick: String, 19 | override var remark: String, 20 | override val friendGroupId: Int = 0 21 | ) : FriendInfo 22 | 23 | internal class StrangerInfoImpl( 24 | override val uin: Long, 25 | override val nick: String, 26 | override val fromGroup: Long = 0, 27 | override val remark: String = "", 28 | ): StrangerInfo 29 | 30 | internal class MemberInfoImpl( 31 | override val honors: Set, 32 | override val isOfficialBot: Boolean, 33 | override val joinTimestamp: Int, 34 | override val lastSpeakTimestamp: Int, 35 | override val muteTimestamp: Int, 36 | override val nameCard: String, 37 | override val permission: MemberPermission, 38 | override val point: Int, 39 | override val rank: Int, 40 | override val specialTitle: String, 41 | override val temperature: Int, 42 | override val nick: String, 43 | override val remark: String, 44 | override val uin: Long 45 | ): MemberInfo 46 | 47 | internal val FriendInfo.asOnebot: FriendInfoResp 48 | get() = FriendInfoResp().apply { 49 | userId = uin 50 | nickname = nick 51 | remark = this@asOnebot.remark 52 | } 53 | internal val StrangerInfo.asOnebot: StrangerInfoResp 54 | get() = StrangerInfoResp().apply { 55 | userId = uin 56 | nickname = nick 57 | ext = JsonObject().also { 58 | if (fromGroup > 0) it.addProperty("add_src_id", fromGroup) 59 | } 60 | } 61 | internal val StrangerInfoResp.asMirai: StrangerInfo 62 | get() = StrangerInfoImpl( 63 | uin = userId, 64 | nick = nickname, 65 | fromGroup = ext.ignorable("add_src_id", 0L), 66 | ) 67 | 68 | internal val GroupMemberInfoResp.asMirai: MemberInfoImpl 69 | get() = MemberInfoImpl(setOf(), false, joinTime, lastSentTime, 0, card, 70 | when(role) { 71 | "owner" -> MemberPermission.OWNER 72 | "admin" -> MemberPermission.ADMINISTRATOR 73 | else -> MemberPermission.MEMBER 74 | }, 0, 0, title, 0, nickname, "", userId) 75 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/data/UserProfileImpl.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.data 2 | 3 | import net.mamoe.mirai.data.UserProfile 4 | 5 | internal class UserProfileImpl( 6 | override val age: Int, 7 | override val email: String, 8 | override val friendGroupId: Int, 9 | override val nickname: String, 10 | override val qLevel: Int, 11 | override val sex: UserProfile.Sex, 12 | override val sign: String 13 | ) : UserProfile 14 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/listener/bot.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.listener 2 | 3 | import cn.evolvefield.onebot.client.handler.EventBus.listen 4 | import cn.evolvefield.onebot.client.handler.EventBus.listenNormally 5 | import cn.evolvefield.onebot.sdk.event.UnsolvedEvent 6 | import cn.evolvefield.onebot.sdk.event.notice.NotifyNoticeEvent 7 | import kotlinx.coroutines.launch 8 | import net.mamoe.mirai.event.broadcast 9 | import net.mamoe.mirai.event.events.NudgeEvent 10 | import top.mrxiaom.overflow.Overflow 11 | import top.mrxiaom.overflow.event.UnsolvedOnebotEvent 12 | import top.mrxiaom.overflow.internal.contact.BotWrapper 13 | import top.mrxiaom.overflow.internal.scope 14 | import top.mrxiaom.overflow.internal.utils.group 15 | 16 | internal fun addBotListeners() { 17 | listen("poke") { e -> 18 | // 戳一戳通知 19 | val operatorId = e.realOperatorId 20 | if (checkId(operatorId, "%onebot 返回了异常的数值 operator_id=%value")) return@listen 21 | // Lagrange: notice -> notify -> poke 不仅仅适用于群聊 22 | if (e.groupId == 0L) { 23 | val operator = bot.getFriend(operatorId) 24 | ?: throw IllegalStateException("好友 $operatorId 戳一戳事件 操作者不在好友列表内") 25 | val target = bot.asFriend // 有的 Onebot 实现不一定提供 target_id,所以直接使用机器人实例 26 | // TODO: 戳一戳无法获取被戳一方的动作、后缀信息 27 | bot.eventDispatcher.broadcastAsync(NudgeEvent(operator, target, operator, "拍了拍", "")) 28 | } else { 29 | val group = bot.group(e.groupId) 30 | val operator = group.queryMember(operatorId) 31 | ?: throw IllegalStateException("群 ${group.id} 戳一戳事件 无法获取操作者") 32 | val target = group.queryMember(e.targetId) 33 | ?: throw IllegalStateException("群 ${group.id} 戳一戳事件 无法获取目标") 34 | // TODO: 戳一戳无法获取被戳一方的动作、后缀信息 35 | bot.eventDispatcher.broadcastAsync(NudgeEvent(operator, target, group, "拍了拍", "")) 36 | } 37 | } 38 | listenNormally { e -> 39 | // 未处理 Onebot 事件 40 | Overflow.scope.launch { 41 | UnsolvedOnebotEvent(e.selfId, e.jsonString, e.timeInSecond()).broadcast() 42 | } 43 | } 44 | } 45 | internal fun BotWrapper.checkId(id: Long, msg: String): Boolean { 46 | return (id <= 0).also { 47 | if (it) logger.warning(msg 48 | .replace("%onebot", "${impl.appName} v${impl.appVersion}") 49 | .replace("%value", id.toString()) 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/message/data/UnknownMessage.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.message.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import net.mamoe.mirai.message.data.AbstractPolymorphicMessageKey 6 | import net.mamoe.mirai.message.data.MessageContent 7 | import net.mamoe.mirai.utils.createFileIfNotExists 8 | import net.mamoe.mirai.utils.safeCast 9 | import java.io.File 10 | import java.text.SimpleDateFormat 11 | import java.util.* 12 | 13 | private fun currentTime() = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date()) 14 | 15 | @Serializable 16 | @SerialName(UnknownMessage.SERIAL_NAME) 17 | internal data class UnknownMessage( 18 | val msgType: String, 19 | val data: String 20 | ) : MessageContent { 21 | fun printLog(): UnknownMessage = apply { 22 | val logFile = File("logs/unknown_messages.log") 23 | logFile.createFileIfNotExists() 24 | logFile.appendText("${currentTime()} I/UnknownMessage: ${contentToString()}\n") 25 | } 26 | 27 | override fun contentToString(): String { 28 | return "[overflow,unknown:$msgType,$data}]" 29 | } 30 | 31 | override fun toString(): String = contentToString() 32 | 33 | public companion object Key : 34 | AbstractPolymorphicMessageKey(MessageContent, { it.safeCast() }) { 35 | public const val SERIAL_NAME: String = "UnknownMessage" 36 | } 37 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/message/data/WrappedAudio.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 2 | package top.mrxiaom.overflow.internal.message.data 3 | 4 | import cn.evolvefield.onebot.client.util.fileType 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | import net.mamoe.mirai.message.data.* 8 | import net.mamoe.mirai.utils.safeCast 9 | import top.mrxiaom.overflow.internal.utils.base64Length 10 | import top.mrxiaom.overflow.internal.utils.lengthToString 11 | import top.mrxiaom.overflow.spi.MediaURLService 12 | import top.mrxiaom.overflow.spi.MediaURLService.Companion.queryAudioUrl 13 | import java.util.* 14 | 15 | @Serializable 16 | @SerialName(WrappedAudio.SERIAL_NAME) 17 | internal data class WrappedAudio( 18 | var url: String, 19 | override val length: Long, 20 | ): OnlineAudio, OfflineAudio { 21 | private val _stringValue: String by lazy(LazyThreadSafetyMode.NONE) { 22 | val fileString = if (file.startsWith("base64://") && file.length > 32) { 23 | val s = file.replace("base64://", "") 24 | val len = base64Length(s) 25 | val type = Base64.getDecoder().decode(s).fileType ?: "*" 26 | "${file.substring(0, 32)}... (${if (type == "*") "" else "$type, "}about ${lengthToString(len)})" 27 | } else file 28 | "[overflow:audio,file=$fileString]" 29 | } 30 | override val codec: AudioCodec = AudioCodec.AMR 31 | override val extraData: ByteArray? = null 32 | override val fileMd5: ByteArray = ByteArray(16) 33 | override val fileSize: Long by lazy { 34 | length.takeIf { it > 0 } ?: if (!file.startsWith("base64://")) 0 35 | else base64Length(file.replace("base64://", "")) 36 | } 37 | override val urlForDownload: String 38 | get() { 39 | val extUrl = MediaURLService.instances.queryAudioUrl(this) 40 | return extUrl ?: url 41 | } 42 | override val filename: String = urlForDownload.substringAfterLast("/") 43 | val file: String = urlForDownload 44 | 45 | override fun toString(): String = _stringValue 46 | 47 | override val key: MessageKey get() = Key 48 | public companion object Key : 49 | AbstractPolymorphicMessageKey(Audio, { it.safeCast() }) { 50 | public const val SERIAL_NAME: String = "WrappedAudio" 51 | } 52 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/message/data/WrappedFileMessage.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.message.data 2 | 3 | import kotlinx.serialization.Serializable 4 | import net.mamoe.mirai.contact.FileSupported 5 | import net.mamoe.mirai.contact.file.AbsoluteFile 6 | import top.mrxiaom.overflow.message.data.FileMessageWithUrl 7 | 8 | @Serializable 9 | internal data class WrappedFileMessage( 10 | override val id: String, 11 | override val internalId: Int, 12 | override val name: String, 13 | override val size: Long, 14 | override val url: String = "" 15 | ) : FileMessageWithUrl { 16 | override suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? { 17 | return contact.files.root.resolveFileById(id, true) 18 | } 19 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/message/data/WrappedImageProtocol.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.message.data 2 | 3 | import cn.evolvefield.onebot.client.util.fileType 4 | import kotlinx.serialization.Serializable 5 | import net.mamoe.mirai.Bot 6 | import net.mamoe.mirai.contact.Contact 7 | import net.mamoe.mirai.message.data.Image 8 | import net.mamoe.mirai.message.data.ImageType 9 | import net.mamoe.mirai.message.data.InternalImageProtocol 10 | import top.mrxiaom.overflow.internal.utils.base64Length 11 | import top.mrxiaom.overflow.internal.utils.lengthToString 12 | import java.util.* 13 | 14 | internal class WrappedImageProtocol : InternalImageProtocol { 15 | override fun createImage( 16 | imageId: String, 17 | size: Long, 18 | type: ImageType, 19 | width: Int, 20 | height: Int, 21 | isEmoji: Boolean 22 | ): Image { 23 | return WrappedImage(imageId, type, size, width, height) 24 | } 25 | 26 | override suspend fun isUploaded( 27 | bot: Bot, 28 | md5: ByteArray, 29 | size: Long, 30 | context: Contact?, 31 | type: ImageType, 32 | width: Int, 33 | height: Int 34 | ): Boolean { 35 | return true 36 | } 37 | } 38 | @Serializable 39 | internal data class WrappedImage( 40 | var url: String, 41 | override val imageType: ImageType, 42 | override val size: Long, 43 | override val width: Int, 44 | override val height: Int, 45 | ): Image { 46 | private val _stringValue: String? by lazy(LazyThreadSafetyMode.NONE) { 47 | val fileString = if (url.startsWith("base64://") && url.length > 32) { 48 | val s = url.replace("base64://", "") 49 | val len = base64Length(s) 50 | val type = Base64.getDecoder().decode(s).fileType ?: "*" 51 | "${url.substring(0, 32)}... (${if (type == "*") "" else "$type, "}about ${lengthToString(len)})" 52 | } else url 53 | "[overflow:image,url=$fileString]" 54 | } 55 | override val imageId: String 56 | get() = url 57 | 58 | override fun contentToString(): String = if (isEmoji) { 59 | "[动画表情]" 60 | } else { 61 | "[图片]" 62 | } 63 | 64 | override fun appendMiraiCodeTo(builder: StringBuilder) { 65 | builder.append("[mirai:image:").append(imageId).append("]") 66 | } 67 | 68 | override fun toString(): String = _stringValue!! 69 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/message/data/WrappedMusicShare.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.message.data 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import kotlinx.serialization.json.* 6 | import net.mamoe.mirai.message.data.MusicKind 7 | import net.mamoe.mirai.message.data.MusicShare 8 | import net.mamoe.mirai.utils.toUHexString 9 | import top.mrxiaom.overflow.internal.message.OnebotMessages.int 10 | import top.mrxiaom.overflow.internal.message.OnebotMessages.string 11 | import java.net.URL 12 | 13 | 14 | internal suspend fun deserializeNeteaseMusic(id: String): MusicShare { 15 | return withContext(Dispatchers.IO) { 16 | val conn = URL("https://music.163.com/api/song/detail/?id=$id&ids=[$id]") 17 | .openConnection().also { it.connect() } 18 | val result = conn.inputStream.use { 19 | it.readBytes().toString(Charsets.UTF_8) 20 | } 21 | val songInfo = Json.parseToJsonElement(result).jsonObject["songs"]!!.jsonArray.first().jsonObject 22 | val title = songInfo["name"].string 23 | val singerName = songInfo["artists"]!!.jsonArray.first().jsonObject["name"].string 24 | val previewUrl = songInfo["album"]!!.jsonObject["picUrl"].string 25 | val playUrl = "https://music.163.com/song/media/outer/url?id=$id.mp3" 26 | val jumpUrl = "https://music.163.com/#/song?id=$id" 27 | MusicShare(MusicKind.NeteaseCloudMusic, title, singerName, jumpUrl, previewUrl, playUrl) 28 | } 29 | } 30 | 31 | internal suspend fun deserializeQQMusic(id: String): MusicShare { 32 | return withContext(Dispatchers.IO) { 33 | val conn = URL("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}}") 34 | .openConnection().also { it.connect() } 35 | val result = conn.inputStream.use { 36 | it.readBytes().toString(Charsets.UTF_8) 37 | } 38 | val songInfo = Json.parseToJsonElement(result).jsonObject["songinfo"]!!.jsonObject.takeIf { it["code"].int != 0 } ?: throw IllegalStateException("QQMusic code = 0") 39 | val data = songInfo["data"]!!.jsonObject 40 | val trackInfo = data["track_info"]!!.jsonObject 41 | val mid = trackInfo["mid"].string 42 | val previewMid = trackInfo["album"]!!.jsonObject["mid"].string 43 | val singerMid = (trackInfo["singer"] as? JsonArray)?.let { 44 | it[0].jsonObject["mid"]?.jsonPrimitive?.contentOrNull 45 | } ?: "" 46 | val title = trackInfo["title"].string 47 | val singerName = trackInfo["singer"]!!.jsonArray.first().jsonObject["name"].string 48 | val vs = (trackInfo["vs"] as? JsonArray)?.let { 49 | it[0].jsonPrimitive.contentOrNull 50 | } ?: "" 51 | val code = "${mid}q;z(&l~sdf2!nK".toByteArray().toUHexString("").substring(0 .. 4).uppercase() 52 | val playUrl = "http://c6.y.qq.com/rsc/fcgi-bin/fcg_pyq_play.fcg?songid=&songmid=$mid&songtype=1&fromtag=50&uin=&code=$code" 53 | val previewUrl = if (vs.isNotEmpty()) { 54 | "http://y.gtimg.cn/music/photo_new/T062R150x150M000$vs}.jpg" 55 | } else if (previewMid.isNotEmpty()) { 56 | "http://y.gtimg.cn/music/photo_new/T002R150x150M000$previewMid.jpg" 57 | } else if (singerMid.isNotEmpty()){ 58 | "http://y.gtimg.cn/music/photo_new/T001R150x150M000$singerMid.jpg" 59 | } else { 60 | "" 61 | } 62 | 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" 63 | 64 | MusicShare(MusicKind.QQMusic, title, singerName, jumpUrl, previewUrl, playUrl) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/message/data/WrappedVideo.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 2 | package top.mrxiaom.overflow.internal.message.data 3 | 4 | import cn.evolvefield.onebot.client.util.fileType 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | import net.mamoe.mirai.message.data.AbstractPolymorphicMessageKey 8 | import net.mamoe.mirai.message.data.MessageKey 9 | import net.mamoe.mirai.message.data.OnlineShortVideo 10 | import net.mamoe.mirai.message.data.ShortVideo 11 | import net.mamoe.mirai.utils.safeCast 12 | import top.mrxiaom.overflow.internal.utils.base64Length 13 | import top.mrxiaom.overflow.internal.utils.lengthToString 14 | import top.mrxiaom.overflow.spi.MediaURLService 15 | import top.mrxiaom.overflow.spi.MediaURLService.Companion.queryVideoUrl 16 | import java.util.* 17 | 18 | @Serializable 19 | @SerialName(WrappedVideo.SERIAL_NAME) 20 | internal data class WrappedVideo( 21 | var file: String, 22 | override var filename: String = if (file.startsWith("base64://")) "base64" else file.substringAfterLast("/"), 23 | override var videoId: String = filename, 24 | ) : OnlineShortVideo { 25 | private val _stringValue: String by lazy(LazyThreadSafetyMode.NONE) { 26 | val fileString = if (urlForDownload.startsWith("base64://") && urlForDownload.length > 32) { 27 | val s = urlForDownload.replace("base64://", "") 28 | val len = base64Length(s) 29 | val type = Base64.getDecoder().decode(s).fileType ?: "*" 30 | "${urlForDownload.substring(0, 32)}... (${if (type == "*") "" else "$type, "}about ${lengthToString(len)})" 31 | } else urlForDownload 32 | "[overflow:video,file=$fileString]" 33 | } 34 | override val fileFormat: String = "mp4" 35 | override val fileMd5: ByteArray = ByteArray(16) 36 | override val fileSize: Long by lazy { 37 | if (!urlForDownload.startsWith("base64://")) 0 38 | else base64Length(urlForDownload.substring(9)) 39 | } 40 | override val urlForDownload: String 41 | get() { 42 | val extUrl = MediaURLService.instances.queryVideoUrl(this) 43 | return extUrl ?: file 44 | } 45 | 46 | override fun contentToString(): String { 47 | return "[视频消息]" 48 | } 49 | 50 | override fun toString(): String = _stringValue 51 | 52 | override val key: MessageKey get() = WrappedAudio 53 | public companion object Key : 54 | AbstractPolymorphicMessageKey(ShortVideo, { it.safeCast() }) { 55 | public const val SERIAL_NAME: String = "WrappedVideo" 56 | } 57 | } -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/utils/Base64FileService.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.utils 2 | 3 | import net.mamoe.mirai.utils.ExternalResource 4 | import top.mrxiaom.overflow.spi.FileService 5 | 6 | /** 7 | * 文件服务默认实现 8 | */ 9 | class Base64FileService : FileService { 10 | override val priority: Int = 1000 11 | override suspend fun upload(res: ExternalResource): String = res.toBase64File() 12 | } 13 | -------------------------------------------------------------------------------- /overflow-core/src/main/kotlin/top/mrxiaom/overflow/internal/utils/RequestManager.kt: -------------------------------------------------------------------------------- 1 | package top.mrxiaom.overflow.internal.utils 2 | 3 | import java.util.concurrent.atomic.AtomicLong 4 | 5 | internal class RequestManager { 6 | val ids = AtomicLong(0L) 7 | val map = mutableMapOf() 8 | 9 | fun put(eventFlag: String): Long { 10 | val id = ids.getAndIncrement() 11 | map[id] = eventFlag 12 | return id 13 | } 14 | 15 | fun get(id: Long): String? { 16 | return map[id] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /overflow-core/src/main/resources/META-INF/services/net.mamoe.mirai.IMirai: -------------------------------------------------------------------------------- 1 | top.mrxiaom.overflow.internal.Overflow -------------------------------------------------------------------------------- /overflow-core/src/main/resources/META-INF/services/net.mamoe.mirai.event.InternalGlobalEventChannelProvider: -------------------------------------------------------------------------------- 1 | net.mamoe.mirai.internal.event.GlobalEventChannelProviderImpl -------------------------------------------------------------------------------- /overflow-core/src/main/resources/META-INF/services/net.mamoe.mirai.message.data.InternalImageProtocol: -------------------------------------------------------------------------------- 1 | top.mrxiaom.overflow.internal.message.data.WrappedImageProtocol -------------------------------------------------------------------------------- /overflow-core/src/main/resources/META-INF/services/net.mamoe.mirai.utils.InternalProtocolDataExchange: -------------------------------------------------------------------------------- 1 | net.mamoe.mirai.internal.utils.MiraiProtocolInternal$Exchange -------------------------------------------------------------------------------- /overflow-core/src/main/resources/META-INF/services/top.mrxiaom.overflow.spi.FileService: -------------------------------------------------------------------------------- 1 | top.mrxiaom.overflow.internal.utils.Base64FileService -------------------------------------------------------------------------------- /overflow-core/src/test/kotlin/RunConsole.kt: -------------------------------------------------------------------------------- 1 | import net.mamoe.mirai.console.ConsoleFrontEndImplementation 2 | import net.mamoe.mirai.console.MiraiConsole 3 | import net.mamoe.mirai.console.terminal.MiraiConsoleImplementationTerminal 4 | import net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader 5 | import net.mamoe.mirai.console.util.ConsoleExperimentalApi 6 | 7 | @OptIn(ConsoleExperimentalApi::class, ConsoleFrontEndImplementation::class) 8 | suspend fun main() { 9 | MiraiConsoleTerminalLoader.startAsDaemon(MiraiConsoleImplementationTerminal()) 10 | MiraiConsole.job.join() 11 | } -------------------------------------------------------------------------------- /overflow-core/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | if (java.util.Locale.getDefault().country == "CN") { 6 | extra["mirror.repo"]?.also(::maven) 7 | } 8 | mavenCentral() 9 | gradlePluginPortal() 10 | } 11 | } 12 | dependencyResolutionManagement { 13 | repositories { 14 | if (java.util.Locale.getDefault().country == "CN") { 15 | extra["mirror.repo"]?.also(::maven) 16 | } 17 | mavenCentral() 18 | } 19 | } 20 | rootProject.name = "Overflow" 21 | 22 | if (System.getProperty("justTasks") == null) { 23 | include(":overflow-core-api") 24 | include(":overflow-core") 25 | include(":overflow-core-all") 26 | } 27 | --------------------------------------------------------------------------------