Browse Source

Fixed image

Him188 6 years ago
parent
commit
b6f54dbb7b

+ 265 - 42
README.md

@@ -1,64 +1,287 @@
 # Mirai
 [![HitCount](http://hits.dwyl.io/him188/mamoe/mirai.svg)](http://hits.dwyl.io/him188/mamoe/mirai) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/7d0ec3ea244b424f93a6f59038a9deeb)](https://www.codacy.com/manual/Him188/mirai?utm_source=github.com&utm_medium=referral&utm_content=mamoe/mirai&utm_campaign=Badge_Grade)
 
-一个以 **TIM PC协议(非web)** 驱动的跨平台QQ机器人服务端核心, 虽然目前仅支持 JVM  
-采用服务端-插件模式运行,同时提供独立的跨平台核心库.  
-Mirai 的所有模块均开源
+一个以 **TIM PC协议(非web)** 驱动的跨平台开源 QQ 机器人服务端核心, 目前仅支持 JVM  
+Mirai 在 JVM 平台采用插件模式运行,同时提供独立的跨平台核心库.  
+未来会在 Native(Win32) 平台提供目前比较流行的几种机器人软件的 API 转接  
   
-项目处于开发阶段, 还有很多未完善的地方. 欢迎任何的代码贡献, 或是 issue.  
+若您有任何意见或建议, 欢迎提交 issue.  
+
 部分协议来自网络上开源项目  
+
 **一切开发旨在学习,请勿用于非法用途**
 
-## 抢先体验  
-核心框架结构已经开发完毕,一些核心功能也测试完成。  
-仅需几分钟就可以测试 Mirai.  
+## Try
+
+现在您可以开始体验低付出高效率的 Mirai
 
-目前还没有写构建,请使用 IDE 运行单个 main 函数。
 1. Clone
 2. Import as Gradle project
-3. Run demo main [Demo 1 Main](mirai-demos/mirai-demo-1/src/main/java/demo1/Main.kt#L22)
+3. Run demo main [Demo 1 Main](mirai-demos/mirai-demo-1/src/main/java/demo/subscribe/SubscribeSamples.kt)
 
-### 事件
+**转到[开发文档](#Development-Guide---Kotlin)**
 
-#### 使用 Kotlin
-这里只演示进行不终止地监听。
-##### Top-level reified
-多数情况下这是最好的方式。
-```kotlin
-inline fun <reified E: Event> subscribeAlways(handler: (E) -> Unit)
+## Update log
+
+- 发送好友/群消息(10/14)
+- 接受解析好友消息(10/14)
+- 接收解析群消息(10/14)
+  - 成员昵称(10/18)
+  - 成员权限(10/18, 计划优化)
+- 好友在线状态改变(10/14)
+- Android客户端上线/下线(10/18)
+- 上传并发送好友/群图片(10/21, 10/26)
+
+计划中: 添加好友
+
+## Requirements
+
+所有平台: 
+- Kotlin 1.3.50
+
+JVM 平台:
+- Java 8
+
+#### Libraries used
+Mirai 使用以下开源库:
+- kotlin-stdlib
+- kotlinx-coroutines
+- kotlinx-io
+- kotlin-reflect
+- pcap4j
+- atomicfu
+- ktor
+- klock
+- tornadofx
+- javafx
+
+## Development Guide - Kotlin
+
+平台通用开发帮助(不含协议层).   
+
+您需要有一定 Kotlin 基础才能读懂以下内容.  
+若您对本文档有建议, 请告诉我们      
+
+目录:  
+- [Introduction](#Introduction) Mirai 介绍
+- [Modules](#Modules) 模块介绍
+  - [mirai-core](#mirai-core) 核心模块
+  - [mirai-console](#mirai-console) JVM 控制台
+  - [mirai-demo](#mirai-demo) 示例和演示程序
+  - [mirai-debug](#mirai-debug) 抓包工具和分析工具\
+- [Logger](#Logger) 日志系统
+- [Bot](#Bot) 机器人类
+- [Contact](#Contact) 联系人
+- [Message](#Message) 消息
+  - [MessageChain](#MessageChain) `MessageChain`
+  - [Types](#Types) 消息类型
+  - [Operators](#Operators) `Message` 一般用法
+  - [Extensions](#Extensions) `Message` 的常用扩展方法
+- [Image](#Image) 图片
+  - [Image JVM](#Image-JVM) JVM 平台扩展实现
+- [Event](#Event) 事件
+  - [Subscription](#Subscription) 事件监听(订阅) 
+  - [Message Event](#Message-Event) 针对消息事件的订阅实现
+
+### Introduction 
+
+Mirai 目前为快速流转(Moving fast)状态, 增量版本之间可能不具有兼容性,任何功能都可能在没有警告的情况下添加、删除或者更改。
+
+### Modules
+Mirai 的模块组成
+
+#### mirai-core
+Mirai 的核心部分.
+
+- 独立的跨平台设计, 可以被以库的形式内置在任意项目内.
+- 现有 JVM 支持
+- 未来计划 Android, Native 支持 
+
+#### mirai-console
+- 仅 JVM 平台
+- 仅命令行
+- Jar 插件支持
+
+#### mirai-demo
+Samples and demos.
+目前仅有 [SubscribeSamples](mirai-demos/mirai-demo-1/src/main/java/demo/subscribe/SubscribeSamples.kt)
+
+#### mirai-debug
+抓包工具和分析工具. 不会进行稳定性维护.  
+
+- 抓包自动解密和分析
+- Hex 着色比较器
+- GUI Hex 调试器(值转换)
+
+### Logger
+[Contact](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/MiraiLogger.kt)  
+Mirai 维护跨平台日志系统, 针对平台的实现为 `expect class PlatformLogger`,  
+一般推荐使用顶层的 `var DefaultLogger: (identity: String?) -> PlatformLogger` 通过 `DefaultLogger( ... )` 来创建日志记录器.  
+每个 `Bot` 都拥有一个日志记录器, 可通过 `Bot.logger` 获取
+
+-日志记录尚不完善, 以后可能会修改-
+
+### Bot
+[Bot](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt) 为机器人  
+一个机器人实例只有一个账号.  
+一个机器人实例由多个模块构成.  
+- `BotNetworkHandler` (管理所有网络方面事务, 本文不介绍)
+- `ContactSystem` (管理联系人, 维护一个 `QQ` 列表和一个 `Group` 列表)
+
+Mirai 能同时维护多个机器人账号.
+
+[BotHelper](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotHelper.kt) 中存在一些快捷方法  
+
+### Contact
+[Contact](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt) 为联系人.  
+虽是联系人, 但它包含 `QQ` 和 `Group`.  
+联系人并不是独立的, 它必须隶属于某个 `Bot`  
+
+**共有方法**:  
+- `sendMessage`(`String`|`Message`|`MessageChain`)
+
+**共有属性**:
+- id (即 QQ 号和群号)
+
+注: 为减少出错概率, 联系人的 `id` 均使用无符号整型 `UInt`, 这是 Kotlin 1.3 的一个实验性类型  
+我们建议您在开发中也使用 `UInt`, 以避免产生一些难以发现的问题
 
+### Message
+Mirai 中所有的消息均为对象化的 [Message](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/Message.kt)  
+实际上, 所有的 `Message` 都是 `inline class`, 保证无性能损失的前提下又不失使用的严谨性和便捷性.  
+`Message` 有大量扩展和相关函数. 本文只介绍使用较多的一部分. 其他函数您也将会在实际开发中通过注释指引了解到.    
+
+#### MessageChain
+
+一条消息为一个 `MessageChain` 对象.  
+`MessageChain` 也是 `Message` 的一种  
+`MessageChain` 实现 `MutableList` 接口.  
+它有多种实现:
+- `inline class MessageChainImpl` 通常的 `MutableList<Message>` 实现
+- `inline class SingleMessageChain` 单个消息的不可变代表包装
+- `object NullMessageChain` 空的不可变实现. 用于替代 `null` 情况  
+
+仅 `NullMessageChain` 是公开(public)的. 在开发中无需考虑另外两个的存在, 他们将会在 Mirai 内部合适地使用.
+
+#### Types 
+现支持的消息类型:  
+- `PlainText` 纯文本
+- `Image` 图片 (将会有独立章节来说明图片的上传等)
+- `Face` 表情 (QQ 自带表情)
+
+计划中:  
+- `At` (仅限群, 将会被 QQ 显示为蓝色的连接)
+- `XML`
+- `File` (文件上传)
+
+#### Operators
+
+| 操作表示   |  说明  |
+|---| ---|
+| Message + Message | 连接 `Message`, 得到 `MessageChain`  |
+| Message + String | 连接 `Message` 与 `String`(`PlainText`) 为 `MessageChain` |
+| Message eq String | 可读字符串如 "\[@10000\]" 判断 |
+| String in Message | 内容包含判断 |
+
+#### Extensions
+
+| 扩展方法   |  说明  |
+|---| ---|
+|String.toChain():MessageChain| PlainText(this) |
+|Message.toChain():MessageChain| 构造上文提到的 SingleMessageChain |
+|suspend Message.sendTo(Contact)| 发送给联系人 |
+
+### Image
+考虑到协议需求和内存消耗, Mirai 的所有 API 均使用 [ExternalImage](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt)
+`ExternalImage` 包含图片长宽、大小、格式、文件数据
+您只需通过扩展函数处理图片.
+
+| 扩展函数   |  说明  |
+|---| ---|
+|suspend ExternalImage.sendTo(Contact)| 上传图片并以纯图片消息发送给联系人 |
+|suspend ExternalImage.upload():Image | 上传图片并得到 [Image] 消息 |
+|suspend Contact.sendImage(ExternalImage) | 上传图片并发送给指定联系人 |
+
+注: 使用 `upload` 而不是 `toMessage` 作为函数名是为了强调它是一个耗时的过程.
+
+#### Image JVM
+
+对于 JVM 平台, Mirai 提供额外的足以应对大多数情况的扩展函数:  
+[ExternalImageJvm](mirai-core/src/jvmMain/kotlin/net.mamoe.mirai/utils/ExternalImageJvm.kt)  
+若有必要, 这些函数将会创建临时文件以避免使用内存缓存图片  
+一下内容中, `IMAGE` 可替换为 `ExternalImage`, `BufferedImage`, `File`, `InputStream`, `URL` 或 `Input` (来自 `kotlinx.io`) 
+
+转为 `ExternalImage`  
+- `suspend IMAGE.toExternalImage():ExternalImage`
+
+直接发送  
+- `suspend IMAGE.sendTo(Contact)`
+- `suspend Contact.sendImage(IMAGE)`
+
+转为 Message  
+- `suspend IMAGE.upload(Contact)`
+- `suspend Contact.upload(IMAGE)`
+
+只要语义上正确的函数, 在 Mirai 都是可行的.
+
+### Event
+
+#### Subscription
+
+[查看相关监听代码](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt)  
+ 
+您可以通过顶层 (top-level) 方法 `subscribeXXX` 对某个事件进行监听, 其中 `XXX` 可以是   
+ - Always (不断监听)
+ - Once (一次监听)
+ - Until / While (条件监听)  
+
+例:    
+```kotlin
 subscribeAlways<FriendMessageEvent>{
   //it: FriendMessageEvent
 }
 ```
 
-![AYWVE86P](.github/A%7DYWVE860U%28%25YQD%24R1GB1%5BP.png)
-
-### 图片测试
-现在可以接收图片消息(并解析为消息链):  
-![JsssF](.github/J%5DCE%29IK4BU08%28EO~UVLJ%7B%5BF.png)  
-![](.github/68f8fec9.png)
+#### Message Event
 
-上传发送图片已经完成, 您可以在 Demo 中找到发送方式.  
-机器人可以转发图片消息.详情查看 [Image.kt](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/Message.kt#L81)
+对于消息事件, Mirai 还提供了更强大的 DSL 监听方式.  
+[MessageSubscribersBuilder](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/MessageSubscribers.kt#L140)  
+可用条件方法为:  
+ - case (内容相等) 
+ - contains
+ - startsWith
+ - endsWith
+ - sentBy (特定发送者)
 
-## 现已支持
+```kotlin
+// 监听所有群和好友消息
+subscribeMessages {// this: MessageSubscribersBuilder
+  case("你好"){
+    // this: SenderAndMessage
+    // message: MessageChain
+    // sender: QQ
+    // it: String (来自 MessageChain.toString)
+    // group: Group (如果是群消息)
+    reply("你好!")// reply将发送给这个事件的主体(群消息的群, 好友消息的好友)
+  }
+  
+  replyCase("你好"){ "你好!" } // lambda 的返回值将会作为回复消息
+  
+  "Hello" reply "World" // 收到 "Hello" 回复 "World"
+}
+```
 
-- 发送好友/群消息(10/14)
-- 接受解析好友消息(10/14)
-- 接收解析群消息(10/14)
-  - 成员权限, 昵称(10/18)
-- 好友在线状态改变(10/14)
-- Android客户端上线/下线(10/18)
-- 上传并发送图片(10/21)
-
-## 使用方法
-### 要求
-  - Kotlin 1.3+
-#### 用于 JVM 平台
- - Java 8
-## 插件开发
-``` text
-    to be continued
-    ...
+当然, 您也可以仅监听来自群或好友的消息      
+```kotlin
+// 监听所有好友消息
+subscribeFriendMessages {  }
+//监听所有群消息
+subscribeGroupMessages {  }
 ```
+
+另外, 由于 Mirai 可同时维护多个机器人账号, Mirai 也提供了对单个机器人的事件的监听.  
+为了限制只监听来自某个机器人账号的事件, 您只需要在 `subscribeMessages` 前添加 `bot.` 将其修改为调用扩展方法.  
+例:    
+```kotlin
+bot.subscribeMessages {  }
+```

+ 4 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/internal/MessageDataInternal.kt

@@ -27,8 +27,9 @@ internal fun IoBuffer.parseLongText0x19(): PlainText {
     //01  00  59  AA  02  56  30  01  3A  40  6E  35  46  4F  62  68  75  4B  6F  65  31  4E  63  45  41  6B  77  4B  51  5A  5A  4C  47  54  57  43  68  30  4B  56  7A  57  44  38  67  58  70  37  62  77  6A  67  51  69  66  66  53  4A  63  4F  69  78  4F  75  37  36  49  49  4F  37  48  32  55  63  9A  01  0F  80  01  01  C8  01  00  F0  01  00  F8  01  00  90  02  00  14  01  75  01  01  6B  01  78  9C  CD  92  BB  4E  C3  30  14  86  77  9E  C2  32  73  DA  A4  21  24  48  4E  AA  F4  06  A5  B4  51  55  A0  A8  0B  4A  5D  27  35  E4  82  72  69  4B  B7  6E  08  06  C4  C0  06  42  48  30  20  21  60  62  EB  E3  34  F4  31  70  4A  11  23  23  FC  96  2C  F9  D8  BF  CF  F1  77  8C  F2  23  D7  01  03  12  84  D4  F7  54  28  64  78  08  88  87  FD  1E  F5  6C  15  C6  91  C5  29  30  AF  AD  00  26  E4  86  36  E8  06  94  58  2A  CC  FC  73  41  E0  1E  5A  D4  21  0D  D3  25  2A  2C  55  0A  1B  D2  BA  5E  E0  24  91  D7  B9  B5  72  41  E1  74  B9  5C  E0  78  25  27  8B  92  28  14  45  45  FF  76  B4  E8  98  39  18  05  13  47  0B  24  03  4A  86  F5  D8  89  68  3D  B4  21  B0  1C  93  71  11  21  08  49  30  A0  98  54  4B  6C  25  A5  E6  80  84  B4  A7  42  4F  AA  18  DD  7E  5C  F3  89  D0  C0  65  FD  78  58  6B  76  3A  3B  9B  BB  ED  62  9F  AF  ED  8F  DB  25  C5  3E  38  91  BB  C3  23  BB  49  2D  AB  B5  8D  0D  3A  32  62  79  BD  5A  35  E4  AD  DC  1E  86  40  03  88  46  C4  05  8E  79  EA  C7  11  EB  09  64  91  88  46  0E  D1  C0  5F  73  FD  4D  00  65  97  95  02  D4  0F  34  94  65  D3  B2  78  80  7D  C7  0F  54  B8  AA  F0  E9  60  8F  4A  EE  1E  3F  6E  2E  84  E4  F6  7E  3E  7D  9E  5D  5E  25  EF  67  C9  E4  15  FC  DC  81  B2  29  08  0D  85  7E  1C  60  02  BC  45  33  E7  93  F3  D9  C3  D3  FC  E5  6D  36  BD  86  2C  C3  D7  66  7A  98  FD  4F  ED  13  9B  C7  C1  78  02  00  04  00  00  00  23  0E  00  07  01  00  04  00  00  00  09
     discardExact(1)//0x01
     val raw = readLVByteArray()
+    //TODO 这应该是手机发送时的字体或气泡之类的
     println("parseLongText0x19.raw=${raw.toUHexString()}")
-    return PlainText(raw.toUHexString())
+    return PlainText("")
 }
 
 internal fun IoBuffer.parseMessageImage0x06(): Image {
@@ -100,6 +101,8 @@ internal fun ByteReadPacket.readMessage(): Message? {
                 println("0x14的未知压缩的data=" + value.toUHexString())
                 //todo 未知压缩算法
 
+                return PlainText("")
+
                 //后面似乎还有一节?
                 //discardExact(7)//02  00  04  00  00  00  23
                 return PlainText(value.toUHexString())

+ 2 - 0
mirai-demos/mirai-demo-gentleman/build.gradle

@@ -8,4 +8,6 @@ dependencies {
 
     compile group: 'com.alibaba', name: 'fastjson', version: '1.2.62'
     implementation 'org.jsoup:jsoup:1.12.1'
+
+    implementation files('./lib/ExImageGallery.jar')
 }

BIN
mirai-demos/mirai-demo-gentleman/lib/ExImageGallery.jar


+ 115 - 4
mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Gentleman.kt

@@ -1,10 +1,23 @@
 package demo.gentleman
 
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.launch
+import net.mamoe.ex.content.RandomAccessHDImage
+import net.mamoe.ex.network.ExNetwork
+import net.mamoe.ex.network.connections.defaults.DownloadHDImageStreamSpider
+import net.mamoe.ex.network.connections.defaults.ExIPWhiteListSpider
 import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.QQ
+import net.mamoe.mirai.message.sendTo
+import net.mamoe.mirai.message.uploadImage
+import net.mamoe.robot.AsyncTaskPool
+import java.io.Closeable
+import java.io.InputStream
+import java.util.*
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Future
 
 
 /**
@@ -23,6 +36,104 @@ object Gentlemen : MutableMap<UInt, Gentleman> by mutableMapOf() {
     fun getOrPut(key: Contact): Gentleman = this.getOrPut(key.id) { Gentleman(key) }
 }
 
+
+private val sessionMap = LinkedHashMap<Long, HPictureSession>()
+
+val ERROR_LINK = "https://i.loli.net/2019/08/05/usINjXSiZxrQJkT.jpg"
+val TITLE_PICTURE_LINK = "https://i.loli.net/2019/08/04/B5ZMw1rdzVQI7Yv.jpg"
+
+var minstar = 100
+
+class HPictureSession constructor(private val group: Group, private val sender: QQ, val keyword: String) : Closeable {
+
+    private var hdImage: RandomAccessHDImage? = null
+    var sentCount: Int = 0
+        set(sentCount) {
+            field = this.sentCount
+        }//已经发送了几个 ImageSet
+
+    private var fetchTask: Future<RandomAccessHDImage>? = null
+
+    init {
+        AsyncTaskPool.submit {
+            try {
+                Thread.sleep((1000 * 60 * 10).toLong())//10min
+            } catch (ignored: InterruptedException) {
+            }
+
+            close()
+        }
+    }
+
+    init {
+        GlobalScope.launch { reloadImage() }
+    }
+
+    private suspend fun reloadImage() {
+        if (keyword.isEmpty()) {
+            group.sendMessage("正在搜寻随机色图")
+        } else {
+            group.sendMessage("正在搜寻有关 $keyword 的色图")
+        }
+        try {
+            withContext(IO) {
+                if (!ExNetwork.doSpider(ExIPWhiteListSpider()).get()) {
+                    group.sendMessage("无法连接EX")
+                    close()
+                    return@withContext
+                }
+            }
+        } catch (e: InterruptedException) {
+            e.printStackTrace()
+            close()
+            return
+        } catch (e: ExecutionException) {
+            e.printStackTrace()
+            close()
+            return
+        }
+
+        this.fetchTask = ExNetwork.getRandomImage(keyword, minstar) { value ->
+            this.hdImage = value
+            if (this.hdImage == null) {
+                runBlocking { group.sendMessage("没找到") }
+                close()
+            } else {
+                with(this.hdImage!!) {
+                    if (this.picId != null) {
+                        runBlocking {
+                            group.sendMessage(picId)
+                        }
+                    } else {
+                        AsyncTaskPool.submit {
+                            try {
+                                runBlocking {
+                                    group.uploadImage(ExNetwork.doSpider(DownloadHDImageStreamSpider(this@with)).get() as InputStream).sendTo(group)
+                                }
+                            } catch (var7: Exception) {
+                                var7.printStackTrace()
+                            }
+
+                        }
+                    }
+                }
+
+            }
+        }
+    }
+
+    override fun close() {
+        this.hdImage = null
+        if (this.fetchTask != null) {
+            if (!this.fetchTask!!.isCancelled && !this.fetchTask!!.isDone) {
+                this.fetchTask!!.cancel(true)
+            }
+        }
+        this.fetchTask = null
+        sessionMap.entries.removeIf { longHPictureSessionEntry -> longHPictureSessionEntry.value === this }
+    }
+}
+
 @ExperimentalCoroutinesApi
 class Gentleman(private val contact: Contact) : Channel<GentleImage> by Channel(IMAGE_BUFFER_CAPACITY) {
     init {
@@ -30,7 +141,7 @@ class Gentleman(private val contact: Contact) : Channel<GentleImage> by Channel(
         GlobalScope.launch {
             while (!isClosedForSend) {
                 send(GentleImage().apply {
-                    sample_url = "http://dev.itxtech.org:10322/randomImg.uue?tdsourcetag=s_pctim_aiomsg"
+                    sample_url = "http://dev.itxtech.org:10322/randomImg.uue?tdsourcetag=s_pctim_aiomsg&size=large"
                     contact = [email protected]
 
                     image.await()

+ 24 - 9
mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt

@@ -2,12 +2,14 @@
 
 package demo.gentleman
 
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.BotAccount
+import net.mamoe.mirai.event.subscribeGroupMessages
 import net.mamoe.mirai.event.subscribeMessages
 import net.mamoe.mirai.login
-import net.mamoe.mirai.message.Image
-import net.mamoe.mirai.message.ImageId
 import net.mamoe.mirai.network.protocol.tim.packet.login.requireSuccess
 import java.io.File
 
@@ -37,17 +39,30 @@ suspend fun main() {
     bot.subscribeMessages {
         "你好" reply "你好!"
 
-        startsWith("发送图片", removePrefix = true) {
-            reply(Image(ImageId(it)))
+        startsWith("随机色图", removePrefix = true) {
+            withContext(Dispatchers.Default) {
+                try {
+                    repeat(it.toIntOrNull() ?: 1) {
+                        launch {
+                            Gentlemen.getOrPut(subject).receive().image.await().send()
+                        }
+                    }
+                } catch (e: Exception) {
+                    reply(e.message ?: "exception: null")
+                }
+            }
         }
 
-        case("随机色图") {
-            Gentlemen.getOrPut(subject).receive().image.await().send()
-        }
+    }
 
-        "色图" caseReply {
+    bot.subscribeGroupMessages {
+        startsWith("色图", removePrefix = true) {
+            HPictureSession(group, sender, it)
+        }
 
-            ""
+        startsWith("minstar=", removePrefix = true) {
+            minstar = it.toInt()
+            reply("minStar set to $minstar")
         }
     }