Commit a9502957 authored by jiahua.liu's avatar jiahua.liu

Merge remote-tracking branch 'origin/master'

parents 6f67edfd 28cdb759
# Mirai # 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)
[![Gitter](https://badges.gitter.im/mamoe/mirai.svg)](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Gitter](https://badges.gitter.im/mamoe/mirai.svg)](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Actions Status](https://github.com/mamoe/mirai/workflows/CI/badge.svg)](https://github.com/mamoe/mirai/actions) [![Actions Status](https://github.com/mamoe/mirai/workflows/CI/badge.svg)](https://github.com/mamoe/mirai/actions)
[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/) [![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
......
# mirai-api-http # mirai-api-http
<b> <b>Mirai-API-http 提供HTTP API供所有语言使用mirai</b>
Mirai-API-http 提供HTTP API供所有语言使用mirai<br>
</b> ### 快速开始
```kotlin
fun main() {
val bot = Bot(123456789, "password")
bot.login()
MiraiHttpAPIServer.start()
bot.network.awaitDisconnection()
}
```
### 开始会话-认证(Authorize) ### 开始会话-认证(Authorize)
``` ```
[POST] /auth [POST] /auth
``` ```
使用此方法验证你的会话连接, 并将这个会话绑定一个BOT<br> 使用此方法验证你的身份,并返回一个会话
注意: 每个会话只能绑定一个BOT.
#### 请求:<br> #### 请求:
```json5
{
"authKey": "U9HSaDXl39ksd918273hU"
}
```
| 名字 | 类型 | 可选 | 举例 | 说明 | | 名字 | 类型 | 可选 | 举例 | 说明 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| key | String |false|U9HSaDXl39ksd918273hU|MIRAI API HTTP key, HTTP API的核心key| | authKey | String |false|"U9HSaDXl39ksd918273hU"|创建Mirai-Http-Server时生成的key,可在启动时指定或随机生成|
| qq | String |false|1040400290|需要绑定的BOT QQ号|
#### 响应: 返回(成功):
#### 返回(成功):<br> ```json5
{
"code": 0,
"session": "UnVerifiedSession"
}
```
| 名字 | 类型 | 举例 | 说明| | 名字 | 类型 | 举例 | 说明|
| --- | --- | --- | --- | | --- | --- | --- | --- |
| code |Int |0|返回状态| | code |Int |0|返回状态|
| session |String |UANSHDKSLAOISN|你的session key| | session |String |"UnVerifiedSession"|你的session key|
#### 状态码:<br> #### 状态码:
| 代码 | 原因| | 代码 | 原因|
| --- | --- | | --- | --- |
| 0 | 正常 | | 0 | 正常 |
| 1 | 错误的MIRAI API HTTP key| | 1 | 错误的MIRAI API HTTP auth key|
| 2 | 试图绑定不存在的bot|
session key 是使用以下方法必须携带的</br> session key 是使用以下方法必须携带的</br>
session key 需要被以cookie的形式上报 <b>cookies</b> : session key 使用前必须进行校验和绑定指定的Bot,**每个Session只能绑定一个Bot,但一个Bot可有多个Session**
| 名字 | 值 |
| --- | --- |
| session |your session key here |
如果出现HTTP 403错误码,代表session key已过期, 需要重新获取
### 校验Session
```
[post] /verify
```
使用此方法校验并激活你的Session,同时将Session与一个**已登录**的Bot绑定
#### 请求:
```json5
{
"sessionKey": "UnVerifiedSession",
"qq": 123456789
}
```
| 名字 | 类型 | 可选 | 举例 | 说明 |
| ---------- | ------ | ----- | ------------------- | -------------------------- |
| sessionKey | String | false | "UnVerifiedSession" | 你的session key |
| qq | Long | false | 123456789 | Session将要绑定的Bot的qq号 |
#### 响应: 返回统一状态码(后续不再赘述)
```json5
{
"code": 0,
"msg": "success"
}
```
| 状态码 | 原因 |
| ------ | ---------------------------------- |
| 0 | 正常 |
| 1 | 错误的auth key |
| 2 | 绑定的Bot不存在 |
| 3 | Session失效或不存在 |
| 4 | Session未认证(未激活) |
| 5 | 发送消息目标不存在(指定对象不存在) |
| 400 | 错误的访问,如参数错误等 |
### 发送好友消息 ### 发送好友消息
...@@ -50,3 +110,198 @@ Mirai-API-http 提供HTTP API供所有语言使用mirai<br> ...@@ -50,3 +110,198 @@ Mirai-API-http 提供HTTP API供所有语言使用mirai<br>
[POST] /sendFriendMessage [POST] /sendFriendMessage
``` ```
使用此方法向指定好友发送消息
#### 请求
```json5
{
"sessionKey": "YourSession",
"target": 987654321,
"messageChain": [
{ "type": "Plain", "text":"hello\n" },
{ "type": "Plain", "text":"world" }
]
}
```
| 名字 | 类型 | 可选 | 举例 | 说明 |
| ------------ | ------ | ----- | ----------- | -------------------------------- |
| sessionKey | String | false | YourSession | 已经激活的Session |
| target | Long | false | 987654321 | 发送消息目标好友的QQ号 |
| messageChain | Array | false | [] | 消息链,是一个消息对象构成的数组 |
#### 响应: 返回统一状态码
```json5
{
"code": 0,
"msg": "success"
}
```
### 发送群消息
```
[POST] /sendGroupMessage
```
使用此方法向指定群发送消息
#### 请求
```json5
{
"sessionKey": "YourSession",
"target": 987654321,
"messageChain": [
{ "type": "Plain", "text":"hello\n" },
{ "type": "Plain", "text":"world" }
]
}
```
| 名字 | 类型 | 可选 | 举例 | 说明 |
| ------------ | ------ | ----- | ----------- | -------------------------------- |
| sessionKey | String | false | YourSession | 已经激活的Session |
| target | Long | false | 987654321 | 发送消息目标群的群号 |
| messageChain | Array | false | [] | 消息链,是一个消息对象构成的数组 |
#### 响应: 返回统一状态码
```json5
{
"code": 0,
"msg": "success"
}
```
### 获取Bot收到的消息
```
[GET] /fetchMessage?sessionKey=YourSessionKey&count=10
```
#### 请求:
| 名字 | 可选 | 举例 | 说明 |
| ---------- | ----- | -------------- | --------------- |
| sessionKey | false | YourSessionKey | 你的session key |
| count | false | 10 | 获取消息的数量 |
#### 响应: 返回JSON对象
```json5
[{
"type": "GroupMessage", // 消息类型:GroupMessage或FriendMessage
"messageChain": [{ // 消息链,是一个消息对象构成的数组
"type": "Plain",
"text": "Miral牛逼"
}],
"sender": { // 发送者信息
"id": 123456789, // 发送者的QQ号码
"memberName": "化腾", // 发送者的群名片
"permission": "MEMBER", // 发送者的群限权:OWNER、ADMINISTRATOR或MEMBER
"group": { // 消息发送群的信息
"id": 1234567890, // 发送群的群号
"name": "Miral Technology" // 发送群的群名称
}
}
},
{
"type": "FriendMessage", // 消息类型:GroupMessage或FriendMessage
"messageChain": [{ // 消息链,是一个消息对象构成的数组
"type": "Plain",
"text": "Miral牛逼"
}],
"sender": { // 发送者信息
"id": 1234567890, // 发送者的QQ号码
"nickName": "", // 发送者的昵称
"remark": "" // 发送者的备注
}
}]
```
### 消息类型一览
#### 消息是构成消息链的基本对象,目前支持的消息类型有
+ [x] At,@消息
+ [x] Face,表情消息
+ [x] Plain,文字消息
+ [ ] Image,图片消息
+ [ ] Xml,Xml卡片消息
+ [ ] 敬请期待
#### At
```json5
{
"type": "At",
"target": 123456,
"display": "@Mirai"
}
```
| 名字 | 类型 | 说明 |
| ------- | ------ | ------------------------- |
| target | Long | 群员QQ号 |
| display | String | @时显示的文本如:"@Mirai" |
#### Face
```json5
{
"type": "Face",
"faceID": 123
}
```
| 名字 | 类型 | 说明 |
| ------ | ---- | ---------- |
| faceID | Int | QQ表情编号 |
#### Plain
```json5
{
"type": "Plain",
"text": "Mirai牛逼"
}
```
| 名字 | 类型 | 说明 |
| ---- | ------ | -------- |
| text | String | 文字消息 |
#### Image
```json5
{
"type": "Image"
// 暂时不支持Image
}
```
| 名字 | 类型 | 说明 |
| ---- | ---- | ---- |
| | | |
#### Xml
```json5
{
"type": "Xml",
"xml": "XML"
}
```
| 名字 | 类型 | 说明 |
| ---- | ------ | ------- |
| xml | String | XML文本 |
\ No newline at end of file
...@@ -18,16 +18,11 @@ object MiraiHttpAPIServer { ...@@ -18,16 +18,11 @@ object MiraiHttpAPIServer {
@UseExperimental(KtorExperimentalAPI::class) @UseExperimental(KtorExperimentalAPI::class)
fun start( fun start(
port: Int = 8080, port: Int = 8080,
authKey: String? = null, authKey: String,
callback: (() -> Unit)? = null callback: (() -> Unit)? = null
) { ) {
authKey?.apply { require(authKey.length in 8..128) { "Expected authKey length is between 8 to 128" }
if (authKey.length in 8..128) { SessionManager.authKey = authKey
SessionManager.authKey = authKey
} else {
logger.error("Expected authKey length is between 8 to 128")
}
}
// TODO: start是无阻塞的,理应获取启动状态后再执行后续代码 // TODO: start是无阻塞的,理应获取启动状态后再执行后续代码
try { try {
......
...@@ -9,7 +9,7 @@ import net.mamoe.mirai.message.MessagePacket ...@@ -9,7 +9,7 @@ import net.mamoe.mirai.message.MessagePacket
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
tailrec fun generateSessionKey():String{ tailrec fun generateSessionKey(): String {
fun generateRandomSessionKey(): String { fun generateRandomSessionKey(): String {
val all = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm" val all = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm"
return buildString(capacity = 8) { return buildString(capacity = 8) {
...@@ -20,27 +20,27 @@ tailrec fun generateSessionKey():String{ ...@@ -20,27 +20,27 @@ tailrec fun generateSessionKey():String{
} }
val key = generateRandomSessionKey() val key = generateRandomSessionKey()
if(!SessionManager.allSession.containsKey(key)){ if (!SessionManager.allSession.containsKey(key)) {
return key return key
} }
return generateSessionKey() return generateSessionKey()
} }
object SessionManager { internal object SessionManager {
val allSession:MutableMap<String,Session> = mutableMapOf() val allSession: MutableMap<String, Session> = mutableMapOf()
lateinit var authKey:String lateinit var authKey: String
fun createTempSession():TempSession = TempSession(EmptyCoroutineContext).also {newTempSession -> fun createTempSession(): TempSession = TempSession(EmptyCoroutineContext).also { newTempSession ->
allSession[newTempSession.key] = newTempSession allSession[newTempSession.key] = newTempSession
//设置180000ms后检测并回收 //设置180000ms后检测并回收
newTempSession.launch{ newTempSession.launch {
delay(180000) delay(180000)
allSession[newTempSession.key]?.run { allSession[newTempSession.key]?.run {
if(this is TempSession) if (this is TempSession)
closeSession(newTempSession.key) closeSession(newTempSession.key)
} }
} }
...@@ -50,15 +50,13 @@ object SessionManager { ...@@ -50,15 +50,13 @@ object SessionManager {
fun containSession(sessionKey: String): Boolean = allSession.containsKey(sessionKey) fun containSession(sessionKey: String): Boolean = allSession.containsKey(sessionKey)
fun closeSession(sessionKey: String) = allSession.remove(sessionKey)?.also {it.close() } fun closeSession(sessionKey: String) = allSession.remove(sessionKey)?.also { it.close() }
fun closeSession(session: Session) = closeSession(session.key) fun closeSession(session: Session) = closeSession(session.key)
} }
/** /**
* @author NaturalHG * @author NaturalHG
* 这个用于管理不同Client与Mirai HTTP的会话 * 这个用于管理不同Client与Mirai HTTP的会话
...@@ -68,20 +66,19 @@ object SessionManager { ...@@ -68,20 +66,19 @@ object SessionManager {
*/ */
abstract class Session internal constructor( abstract class Session internal constructor(
coroutineContext: CoroutineContext coroutineContext: CoroutineContext
): CoroutineScope { ) : CoroutineScope {
val supervisorJob = SupervisorJob(coroutineContext[Job]) val supervisorJob = SupervisorJob(coroutineContext[Job])
final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext
val key:String = generateSessionKey() val key: String = generateSessionKey()
internal open fun close(){ internal open fun close() {
supervisorJob.complete() supervisorJob.complete()
} }
} }
/** /**
* 任何新链接建立后分配一个[TempSession] * 任何新链接建立后分配一个[TempSession]
* *
...@@ -93,10 +90,10 @@ class TempSession internal constructor(coroutineContext: CoroutineContext) : Ses ...@@ -93,10 +90,10 @@ class TempSession internal constructor(coroutineContext: CoroutineContext) : Ses
* 任何[TempSession]认证后转化为一个[AuthedSession] * 任何[TempSession]认证后转化为一个[AuthedSession]
* 在这一步[AuthedSession]应该已经有assigned的bot * 在这一步[AuthedSession]应该已经有assigned的bot
*/ */
class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext):Session(coroutineContext){ class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext) : Session(coroutineContext) {
val messageQueue = MessageQueue() val messageQueue = MessageQueue()
private val _listener : Listener<MessagePacket<*, *>> private val _listener: Listener<MessagePacket<*, *>>
init { init {
bot.subscribeMessages { bot.subscribeMessages {
......
...@@ -18,7 +18,8 @@ data class QQDTO( ...@@ -18,7 +18,8 @@ data class QQDTO(
val remark: String val remark: String
) : ContactDTO() ) : ContactDTO()
suspend fun QQDTO(qq: QQ): QQDTO = QQDTO(qq.id, qq.queryProfile().nickname, qq.queryRemark().value) // TODO: queryProfile.nickname & queryRemark.value not support now
suspend fun QQDTO(qq: QQ): QQDTO = QQDTO(qq.id, "", "")
@Serializable @Serializable
data class MemberDTO( data class MemberDTO(
......
...@@ -59,6 +59,9 @@ internal class QQImpl(bot: QQAndroidBot, override val coroutineContext: Coroutin ...@@ -59,6 +59,9 @@ internal class QQImpl(bot: QQAndroidBot, override val coroutineContext: Coroutin
TODO("not implemented") TODO("not implemented")
} }
override fun equals(other: Any?): Boolean {
return other is QQ && other.id == this.id
}
} }
...@@ -107,6 +110,9 @@ internal class MemberImpl( ...@@ -107,6 +110,9 @@ internal class MemberImpl(
return mute(0) return mute(0)
} }
override fun equals(other: Any?): Boolean {
return other is Member && other.id == this.id
}
} }
...@@ -324,4 +330,8 @@ internal class GroupImpl( ...@@ -324,4 +330,8 @@ internal class GroupImpl(
} }
} }
} }
override fun equals(other: Any?): Boolean {
return other is Group && other.id == this.id
}
} }
\ No newline at end of file
...@@ -19,24 +19,40 @@ import kotlin.contracts.contract ...@@ -19,24 +19,40 @@ import kotlin.contracts.contract
*/ */
interface Contact : CoroutineScope { interface Contact : CoroutineScope {
/** /**
* 这个联系人所属 [Bot] * 这个联系人所属 [Bot].
*/ */
@WeakRefProperty @WeakRefProperty
val bot: Bot // weak ref val bot: Bot // weak ref
/** /**
* 可以是 QQ 号码或者群号码. * 可以是 QQ 号码或者群号码.
*
* 对于 QQ, `uin` 与 `id` 是相同的意思.
* 对于 Group, `groupCode` 与 `id` 是相同的意思.
*/ */
val id: Long val id: Long
/** /**
* 向这个对象发送消息. * 向这个对象发送消息.
*
* 速度太快会被服务器屏蔽(无响应). 在测试中不延迟地发送 6 条消息就会被屏蔽之后的数据包 1 秒左右.
*/ */
suspend fun sendMessage(message: MessageChain) suspend fun sendMessage(message: MessageChain)
/**
* 上传一个图片以备发送.
* TODO: 群图片与好友图片之间是否通用还不确定.
* TODO: 好友之间图片是否通用还不确定.
*/
suspend fun uploadImage(image: ExternalImage): Image suspend fun uploadImage(image: ExternalImage): Image
/**
* 判断 `this` 和 [other] 是否是相同的类型, 并且 [id] 相同.
*
* 注:
* [id] 相同的 [Member] 和 [QQ], 他们并不 [equals].
* 因为, [Member] 含义为群员, 必属于一个群.
* 而 [QQ] 含义为一个独立的人, 可以是好友, 也可以是陌生人.
*/
override fun equals(other: Any?): Boolean
} }
suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.toChain()) suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.toChain())
......
...@@ -9,38 +9,38 @@ import kotlinx.coroutines.CoroutineScope ...@@ -9,38 +9,38 @@ import kotlinx.coroutines.CoroutineScope
* 群. 在 QQ Android 中叫做 "Troop" * 群. 在 QQ Android 中叫做 "Troop"
*/ */
interface Group : Contact, CoroutineScope { interface Group : Contact, CoroutineScope {
/**
* ====以下字段在更新值的时候会自动异步上报服务器更改群信息====
*/
/** /**
* 群名称 * 群名称.
* [可查可改已完成] *
* 在修改时将会异步上传至服务器.
*
* 注: 频繁修改可能会被服务器拒绝
*/ */
var name: String var name: String
/** /**
* 入群公告, 没有时为空字符串 * 入群公告, 没有时为空字符串.
* [可查可改已完成] *
* 在修改时将会异步上传至服务器.
*/ */
var announcement: String var announcement: String
/** /**
* 全体禁言状态 * 全体禁言状态. `true` 为开启.
* [可改已完成] *
*/ * 当前仅能修改状态.
*/// TODO: 2020/2/5 实现 muteAll 的查询
var muteAll: Boolean var muteAll: Boolean
/** /**
* 坦白说状态 * 坦白说状态. `true` 为允许.
* [可查可改已完成] *
* 在修改时将会异步上传至服务器.
*/ */
var confessTalk: Boolean var confessTalk: Boolean
/** /**
* 允许群员拉人状态 * 允许群员邀请好友入群的状态. `true` 为允许
* [可查可改已完成]
*/ */
var allowMemberInvite: Boolean var allowMemberInvite: Boolean
/** /**
* QQ中的自动加群审批 * 自动加群审批
* [可查已完成]
*/ */
val autoApprove: Boolean val autoApprove: Boolean
/** /**
......
...@@ -11,18 +11,18 @@ import kotlin.time.ExperimentalTime ...@@ -11,18 +11,18 @@ import kotlin.time.ExperimentalTime
*/ */
interface Member : QQ, Contact { interface Member : QQ, Contact {
/** /**
* 所在的群 * 所在的群.
*/ */
@WeakRefProperty @WeakRefProperty
val group: Group val group: Group
/** /**
* 权限 * 成员的权限, 动态更新.
*/ */
val permission: MemberPermission val permission: MemberPermission
/** /**
* * 群名片 (如果有) 或个人昵称. 动态更新.
*/ */
var groupCard: String var groupCard: String
...@@ -30,7 +30,7 @@ interface Member : QQ, Contact { ...@@ -30,7 +30,7 @@ interface Member : QQ, Contact {
* 禁言 * 禁言
* *
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常. * @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 若机器人无权限禁言这个群成员, 返回 `false` * @return 仅当机器人无权限禁言这个群成员时返回 `false`
* *
* @see Int.minutesToSeconds * @see Int.minutesToSeconds
* @see Int.hoursToSeconds * @see Int.hoursToSeconds
...@@ -39,10 +39,14 @@ interface Member : QQ, Contact { ...@@ -39,10 +39,14 @@ interface Member : QQ, Contact {
suspend fun mute(durationSeconds: Int): Boolean suspend fun mute(durationSeconds: Int): Boolean
/** /**
* 解除禁言 * 解除禁言. 在没有权限时会返回 `false`. 否则均返回 `true`.
*/ */
suspend fun unmute(): Boolean suspend fun unmute(): Boolean
/**
* 当且仅当 `[other] is [Member] && [other].id == this.id && [other].group == this.group` 时为 true
*/
override fun equals(other: Any?): Boolean
} }
@ExperimentalTime @ExperimentalTime
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
package net.mamoe.mirai.message package net.mamoe.mirai.message
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.IoBuffer
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
...@@ -15,35 +16,47 @@ import net.mamoe.mirai.utils.* ...@@ -15,35 +16,47 @@ import net.mamoe.mirai.utils.*
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
/** /**
* 平台相关扩展 * 一条从服务器接收到的消息事件.
* 请查看各平台的 `actual` 实现的说明.
*/ */
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot) : MessagePacketBase<TSender, TSubject> expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot) : MessagePacketBase<TSender, TSubject>
/**
* 仅内部使用, 请使用 [MessagePacket]
*/ // Tips: 在 IntelliJ 中 (左侧边栏) 打开 `Structure`, 可查看类结构
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
@MiraiInternalAPI @MiraiInternalAPI
abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : EventPacket, BotEvent() { abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : EventPacket, BotEvent() {
/**
* 接受到这条消息的
*/
override val bot: Bot by _bot.unsafeWeakRef() override val bot: Bot by _bot.unsafeWeakRef()
/** /**
* 消息事件主体. * 消息事件主体.
* *
* 对于好友消息, 这个属性为 [QQ] 的实例; * 对于好友消息, 这个属性为 [QQ] 的实例, 与 [sender] 引用相同;
* 对于群消息, 这个属性为 [Group] 的实例 * 对于群消息, 这个属性为 [Group] 的实例, 与 [GroupMessage.group] 引用相同
* *
* 在回复消息时, 可通过 [subject] 作为回复对象 * 在回复消息时, 可通过 [subject] 作为回复对象
*/ */
abstract val subject: TSubject abstract val subject: TSubject
/** /**
* 发送人 * 发送人.
*
* 在好友消息时为 [QQ] 的实例, 在群消息时为 [Member] 的实例
*/ */
abstract val sender: TSender abstract val sender: TSender
/**
* 消息内容
*/
abstract val message: MessageChain abstract val message: MessageChain
// region Send to subject // region 发送 Message
/** /**
* 给这个消息事件的主体发送消息 * 给这个消息事件的主体发送消息
...@@ -64,20 +77,41 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : ...@@ -64,20 +77,41 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
@JvmName("reply1") @JvmName("reply1")
suspend inline fun MessageChain.reply() = reply(this) suspend inline fun MessageChain.reply() = reply(this)
suspend inline fun ExternalImage.send() = this.sendTo(subject) // endregion
// region 上传图片
suspend inline fun ExternalImage.upload(): Image = this.upload(subject) suspend inline fun ExternalImage.upload(): Image = this.upload(subject)
// endregion
// region 发送图片
suspend inline fun ExternalImage.send() = this.sendTo(subject)
suspend inline fun Image.send() = this.sendTo(subject) suspend inline fun Image.send() = this.sendTo(subject)
suspend inline fun Message.send() = this.sendTo(subject) suspend inline fun Message.send() = this.sendTo(subject)
suspend inline fun String.send() = this.toMessage().sendTo(subject) suspend inline fun String.send() = this.toMessage().sendTo(subject)
// endregion
inline fun QQ.at(): At = At(this as Member) /**
* 创建 @ 这个账号的消息. 当且仅当消息为群消息时可用. 否则将会抛出 [IllegalArgumentException]
*/
inline fun QQ.at(): At = At(this as? Member ?: error("`QQ.at` can only be used in GroupMessage"))
// endregion // endregion
// region Image download // region Image download
/**
* 将图片下载到内存.
*
* 非常不推荐这样做.
*/
@Deprecated("内存使用效率十分低下", ReplaceWith("this.download()"), DeprecationLevel.WARNING)
suspend inline fun Image.downloadAsByteArray(): ByteArray = bot.run { downloadAsByteArray() } suspend inline fun Image.downloadAsByteArray(): ByteArray = bot.run { downloadAsByteArray() }
// TODO: 2020/2/5 为下载图片添加文件系统的存储方式
/**
* 将图片下载到内存缓存中 (使用 [IoBuffer.Pool])
*/
suspend inline fun Image.download(): ByteReadPacket = bot.run { download() } suspend inline fun Image.download(): ByteReadPacket = bot.run { download() }
// endregion // endregion
......
...@@ -17,6 +17,8 @@ import kotlin.reflect.KProperty ...@@ -17,6 +17,8 @@ import kotlin.reflect.KProperty
* - 若两个 [MessageChain] 连接, 后一个将会被合并到第一个内. * - 若两个 [MessageChain] 连接, 后一个将会被合并到第一个内.
* - 若一个 [MessageChain] 与一个其他 [Message] 连接, [Message] 将会被添加入 [MessageChain]. * - 若一个 [MessageChain] 与一个其他 [Message] 连接, [Message] 将会被添加入 [MessageChain].
* - 若一个 [Message] 与一个 [MessageChain] 连接, [Message] 将会被添加入 [MessageChain]. * - 若一个 [Message] 与一个 [MessageChain] 连接, [Message] 将会被添加入 [MessageChain].
*
* 要获取更多信息, 请查看 [Message]
*/ */
interface MessageChain : Message, MutableList<Message> { interface MessageChain : Message, MutableList<Message> {
// region Message override // region Message override
......
...@@ -70,6 +70,7 @@ fun ByteReadPacket.debugPrintThis(name: String = ""): ByteReadPacket { ...@@ -70,6 +70,7 @@ fun ByteReadPacket.debugPrintThis(name: String = ""): ByteReadPacket {
@MiraiDebugAPI("Low efficiency") @MiraiDebugAPI("Low efficiency")
@UseExperimental(ExperimentalContracts::class) @UseExperimental(ExperimentalContracts::class)
inline fun <R> Input.debugIfFail(name: String = "", onFail: (ByteArray) -> ByteReadPacket = { it.toReadPacket() }, block: ByteReadPacket.() -> R): R { inline fun <R> Input.debugIfFail(name: String = "", onFail: (ByteArray) -> ByteReadPacket = { it.toReadPacket() }, block: ByteReadPacket.() -> R): R {
contract { contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) callsInPlace(block, InvocationKind.EXACTLY_ONCE)
callsInPlace(onFail, InvocationKind.UNKNOWN) callsInPlace(onFail, InvocationKind.UNKNOWN)
......
...@@ -22,37 +22,52 @@ import java.net.URL ...@@ -22,37 +22,52 @@ import java.net.URL
import javax.imageio.ImageIO import javax.imageio.ImageIO
/** /**
* 一条从服务器接收到的消息事件.
* JVM 平台相关扩展 * JVM 平台相关扩展
*/ */
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual constructor(bot: Bot) : MessagePacketBase<TSender, TSubject>(bot) { actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual constructor(bot: Bot) : MessagePacketBase<TSender, TSubject>(bot) {
// region 上传图片
suspend inline fun uploadImage(image: BufferedImage): Image = subject.uploadImage(image) suspend inline fun uploadImage(image: BufferedImage): Image = subject.uploadImage(image)
suspend inline fun uploadImage(image: URL): Image = subject.uploadImage(image) suspend inline fun uploadImage(image: URL): Image = subject.uploadImage(image)
suspend inline fun uploadImage(image: Input): Image = subject.uploadImage(image) suspend inline fun uploadImage(image: Input): Image = subject.uploadImage(image)
suspend inline fun uploadImage(image: InputStream): Image = subject.uploadImage(image) suspend inline fun uploadImage(image: InputStream): Image = subject.uploadImage(image)
suspend inline fun uploadImage(image: File): Image = subject.uploadImage(image) suspend inline fun uploadImage(image: File): Image = subject.uploadImage(image)
// endregion
// region 发送图片
suspend inline fun sendImage(image: BufferedImage) = subject.sendImage(image) suspend inline fun sendImage(image: BufferedImage) = subject.sendImage(image)
suspend inline fun sendImage(image: URL) = subject.sendImage(image) suspend inline fun sendImage(image: URL) = subject.sendImage(image)
suspend inline fun sendImage(image: Input) = subject.sendImage(image) suspend inline fun sendImage(image: Input) = subject.sendImage(image)
suspend inline fun sendImage(image: InputStream) = subject.sendImage(image) suspend inline fun sendImage(image: InputStream) = subject.sendImage(image)
suspend inline fun sendImage(image: File) = subject.sendImage(image) suspend inline fun sendImage(image: File) = subject.sendImage(image)
// endregion
// region 上传图片 (扩展)
suspend inline fun BufferedImage.upload(): Image = upload(subject) suspend inline fun BufferedImage.upload(): Image = upload(subject)
suspend inline fun URL.uploadAsImage(): Image = uploadAsImage(subject) suspend inline fun URL.uploadAsImage(): Image = uploadAsImage(subject)
suspend inline fun Input.uploadAsImage(): Image = uploadAsImage(subject) suspend inline fun Input.uploadAsImage(): Image = uploadAsImage(subject)
suspend inline fun InputStream.uploadAsImage(): Image = uploadAsImage(subject) suspend inline fun InputStream.uploadAsImage(): Image = uploadAsImage(subject)
suspend inline fun File.uploadAsImage(): Image = uploadAsImage(subject) suspend inline fun File.uploadAsImage(): Image = uploadAsImage(subject)
// endregion 上传图片 (扩展)
// region 发送图片 (扩展)
suspend inline fun BufferedImage.send() = sendTo(subject) suspend inline fun BufferedImage.send() = sendTo(subject)
suspend inline fun URL.sendAsImage() = sendAsImageTo(subject) suspend inline fun URL.sendAsImage() = sendAsImageTo(subject)
suspend inline fun Input.sendAsImage() = sendAsImageTo(subject) suspend inline fun Input.sendAsImage() = sendAsImageTo(subject)
suspend inline fun InputStream.sendAsImage() = sendAsImageTo(subject) suspend inline fun InputStream.sendAsImage() = sendAsImageTo(subject)
suspend inline fun File.sendAsImage() = sendAsImageTo(subject) suspend inline fun File.sendAsImage() = sendAsImageTo(subject)
// endregion 发送图片 (扩展)
// region 下载图片 (扩展)
suspend inline fun Image.downloadTo(file: File): Long = file.outputStream().use { downloadTo(it) } suspend inline fun Image.downloadTo(file: File): Long = file.outputStream().use { downloadTo(it) }
/** /**
* 这个函数结束后不会关闭 [output] * 这个函数结束后不会关闭 [output]. 请务必解决好 [OutputStream.close]
*/ */
suspend inline fun Image.downloadTo(output: OutputStream): Long = suspend inline fun Image.downloadTo(output: OutputStream): Long =
download().inputStream().use { input -> withContext(Dispatchers.IO) { input.copyTo(output) } } download().inputStream().use { input -> withContext(Dispatchers.IO) { input.copyTo(output) } }
...@@ -60,4 +75,5 @@ actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual con ...@@ -60,4 +75,5 @@ actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual con
suspend inline fun Image.downloadAsStream(): InputStream = download().inputStream() suspend inline fun Image.downloadAsStream(): InputStream = download().inputStream()
suspend inline fun Image.downloadAsExternalImage(): ExternalImage = withContext(Dispatchers.IO) { download().toExternalImage() } suspend inline fun Image.downloadAsExternalImage(): ExternalImage = withContext(Dispatchers.IO) { download().toExternalImage() }
suspend inline fun Image.downloadAsBufferedImage(): BufferedImage = withContext(Dispatchers.IO) { ImageIO.read(downloadAsStream()) } suspend inline fun Image.downloadAsBufferedImage(): BufferedImage = withContext(Dispatchers.IO) { ImageIO.read(downloadAsStream()) }
// endregion
} }
\ No newline at end of file
package demo.gentleman package demo.gentleman
import com.alibaba.fastjson.JSON import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONObject
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.uploadAsImage import net.mamoe.mirai.message.uploadAsImage
import org.jsoup.Jsoup import org.jsoup.Jsoup
import kotlin.random.Random
class GentleImage { class GentleImage(val contact: Contact, val keyword: String) {
lateinit var contact: Contact
// `Deferred<Image?>` causes a runtime ClassCastException
val image: Deferred<Image> by lazy { getImage(0) } val image: Deferred<Image> by lazy { getImage(0) }
...@@ -18,18 +17,21 @@ class GentleImage { ...@@ -18,18 +17,21 @@ class GentleImage {
fun getImage(r18: Int): Deferred<Image> { fun getImage(r18: Int): Deferred<Image> {
return GlobalScope.async { return GlobalScope.async {
withTimeoutOrNull(5 * 1000) { withTimeoutOrNull(10 * 1000) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val result = val result =
JSON.parseObject( JSON.parseObject(
Jsoup.connect("https://api.lolicon.app/setu/?r18=$r18").ignoreContentType(true).timeout( Jsoup.connect("https://api.lolicon.app/setu/?r18=$r18" + if (keyword.isNotBlank()) "&keyword=$keyword&num=100" else "").ignoreContentType(
true
).timeout(
10_0000 10_0000
).get().body().text() ).get().body().text()
) )
val url: String val url: String
val pid: String val pid: String
with(result.getJSONArray("data").getJSONObject(0)) { val data = result.getJSONArray("data")
with(JSONObject(data.getJSONObject(Random.nextInt(0, data.size)))) {
url = this.getString("url") url = this.getString("url")
pid = this.getString("pid") pid = this.getString("pid")
} }
......
...@@ -18,22 +18,20 @@ private const val IMAGE_BUFFER_CAPACITY: Int = 5 ...@@ -18,22 +18,20 @@ private const val IMAGE_BUFFER_CAPACITY: Int = 5
@ExperimentalUnsignedTypes @ExperimentalUnsignedTypes
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
object Gentlemen : MutableMap<Long, Gentleman> by mutableMapOf() { object Gentlemen : MutableMap<Long, Gentleman> by mutableMapOf() {
fun provide(key: Contact): Gentleman = this.getOrPut(key.id) { Gentleman(key) } fun provide(key: Contact, keyword: String = ""): Gentleman = this.getOrPut(key.id) { Gentleman(key, keyword) }
} }
/** /**
* 工作是缓存图片 * 工作是缓存图片
*/ */
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class Gentleman(private val contact: Contact) : Channel<GentleImage> by Channel(IMAGE_BUFFER_CAPACITY) { class Gentleman(private val contact: Contact, private val keyword: String) : Channel<GentleImage> by Channel(IMAGE_BUFFER_CAPACITY) {
init { init {
GlobalScope.launch { GlobalScope.launch {
while (!isClosedForSend) { while (!isClosedForSend) {
send(GentleImage().apply { send(GentleImage(contact, keyword).apply {
contact = this@Gentleman.contact seImage// start downloading
image// start downloading
}) })
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment