Commit 205432db authored by Him188's avatar Him188

Remove old files

parent 69abf7a5
This diff is collapsed.
# mirai-api-http
<b>
Mirai-API-http provides adapter for ALL langugae to access mirai via HTTP protocol.<br>
</b>
**[中文](README_CH.md)**
### Start Session-Authorize
```php
Path: /auth
Method: POST
```
this verify your session to one bot and you could have full access to that bot<br>
NOTE that only 1 bot could be control under 1 session, you could have multiple session to control all bots.
#### Request:<br>
| name | type | optional|example|note|
| --- | --- | --- | --- | --- |
| key | String |false|U9HSaDXl39ksd918273hU|MIRAI API HTTP key, this could be found after initialize|
| qq | String |false|1040400290|bot QQ number you want to access|
#### Response if success:<br>
| name | type | example|note|
| --- | --- | --- | --- |
| success |Boolean |true|if this session is authorized|
| session |String |UANSHDKSLAOISN|your session key|
#### Response if failed:<br>
| name | type | example|note|
| --- | --- | --- | --- |
| success |Boolean |false|if this session is authorized|
| session |String |null|your session key|
| error |int |0|error code|
#### Error:<br>
| code | reason|
| --- | --- |
| 0 | wrong MIRAI API HTTP key |
| 1 | unknown bot number |
without session key, you are not able to access any method below.</br>
session key should be attached to your <b>cookies</b> like this:
| name | value |
| --- | --- |
| session |your session key here |
if you were getting HTTP error code 403, you should ask for a new session key.
\ No newline at end of file
This diff is collapsed.
@file:Suppress("UNUSED_VARIABLE")
plugins {
id("kotlinx-atomicfu")
kotlin("jvm")
id("kotlinx-serialization")
}
group = "net.mamoe.mirai"
version = rootProject.ext["mirai_version"].toString()
description = "Mirai Http Api"
val kotlinVersion: String by rootProject.ext
val atomicFuVersion: String by rootProject.ext
val coroutinesVersion: String by rootProject.ext
val kotlinXIoVersion: String by rootProject.ext
val coroutinesIoVersion: String by rootProject.ext
val klockVersion: String by rootProject.ext
val ktorVersion: String by rootProject.ext
val serializationVersion: String by rootProject.ext
fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
fun ktor(id: String, version: String = this@Build_gradle.ktorVersion) = "io.ktor:ktor-$id:$version"
kotlin {
sourceSets["main"].apply {
dependencies {
implementation(project(":mirai-core-qqandroid"))
implementation(kotlin("stdlib-jdk8", kotlinVersion))
implementation(kotlin("stdlib-jdk7", kotlinVersion))
implementation(kotlin("reflect", kotlinVersion))
implementation(ktor("server-cio"))
implementation(kotlinx("io-jvm", kotlinXIoVersion))
implementation(ktor("http-jvm"))
implementation("org.slf4j:slf4j-simple:1.7.30")
}
}
sourceSets["test"].apply {
dependencies {
}
kotlin.outputDir = file("build/classes/kotlin/jvm/test")
kotlin.setSrcDirs(listOf("src/$name/kotlin"))
}
sourceSets.all {
languageSettings.enableLanguageFeature("InlineClasses")
languageSettings.useExperimentalAnnotation("kotlin.Experimental")
dependencies {
implementation(kotlin("stdlib", kotlinVersion))
implementation(kotlin("serialization", kotlinVersion))
implementation("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion")
implementation(kotlinx("io", kotlinXIoVersion))
implementation(kotlinx("coroutines-io", coroutinesIoVersion))
implementation(kotlinx("coroutines-core", coroutinesVersion))
implementation(kotlinx("serialization-runtime", serializationVersion))
implementation(ktor("server-core"))
implementation(ktor("http"))
}
}
}
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http
import io.ktor.application.Application
import io.ktor.server.cio.CIO
import io.ktor.server.engine.applicationEngineEnvironment
import io.ktor.server.engine.connector
import io.ktor.server.engine.embeddedServer
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.mamoe.mirai.api.http.route.mirai
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.MiraiLogger
import org.slf4j.helpers.NOPLoggerFactory
import kotlin.coroutines.CoroutineContext
object MiraiHttpAPIServer : CoroutineScope {
var logger: MiraiLogger = DefaultLogger("Mirai HTTP API")
override val coroutineContext: CoroutineContext =
CoroutineExceptionHandler { _, throwable -> logger.error(throwable) }
init {
SessionManager.authKey = generateSessionKey()//用于验证的key, 使用和SessionKey相同的方法生成, 但意义不同
}
fun setAuthKey(key: String) {
SessionManager.authKey = key
}
@UseExperimental(KtorExperimentalAPI::class)
fun start(
port: Int = 8080,
authKey: String,
callback: (() -> Unit)? = null
) {
require(authKey.length in 8..128) { "Expected authKey length is between 8 to 128" }
SessionManager.authKey = authKey
// TODO: start是无阻塞的,理应获取启动状态后再执行后续代码
launch {
embeddedServer(CIO, environment = applicationEngineEnvironment {
this.parentCoroutineContext = coroutineContext
this.log = NOPLoggerFactory().getLogger("NMYSL")
this.module(Application::mirai)
connector {
this.port = port
}
}).start(wait = true)
}
logger.info("Http api server is running with authKey: ${SessionManager.authKey}")
callback?.invoke()
}
}
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http
import kotlinx.coroutines.*
import net.mamoe.mirai.Bot
import net.mamoe.mirai.api.http.queue.MessageQueue
import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.utils.currentTimeSeconds
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
tailrec fun generateSessionKey(): String {
fun generateRandomSessionKey(): String {
val all = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm"
return buildString(capacity = 8) {
repeat(8) {
append(all.random())
}
}
}
val key = generateRandomSessionKey()
if (!SessionManager.allSession.containsKey(key)) {
return key
}
return generateSessionKey()
}
internal object SessionManager {
val allSession: MutableMap<String, Session> = mutableMapOf()
lateinit var authKey: String
fun createTempSession(): TempSession = TempSession(EmptyCoroutineContext).also { newTempSession ->
allSession[newTempSession.key] = newTempSession
//设置180000ms后检测并回收
newTempSession.launch {
delay(180000)
allSession[newTempSession.key]?.run {
if (this is TempSession)
closeSession(newTempSession.key)
}
}
}
fun createAuthedSession(bot: Bot, originKey: String): AuthedSession = AuthedSession(bot, originKey, EmptyCoroutineContext).also { session ->
closeSession(originKey)
allSession[originKey] = session
}
operator fun get(sessionKey: String) = allSession[sessionKey]?.also {
if (it is AuthedSession) it.latestUsed = currentTimeSeconds }
fun containSession(sessionKey: String): Boolean = allSession.containsKey(sessionKey)
fun closeSession(sessionKey: String) = allSession.remove(sessionKey)?.also { it.close() }
fun closeSession(session: Session) = closeSession(session.key)
}
/**
* 管理不同 Client 与 Mirai HTTP 的会话
*
* [Session] 均为内部操作用类
* @see [SessionManager]
* @author NaturalHG
*/
internal abstract class Session internal constructor(
coroutineContext: CoroutineContext,
val key: String = generateSessionKey()
) : CoroutineScope {
private val supervisorJob = SupervisorJob(coroutineContext[Job])
final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext
internal open fun close() {
supervisorJob.complete()
}
}
/**
* 任何新链接建立后分配一个[TempSession]
*
* TempSession在建立180s内没有转变为[AuthedSession]应被清除
*/
internal class TempSession internal constructor(coroutineContext: CoroutineContext) : Session(coroutineContext)
/**
* 任何[TempSession]认证后转化为一个[AuthedSession]
* 在这一步[AuthedSession]应该已经有assigned的bot
*/
internal class AuthedSession internal constructor(val bot: Bot, originKey: String, coroutineContext: CoroutineContext) : Session(coroutineContext, originKey) {
companion object {
const val CHECK_TIME = 1800L // 1800s aka 30min
}
val messageQueue = MessageQueue()
private val _listener: Listener<BotEvent>
private val releaseJob: Job //手动释放将会在下一次检查时回收Session
internal var latestUsed = currentTimeSeconds
init {
_listener = bot.subscribeAlways{ this.run(messageQueue::add) }
releaseJob = launch {
while (true) {
delay(CHECK_TIME * 1000)
if (currentTimeSeconds - latestUsed >= CHECK_TIME) {
SessionManager.closeSession(this@AuthedSession)
break
}
}
}
}
override fun close() {
messageQueue.clear()
_listener.complete()
super.close()
}
}
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.data
import kotlinx.serialization.Serializable
import net.mamoe.mirai.api.http.data.common.DTO
@Serializable
open class StateCode(val code: Int, var msg: String) : DTO {
object Success : StateCode(0, "success") // 成功
object NoBot : StateCode(2, "指定Bot不存在")
object IllegalSession : StateCode(3, "Session失效或不存在")
object NotVerifySession : StateCode(4, "Session未认证")
object NoElement : StateCode(5, "指定对象不存在")
object PermissionDenied : StateCode(10, "无操作权限")
object BotMuted : StateCode(20, "Bot被禁言")
// KS bug: 主构造器中不能有非字段参数 https://github.com/Kotlin/kotlinx.serialization/issues/575
@Serializable
class IllegalAccess() : StateCode(400, "") { // 非法访问
constructor(msg: String) : this() {
this.msg = msg
}
}
}
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.utils.MiraiExperimentalAPI
@Serializable
sealed class BotEventDTO : EventDTO()
@UseExperimental(MiraiExperimentalAPI::class)
suspend fun BotEvent.toDTO() = when(this) {
is MessagePacket<*, *> -> toDTO()
else -> when(this) {
is BotOnlineEvent -> BotOnlineEventDTO(bot.uin)
is BotOfflineEvent.Active -> BotOfflineEventActiveDTO(bot.uin)
is BotOfflineEvent.Force -> BotOfflineEventForceDTO(bot.uin, title, message)
is BotOfflineEvent.Dropped -> BotOfflineEventDroppedDTO(bot.uin)
is BotReloginEvent -> BotReloginEventDTO(bot.uin)
// is MessageSendEvent.GroupMessageSendEvent -> {}
// is MessageSendEvent.FriendMessageSendEvent -> {}
// is BeforeImageUploadEvent -> {}
// is ImageUploadEvent.Succeed -> {}
is BotGroupPermissionChangeEvent -> BotGroupPermissionChangeEventDTO(origin, new, GroupDTO(group))
is BotMuteEvent -> BotMuteEventDTO(durationSeconds, MemberDTO(operator))
is BotUnmuteEvent -> BotUnmuteEventDTO(MemberDTO(operator))
is BotJoinGroupEvent -> BotJoinGroupEventDTO(GroupDTO(group))
// is GroupSettingChangeEvent<*> -> {} // 不知道会改什么
is GroupNameChangeEvent -> GroupNameChangeEventDTO(origin, new, GroupDTO(group), isByBot)
is GroupEntranceAnnouncementChangeEvent -> GroupEntranceAnnouncementChangeEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
is GroupMuteAllEvent -> GroupMuteAllEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
is GroupAllowAnonymousChatEvent -> GroupAllowAnonymousChatEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
is GroupAllowConfessTalkEvent -> GroupAllowConfessTalkEventDTO(origin, new, GroupDTO(group), isByBot)
is GroupAllowMemberInviteEvent -> GroupAllowMemberInviteEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
is MemberJoinEvent -> MemberJoinEventDTO(MemberDTO(member))
is MemberLeaveEvent.Kick -> MemberLeaveEventKickDTO(MemberDTO(member), operator?.let(::MemberDTO))
is MemberLeaveEvent.Quit -> MemberLeaveEventQuitDTO(MemberDTO(member))
is MemberCardChangeEvent -> MemberCardChangeEventDTO(origin, new, MemberDTO(member), operator?.let(::MemberDTO))
is MemberSpecialTitleChangeEvent -> MemberSpecialTitleChangeEventDTO(origin, new, MemberDTO(member))
is MemberPermissionChangeEvent -> MemberPermissionChangeEventDTO(origin, new, MemberDTO(member))
is MemberMuteEvent -> MemberMuteEventDTO(durationSeconds, MemberDTO(member), operator?.let(::MemberDTO))
is MemberUnmuteEvent -> MemberUnmuteEventDTO(MemberDTO(member), operator?.let(::MemberDTO))
else -> IgnoreEventDTO
}
}
@Serializable
@SerialName("BotOnlineEvent")
data class BotOnlineEventDTO(val qq: Long) : BotEventDTO()
@Serializable
@SerialName("BotOfflineEventActive")
data class BotOfflineEventActiveDTO(val qq: Long) : BotEventDTO()
@Serializable
@SerialName("BotOfflineEventForce")
data class BotOfflineEventForceDTO(val qq: Long, val title: String, val message: String) : BotEventDTO()
@Serializable
@SerialName("BotOfflineEventDropped")
data class BotOfflineEventDroppedDTO(val qq: Long) : BotEventDTO()
@Serializable
@SerialName("BotReloginEvent")
data class BotReloginEventDTO(val qq: Long) : BotEventDTO()
@Serializable
@SerialName("BotGroupPermissionChangeEvent")
data class BotGroupPermissionChangeEventDTO(val origin: MemberPermission, val new: MemberPermission, val group: GroupDTO) : BotEventDTO()
@Serializable
@SerialName("BotMuteEvent")
data class BotMuteEventDTO(val durationSeconds: Int, val operator: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("BotUnmuteEvent")
data class BotUnmuteEventDTO(val operator: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("BotJoinGroupEvent")
data class BotJoinGroupEventDTO(val group: GroupDTO) : BotEventDTO()
@Serializable
@SerialName("GroupNameChangeEvent")
data class GroupNameChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val isByBot: Boolean) : BotEventDTO()
@Serializable
@SerialName("GroupEntranceAnnouncementChangeEvent")
data class GroupEntranceAnnouncementChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("GroupMuteAllEvent")
data class GroupMuteAllEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("GroupAllowAnonymousChatEvent")
data class GroupAllowAnonymousChatEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("GroupAllowConfessTalkEvent")
data class GroupAllowConfessTalkEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val isByBot: Boolean) : BotEventDTO()
@Serializable
@SerialName("GroupAllowMemberInviteEvent")
data class GroupAllowMemberInviteEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("MemberJoinEvent")
data class MemberJoinEventDTO(val member: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("MemberLeaveEventKick")
data class MemberLeaveEventKickDTO(val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("MemberLeaveEventQuit")
data class MemberLeaveEventQuitDTO(val member: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("MemberCardChangeEvent")
data class MemberCardChangeEventDTO(val origin: String, val new: String, val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("MemberSpecialTitleChangeEvent")
data class MemberSpecialTitleChangeEventDTO(val origin: String, val new: String, val member: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("MemberPermissionChangeEvent")
data class MemberPermissionChangeEventDTO(val origin: MemberPermission, val new: MemberPermission, val member: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("MemberMuteEvent")
data class MemberMuteEventDTO(val durationSeconds: Int, val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("MemberUnmuteEvent")
data class MemberUnmuteEventDTO(val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.Serializable
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.utils.MiraiExperimentalAPI
@Serializable
abstract class ContactDTO : DTO {
abstract val id: Long
}
@Serializable
data class QQDTO(
override val id: Long,
val nickName: String,
val remark: String
) : ContactDTO() {
// TODO: queryProfile.nickname & queryRemark.value not support now
constructor(qq: QQ) : this(qq.id, "", "")
}
@Serializable
data class MemberDTO(
override val id: Long,
val memberName: String,
val permission: MemberPermission,
val group: GroupDTO
) : ContactDTO() {
constructor(member: Member) : this(
member.id, member.nameCardOrNick, member.permission,
GroupDTO(member.group)
)
}
@Serializable
data class GroupDTO(
override val id: Long,
val name: String,
val permission: MemberPermission
) : ContactDTO() {
@UseExperimental(MiraiExperimentalAPI::class)
constructor(group: Group) : this(group.id, group.name, group.botPermission)
}
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.mamoe.mirai.api.http.AuthedSession
interface DTO
@Serializable
data class AuthDTO(val authKey: String) : DTO
@Serializable
abstract class VerifyDTO : DTO {
abstract val sessionKey: String
@Transient
internal lateinit var session: AuthedSession // 反序列化验证成功后传入
}
@Serializable
abstract class EventDTO : DTO
object IgnoreEventDTO : EventDTO()
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.uploadImage
import net.mamoe.mirai.utils.MiraiInternalAPI
import java.net.URL
/*
* DTO data class
* */
// MessagePacket
@Serializable
@SerialName("FriendMessage")
data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO()
@Serializable
@SerialName("GroupMessage")
data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO()
// Message
@Serializable
@SerialName("Source")
data class MessageSourceDTO(val id: Long) : MessageDTO()
@Serializable
@SerialName("At")
data class AtDTO(val target: Long, val display: String = "") : MessageDTO()
@Serializable
@SerialName("AtAll")
data class AtAllDTO(val target: Long = 0) : MessageDTO() // target为保留字段
@Serializable
@SerialName("Face")
data class FaceDTO(val faceId: Int) : MessageDTO()
@Serializable
@SerialName("Plain")
data class PlainDTO(val text: String) : MessageDTO()
@Serializable
@SerialName("Image")
data class ImageDTO(val imageId: String? = null, val url: String? = null) : MessageDTO()
@Serializable
@SerialName("Xml")
data class XmlDTO(val xml: String) : MessageDTO()
@Serializable
@SerialName("Unknown")
object UnknownMessageDTO : MessageDTO()
/*
* Abstract Class
* */
@Serializable
sealed class MessagePacketDTO : EventDTO() {
lateinit var messageChain: MessageChainDTO
}
typealias MessageChainDTO = List<MessageDTO>
@Serializable
sealed class MessageDTO : DTO
/*
Extend function
*/
suspend fun MessagePacket<*, *>.toDTO() = when (this) {
is FriendMessage -> FriendMessagePacketDTO(QQDTO(sender))
is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender))
else -> IgnoreEventDTO
}.apply {
if (this is MessagePacketDTO) {
// 将MessagePacket中的所有Message转为DTO对象,并添加到messageChain
// foreachContent会忽略MessageSource,一次主动获取
messageChain = mutableListOf(messageDTO(message[MessageSource])).apply {
message.foreachContent { content -> messageDTO(content).takeUnless { it == UnknownMessageDTO }?.let(::add) }
}
// else: `this` is bot event
}
}
suspend fun MessageChainDTO.toMessageChain(contact: Contact) =
buildMessageChain { this@toMessageChain.forEach { it.toMessage(contact)?.let(::add) } }
@UseExperimental(ExperimentalUnsignedTypes::class)
suspend fun MessagePacket<*, *>.messageDTO(message: Message) = when (message) {
is MessageSource -> MessageSourceDTO(message.id)
is At -> AtDTO(message.target, message.display)
is AtAll -> AtAllDTO(0L)
is Face -> FaceDTO(message.id)
is PlainText -> PlainDTO(message.stringValue)
is Image -> ImageDTO(message.imageId, message.url())
is XMLMessage -> XmlDTO(message.stringValue)
else -> UnknownMessageDTO
}
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
suspend fun MessageDTO.toMessage(contact: Contact) = when (this) {
is AtDTO -> At((contact as Group)[target])
is AtAllDTO -> AtAll
is FaceDTO -> Face(faceId)
is PlainDTO -> PlainText(text)
is ImageDTO -> when {
!imageId.isNullOrBlank() -> Image(imageId)
!url.isNullOrBlank() -> contact.uploadImage(URL(url))
else -> null
}
is XmlDTO -> XMLMessage(xml)
is MessageSourceDTO, is UnknownMessageDTO -> null
}
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.data
/**
* 错误请求. 抛出这个异常后将会返回错误给一个请求
*/
@Suppress("unused")
open class IllegalAccessException : Exception {
override val message: String get() = super.message!!
constructor(message: String) : super(message, null)
constructor(cause: Throwable) : super(cause.toString(), cause)
constructor(message: String, cause: Throwable?) : super(message, cause)
}
/**
* Session失效或不存在
*/
object IllegalSessionException : IllegalAccessException("Session失效或不存在")
/**
* Session未激活
*/
object NotVerifiedSessionException : IllegalAccessException("Session未激活")
/**
* 指定Bot不存在
*/
object NoSuchBotException: IllegalAccessException("指定Bot不存在")
/**
* 错误参数
*/
class IllegalParamException(message: String) : IllegalAccessException(message)
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.queue
import net.mamoe.mirai.api.http.data.common.EventDTO
import net.mamoe.mirai.api.http.data.common.IgnoreEventDTO
import net.mamoe.mirai.api.http.data.common.toDTO
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.firstKey
import java.util.concurrent.ConcurrentLinkedDeque
class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
val cacheSize = 4096
val cache = LinkedHashMap<Long, MessagePacket<*, *>>()
suspend fun fetch(size: Int): List<EventDTO> {
var count = size
val ret = ArrayList<EventDTO>(count)
while (!this.isEmpty() && count > 0) {
val event = pop()
event.toDTO().also {
if (it != IgnoreEventDTO) {
ret.add(it)
count--
}
}
if (event is MessagePacket<*, *>) {
addQuoteCache(event)
}
}
return ret
}
fun cache(messageId: Long) =
cache[messageId] ?: throw NoSuchElementException()
fun addQuoteCache(msg: MessagePacket<*, *>) {
cache[msg.message[MessageSource].id] = msg
if (cache.size > cacheSize) {
cache.remove(cache.firstKey())
}
}
}
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.route
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.routing.routing
import kotlinx.serialization.Serializable
import net.mamoe.mirai.Bot
import net.mamoe.mirai.api.http.AuthedSession
import net.mamoe.mirai.api.http.SessionManager
import net.mamoe.mirai.api.http.data.NoSuchBotException
import net.mamoe.mirai.api.http.data.StateCode
import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO
import kotlin.coroutines.EmptyCoroutineContext
fun Application.authModule() {
routing {
miraiAuth("/auth") {
if (it.authKey != SessionManager.authKey) {
call.respondStateCode(StateCode(1, "Auth Key错误"))
} else {
call.respondDTO(AuthRetDTO(0, SessionManager.createTempSession().key))
}
}
miraiVerify<BindDTO>("/verify", verifiedSessionKey = false) {
val bot = getBotOrThrow(it.qq)
SessionManager.createAuthedSession(bot, it.sessionKey)
call.respondStateCode(StateCode.Success)
}
miraiVerify<BindDTO>("/release") {
val bot = getBotOrThrow(it.qq)
val session = SessionManager[it.sessionKey] as AuthedSession
if (bot.uin == session.bot.uin) {
SessionManager.closeSession(it.sessionKey)
call.respondStateCode(StateCode.Success)
} else {
throw NoSuchElementException()
}
}
}
}
@Serializable
private data class AuthRetDTO(val code: Int, val session: String) : DTO
@Serializable
private data class BindDTO(override val sessionKey: String, val qq: Long) : VerifyDTO()
private fun getBotOrThrow(qq: Long) = try {
Bot.getInstance(qq)
} catch (e: NoSuchElementException) {
throw NoSuchBotException
}
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.route
import io.ktor.application.Application
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CallLogging
import io.ktor.features.DefaultHeaders
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.request.receive
import io.ktor.response.defaultTextContentType
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.Route
import io.ktor.routing.route
import io.ktor.util.pipeline.ContextDsl
import io.ktor.util.pipeline.PipelineContext
import net.mamoe.mirai.api.http.AuthedSession
import net.mamoe.mirai.api.http.SessionManager
import net.mamoe.mirai.api.http.TempSession
import net.mamoe.mirai.api.http.data.*
import net.mamoe.mirai.api.http.data.common.AuthDTO
import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO
import net.mamoe.mirai.api.http.util.jsonParseOrNull
import net.mamoe.mirai.api.http.util.toJson
import net.mamoe.mirai.contact.PermissionDeniedException
import org.slf4j.helpers.NOPLoggerFactory
fun Application.mirai() {
install(DefaultHeaders)
install(CallLogging) {
logger = NOPLoggerFactory().getLogger("NMSL")
}
authModule()
messageModule()
infoModule()
groupManageModule()
}
/**
* Auth,处理http server的验证
* 为闭包传入一个AuthDTO对象
*/
@ContextDsl
internal fun Route.miraiAuth(
path: String,
body: suspend PipelineContext<Unit, ApplicationCall>.(AuthDTO) -> Unit
): Route {
return route(path, HttpMethod.Post) {
intercept {
val dto = context.receiveDTO<AuthDTO>() ?: throw IllegalParamException("参数格式错误")
this.body(dto)
}
}
}
/**
* Get,用于获取bot的属性
* 验证请求参数中sessionKey参数的有效性
*/
@ContextDsl
internal fun Route.miraiGet(
path: String,
body: suspend PipelineContext<Unit, ApplicationCall>.(AuthedSession) -> Unit
): Route {
return route(path, HttpMethod.Get) {
intercept {
val sessionKey = call.parameters["sessionKey"] ?: throw IllegalParamException("参数格式错误")
if (!SessionManager.containSession(sessionKey)) throw IllegalSessionException
when (val session = SessionManager[sessionKey]) {
is TempSession -> throw NotVerifiedSessionException
is AuthedSession -> this.body(session)
}
}
}
}
/**
* Verify,用于处理bot的行为请求
* 验证数据传输对象(DTO)中是否包含sessionKey字段
* 且验证sessionKey的有效性
*
* @param verifiedSessionKey 是否验证sessionKey是否被激活
*
* it 为json解析出的DTO对象
*/
@ContextDsl
internal inline fun <reified T : VerifyDTO> Route.miraiVerify(
path: String,
verifiedSessionKey: Boolean = true,
crossinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit
): Route {
return route(path, HttpMethod.Post) {
intercept {
val dto = context.receiveDTO<T>() ?: throw IllegalParamException("参数格式错误")
SessionManager[dto.sessionKey]?.let {
when {
it is TempSession && verifiedSessionKey -> throw NotVerifiedSessionException
it is AuthedSession -> dto.session = it
}
} ?: throw IllegalSessionException
this.body(dto)
}
}
}
/**
* 统一捕获并处理异常
*/
internal inline fun Route.intercept(crossinline blk: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) = handle {
try {
blk(this)
} catch (e: NoSuchBotException) { // Bot不存在
call.respondStateCode(StateCode.NoBot)
} catch (e: IllegalSessionException) { // Session过期
call.respondStateCode(StateCode.IllegalSession)
} catch (e: NotVerifiedSessionException) { // Session未认证
call.respondStateCode(StateCode.NotVerifySession)
} catch (e: NoSuchElementException) { // 指定对象不存在
call.respondStateCode(StateCode.NoElement)
} catch (e: PermissionDeniedException) { // 缺少权限
call.respondStateCode(StateCode.PermissionDenied)
} catch (e: IllegalStateException) { // Bot被禁言
call.respondStateCode(StateCode.BotMuted)
} catch (e: IllegalAccessException) { // 错误访问
call.respondStateCode(StateCode(400, e.message), HttpStatusCode.BadRequest)
} catch (e: Throwable) {
e.printStackTrace()
call.respond(HttpStatusCode.InternalServerError, e.message!!)
}
}
/*
extend function
*/
internal suspend inline fun <reified T : StateCode> ApplicationCall.respondStateCode(code: T, status: HttpStatusCode = HttpStatusCode.OK) =
respondJson(code.toJson(StateCode.serializer()), status)
internal suspend inline fun <reified T : DTO> ApplicationCall.respondDTO(dto: T, status: HttpStatusCode = HttpStatusCode.OK) =
respondJson(dto.toJson(), status)
internal suspend fun ApplicationCall.respondJson(json: String, status: HttpStatusCode = HttpStatusCode.OK) =
respondText(json, defaultTextContentType(ContentType("application", "json")), status)
internal suspend inline fun <reified T : DTO> ApplicationCall.receiveDTO(): T? = receive<String>().jsonParseOrNull()
fun PipelineContext<Unit, ApplicationCall>.illegalParam(
expectingType: String?,
paramName: String,
actualValue: String? = call.parameters[paramName]
): Nothing = throw IllegalParamException("Illegal param. A $expectingType is required for `$paramName` while `$actualValue` is given")
@Suppress("IMPLICIT_CAST_TO_ANY")
@UseExperimental(ExperimentalUnsignedTypes::class)
internal inline fun <reified R> PipelineContext<Unit, ApplicationCall>.paramOrNull(name: String): R =
when (R::class) {
Byte::class -> call.parameters[name]?.toByte()
Int::class -> call.parameters[name]?.toInt()
Short::class -> call.parameters[name]?.toShort()
Float::class -> call.parameters[name]?.toFloat()
Long::class -> call.parameters[name]?.toLong()
Double::class -> call.parameters[name]?.toDouble()
Boolean::class -> when (call.parameters[name]) {
"true" -> true
"false" -> false
"0" -> false
"1" -> true
null -> null
else -> illegalParam("boolean", name)
}
String::class -> call.parameters[name]
UByte::class -> call.parameters[name]?.toUByte()
UInt::class -> call.parameters[name]?.toUInt()
UShort::class -> call.parameters[name]?.toUShort()
else -> error(name::class.simpleName + " is not supported")
} as R ?: illegalParam(R::class.simpleName, name)
/**
* multi part
*/
internal fun List<PartData>.value(name: String) =
try {
(filter { it.name == name }[0] as PartData.FormItem).value
} catch (e: Exception) {
throw IllegalParamException("参数格式错误")
}
internal fun List<PartData>.file(name: String) =
try {
filter { it.name == name }[0] as? PartData.FileItem
} catch (e: Exception) {
throw IllegalParamException("参数格式错误")
}
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.route
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.route
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.routing.routing
import kotlinx.serialization.Serializable
import net.mamoe.mirai.api.http.data.StateCode
import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
fun Application.groupManageModule() {
routing {
/**
* 禁言(需要相关权限)
*/
miraiVerify<MuteDTO>("/muteAll") {
it.session.bot.getGroup(it.target).isMuteAll = true
call.respondStateCode(StateCode.Success)
}
miraiVerify<MuteDTO>("/unmuteAll") {
it.session.bot.getGroup(it.target).isMuteAll = false
call.respondStateCode(StateCode.Success)
}
miraiVerify<MuteDTO>("/mute") {
it.session.bot.getGroup(it.target)[it.memberId].mute(it.time)
call.respondStateCode(StateCode.Success)
}
miraiVerify<MuteDTO>("/unmute") {
it.session.bot.getGroup(it.target).members[it.memberId].unmute()
call.respondStateCode(StateCode.Success)
}
/**
* 移出群聊(需要相关权限)
*/
miraiVerify<KickDTO>("/kick") {
it.session.bot.getGroup(it.target)[it.memberId].kick(it.msg)
call.respondStateCode(StateCode.Success)
}
/**
* 群设置(需要相关权限)
*/
miraiGet("/groupConfig") {
val group = it.bot.getGroup(paramOrNull("target"))
call.respondDTO(GroupDetailDTO(group))
}
miraiVerify<GroupConfigDTO>("/groupConfig") { dto ->
val group = dto.session.bot.getGroup(dto.target)
with(dto.config) {
name?.let { group.name = it }
announcement?.let { group.entranceAnnouncement = it }
confessTalk?.let { group.isConfessTalkEnabled = it }
allowMemberInvite?.let { group.isAllowMemberInvite = it }
// TODO: 待core接口实现设置可改
// autoApprove?.let { group.autoApprove = it }
// anonymousChat?.let { group.anonymousChat = it }
}
call.respondStateCode(StateCode.Success)
}
/**
* 群员信息管理(需要相关权限)
*/
miraiGet("/memberInfo") {
val member = it.bot.getGroup(paramOrNull("target"))[paramOrNull("memberId")]
call.respondDTO(MemberDetailDTO(member))
}
miraiVerify<MemberInfoDTO>("/memberInfo") { dto ->
val member = dto.session.bot.getGroup(dto.target)[dto.memberId]
with(dto.info) {
name?.let { member.nameCard = it }
specialTitle?.let { member.specialTitle = it }
}
call.respondStateCode(StateCode.Success)
}
}
}
@Serializable
private data class MuteDTO(
override val sessionKey: String,
val target: Long,
val memberId: Long = 0,
val time: Int = 0
) : VerifyDTO()
@Serializable
private data class KickDTO(
override val sessionKey: String,
val target: Long,
val memberId: Long,
val msg: String = ""
) : VerifyDTO()
@Serializable
private data class GroupConfigDTO(
override val sessionKey: String,
val target: Long,
val config: GroupDetailDTO
) : VerifyDTO()
@Serializable
private data class GroupDetailDTO(
val name: String? = null,
val announcement: String? = null,
val confessTalk: Boolean? = null,
val allowMemberInvite: Boolean? = null,
val autoApprove: Boolean? = null,
val anonymousChat: Boolean? = null
) : DTO {
constructor(group: Group) : this(
group.name, group.entranceAnnouncement, group.isConfessTalkEnabled, group.isAllowMemberInvite,
group.isAutoApproveEnabled, group.isAnonymousChatEnabled
)
}
@Serializable
private data class MemberInfoDTO(
override val sessionKey: String,
val target: Long,
val memberId: Long,
val info: MemberDetailDTO
) : VerifyDTO()
@Serializable
private data class MemberDetailDTO(
val name: String? = null,
val specialTitle: String? = null
) : DTO {
constructor(member: Member) : this(member.nameCard, member.specialTitle)
}
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.route
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.routing.routing
import net.mamoe.mirai.api.http.data.common.GroupDTO
import net.mamoe.mirai.api.http.data.common.MemberDTO
import net.mamoe.mirai.api.http.data.common.QQDTO
import net.mamoe.mirai.api.http.util.toJson
import net.mamoe.mirai.contact.toMutableList
fun Application.infoModule() {
routing {
miraiGet("/friendList") {
val ls = it.bot.qqs.toMutableList().map { qq -> QQDTO(qq) }
call.respondJson(ls.toJson())
}
miraiGet("/groupList") {
val ls = it.bot.groups.toMutableList().map { group -> GroupDTO(group) }
call.respondJson(ls.toJson())
}
miraiGet("/memberList") {
val ls = it.bot.getGroup(paramOrNull("target")).members.toMutableList().map { member -> MemberDTO(member) }
call.respondJson(ls.toJson())
}
}
}
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.route
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.readAllParts
import io.ktor.http.content.streamProvider
import io.ktor.request.receiveMultipart
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.post
import io.ktor.routing.routing
import kotlinx.serialization.Serializable
import net.mamoe.mirai.api.http.AuthedSession
import net.mamoe.mirai.api.http.SessionManager
import net.mamoe.mirai.api.http.data.*
import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.MessageChainDTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO
import net.mamoe.mirai.api.http.data.common.toMessageChain
import net.mamoe.mirai.api.http.util.toJson
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.uploadImage
import java.net.URL
fun Application.messageModule() {
routing {
miraiGet("/fetchMessage") {
val count: Int = paramOrNull("count")
val fetch = it.messageQueue.fetch(count)
call.respondJson(fetch.toJson())
}
suspend fun <C : Contact> sendMessage(
quote: QuoteReplyToSend?,
messageChain: MessageChain,
target: C
): MessageReceipt<out Contact> {
val send = if (quote == null) {
messageChain
} else {
(quote + messageChain).toChain()
}
return target.sendMessage(send)
}
miraiVerify<SendDTO>("/sendFriendMessage") {
val quote = it.quote?.let { q ->
it.session.messageQueue.cache(q).run {
this[MessageSource].quote(sender)
}}
it.session.bot.getFriend(it.target).apply {
val receipt = sendMessage(quote, it.messageChain.toMessageChain(this), this)
receipt.source.ensureSequenceIdAvailable()
it.session.messageQueue.addQuoteCache(FriendMessage(bot.selfQQ, receipt.source.toChain()))
call.respondDTO(SendRetDTO(messageId = receipt.source.id))
}
}
miraiVerify<SendDTO>("/sendGroupMessage") {
val quote = it.quote?.let { q ->
it.session.messageQueue.cache(q).run {
this[MessageSource].quote(sender)
}}
it.session.bot.getGroup(it.target).apply {
val receipt = sendMessage(quote, it.messageChain.toMessageChain(this), this)
receipt.source.ensureSequenceIdAvailable()
it.session.messageQueue.addQuoteCache(GroupMessage("", botPermission, botAsMember, receipt.source.toChain()))
call.respondDTO(SendRetDTO(messageId = receipt.source.id))
}
}
miraiVerify<SendImageDTO>("sendImageMessage") {
val bot = it.session.bot
val contact = when {
it.target != null -> bot[it.target]
it.qq != null -> bot.getFriend(it.qq)
it.group != null -> bot.getGroup(it.group)
else -> throw IllegalParamException("target、qq、group不可全为null")
}
val ls = it.urls.map { url -> contact.uploadImage(URL(url)) }
contact.sendMessage(MessageChain(ls))
call.respondJson(ls.map { image -> image.imageId }.toJson())
}
// TODO: 重构
post("uploadImage") {
val parts = call.receiveMultipart().readAllParts()
val sessionKey = parts.value("sessionKey")
if (!SessionManager.containSession(sessionKey)) throw IllegalSessionException
val session = try {
SessionManager[sessionKey] as AuthedSession
} catch (e: TypeCastException) {
throw NotVerifiedSessionException
}
val type = parts.value("type")
parts.file("img")?.apply {
val image = streamProvider().use {
when (type) {
"group" -> session.bot.groups.firstOrNull()?.uploadImage(it)
"friend" -> session.bot.qqs.firstOrNull()?.uploadImage(it)
else -> null
}
}
image?.apply {
call.respondText(imageId)
} ?: throw IllegalAccessException("图片上传错误")
} ?: throw IllegalAccessException("未知错误")
}
miraiVerify<RecallDTO>("recall") {
it.session.messageQueue.cache(it.target).apply {
it.session.bot.recall(get(MessageSource))
}
call.respondStateCode(StateCode.Success)
}
}
}
@Serializable
private data class SendDTO(
override val sessionKey: String,
val quote: Long? = null,
val target: Long,
val messageChain: MessageChainDTO
) : VerifyDTO()
@Serializable
private data class SendImageDTO(
override val sessionKey: String,
val target: Long? = null,
val qq: Long? = null,
val group: Long? = null,
val urls: List<String>
) : VerifyDTO()
@Serializable
private class SendRetDTO(
val code: Int = 0,
val msg: String = "success",
val messageId: Long
) : DTO
@Serializable
private data class RecallDTO(
override val sessionKey: String,
val target: Long
) : VerifyDTO()
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.api.http.util
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import net.mamoe.mirai.api.http.data.common.*
// 解析失败时直接返回null,由路由判断响应400状态
@UseExperimental(ImplicitReflectionSerializer::class)
inline fun <reified T : Any> String.jsonParseOrNull(
serializer: DeserializationStrategy<T>? = null
): T? = try {
if(serializer == null) MiraiJson.json.parse(this) else Json.parse(this)
} catch (e: Exception) { null }
@UseExperimental(ImplicitReflectionSerializer::class, UnstableDefault::class)
inline fun <reified T : Any> T.toJson(
serializer: SerializationStrategy<T>? = null
): String = if (serializer == null) MiraiJson.json.stringify(this)
else MiraiJson.json.stringify(serializer, this)
// 序列化列表时,stringify需要使用的泛型是T,而非List<T>
// 因为使用的stringify的stringify(objs: List<T>)重载
@UseExperimental(ImplicitReflectionSerializer::class, UnstableDefault::class)
inline fun <reified T : Any> List<T>.toJson(
serializer: SerializationStrategy<List<T>>? = null
): String = if (serializer == null) MiraiJson.json.stringify(this)
else MiraiJson.json.stringify(serializer, this)
/**
* Json解析规则,需要注册支持的多态的类
*/
object MiraiJson {
val json = Json(context = SerializersModule {
polymorphic(EventDTO.serializer()) {
GroupMessagePacketDTO::class with GroupMessagePacketDTO.serializer()
FriendMessagePacketDTO::class with FriendMessagePacketDTO.serializer()
BotOnlineEventDTO::class with BotOnlineEventDTO.serializer()
BotOfflineEventActiveDTO::class with BotOfflineEventActiveDTO.serializer()
BotOfflineEventForceDTO::class with BotOfflineEventForceDTO.serializer()
BotOfflineEventDroppedDTO::class with BotOfflineEventDroppedDTO.serializer()
BotReloginEventDTO::class with BotReloginEventDTO.serializer()
BotGroupPermissionChangeEventDTO::class with BotGroupPermissionChangeEventDTO.serializer()
BotMuteEventDTO::class with BotMuteEventDTO.serializer()
BotUnmuteEventDTO::class with BotUnmuteEventDTO.serializer()
BotJoinGroupEventDTO::class with BotJoinGroupEventDTO.serializer()
GroupNameChangeEventDTO::class with GroupNameChangeEventDTO.serializer()
GroupEntranceAnnouncementChangeEventDTO::class with GroupEntranceAnnouncementChangeEventDTO.serializer()
GroupMuteAllEventDTO::class with GroupMuteAllEventDTO.serializer()
GroupAllowAnonymousChatEventDTO::class with GroupAllowAnonymousChatEventDTO.serializer()
GroupAllowConfessTalkEventDTO::class with GroupAllowConfessTalkEventDTO.serializer()
GroupAllowMemberInviteEventDTO::class with GroupAllowMemberInviteEventDTO.serializer()
MemberJoinEventDTO::class with MemberJoinEventDTO.serializer()
MemberLeaveEventKickDTO::class with MemberLeaveEventKickDTO.serializer()
MemberLeaveEventQuitDTO::class with MemberLeaveEventQuitDTO.serializer()
MemberCardChangeEventDTO::class with MemberCardChangeEventDTO.serializer()
MemberSpecialTitleChangeEventDTO::class with MemberSpecialTitleChangeEventDTO.serializer()
MemberPermissionChangeEventDTO::class with MemberPermissionChangeEventDTO.serializer()
MemberMuteEventDTO::class with MemberMuteEventDTO.serializer()
MemberUnmuteEventDTO::class with MemberUnmuteEventDTO.serializer()
}
// Message Polymorphic
// polymorphic(MessageDTO.serializer()) {
// MessageSourceDTO::class with MessageSourceDTO.serializer()
// AtDTO::class with AtDTO.serializer()
// AtAllDTO::class with AtAllDTO.serializer()
// FaceDTO::class with FaceDTO.serializer()
// PlainDTO::class with PlainDTO.serializer()
// ImageDTO::class with ImageDTO.serializer()
// XmlDTO::class with XmlDTO.serializer()
// UnknownMessageDTO::class with UnknownMessageDTO.serializer()
// }
})
}
\ No newline at end of file
plugins {
kotlin("jvm")
java
id("com.github.johnrengelman.shadow")
}
version = "1.0.0"
val kotlinVersion: String by rootProject.ext
val atomicFuVersion: String by rootProject.ext
val coroutinesVersion: String by rootProject.ext
val kotlinXIoVersion: String by rootProject.ext
val coroutinesIoVersion: String by rootProject.ext
val serializationVersion: String by rootProject.ext
val klockVersion: String by rootProject.ext
val ktorVersion: String by rootProject.ext
kotlin {
sourceSets {
all {
languageSettings.enableLanguageFeature("InlineClasses")
languageSettings.useExperimentalAnnotation("kotlin.Experimental")
}
}
}
fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
dependencies {
api(project(":mirai-core"))
api(project(":mirai-console"))
runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main")) // classpath is not added correctly by IDE
api(kotlin("stdlib", kotlinVersion))
api(kotlinx("io-jvm", kotlinXIoVersion))
api(kotlinx("io", kotlinXIoVersion))
api(kotlinx("coroutines-io", coroutinesIoVersion))
api(kotlinx("coroutines-core", coroutinesVersion))
api("org.jsoup:jsoup:1.12.1")
api(group = "com.alibaba", name = "fastjson", version = "1.2.62")
}
tasks.withType<JavaCompile>() {
options.encoding = "UTF-8"
}
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.imageplugin
import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.registerCommand
import net.mamoe.mirai.console.plugins.Config
import net.mamoe.mirai.console.plugins.ConfigSection
import net.mamoe.mirai.console.plugins.PluginBase
import net.mamoe.mirai.console.plugins.withDefaultWriteSave
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.event.events.BotOnlineEvent
import net.mamoe.mirai.event.events.MemberPermissionChangeEvent
import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.event.subscribeGroupMessages
import net.mamoe.mirai.event.subscribeMessages
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.message.upload
import net.mamoe.mirai.message.uploadAsImage
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import org.jsoup.Jsoup
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
class ImageSenderMain : PluginBase() {
lateinit var images: Config
lateinit var normal: List<ConfigSection>
lateinit var r18: List<ConfigSection>
val config by lazy {
loadConfig("setting.yml")
}
val Normal_Image_Trigger by config.withDefaultWriteSave { "色图" }
val R18_Image_Trigger by config.withDefaultWriteSave { "不够色" }
val Image_Resize_Max_Width_Height by config.withDefaultWriteSave { 800 }
val groupsAllowNormal by lazy {
config.getLongList("Allow_Normal_Image_Groups").toMutableList()
}
val groupsAllowR18 by lazy {
config.getLongList("Allow_R18_Image_Groups").toMutableList()
}
override fun onDisable() {
config["Allow_R18_Image_Groups"] = groupsAllowR18
config["Allow_Normal_Image_Groups"] = groupsAllowNormal
config.save()
}
override fun onEnable() {
logger.info("Image Sender plugin enabled")
registerCommands()
subscribeAlways<MemberPermissionChangeEvent> {
logger.info("${this.bot.uin} login succeed, it will be controlled by Image Sender Plugin")
this.bot.subscribeGroupMessages {
(contains(Normal_Image_Trigger)) {
sendImage(subject, normal.random())
}
(contains(R18_Image_Trigger)) {
sendImage(subject, r18.random())
}
}
}
}
private fun sendImage(contact: Contact, configSection: ConfigSection) {
launch {
try {
logger.info("正在推送图片")
getImage(
contact, configSection.getString("url"), configSection.getString("pid"), 800
).plus(configSection.getString("tags")).sendTo(contact)
} catch (e: Exception) {
contact.sendMessage(e.message ?: "unknown error")
}
}
}
private suspend fun getImage(contact: Contact, url: String, pid: String, maxWidthOrHeight: Int): Image {
val bodyStream = withTimeoutOrNull(20 * 1000) {
withContext(Dispatchers.IO) {
Jsoup
.connect(url)
.followRedirects(true)
.timeout(180_000)
.ignoreContentType(true)
.userAgent("Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27")
.referrer("https://www.pixiv.net/member_illust.php?mode=medium&illust_id=$pid")
.ignoreHttpErrors(true)
.maxBodySize(100000000)
.execute().also { check(it.statusCode() == 200) { "Failed to download image" } }
}
}?.bodyStream() ?: error("Failed to download image")
if (maxWidthOrHeight < 1) {
return bodyStream.uploadAsImage(contact)
}
val image = withContext(Dispatchers.IO) {
ImageIO.read(bodyStream)
}
if (image.width.coerceAtLeast(image.height) <= maxWidthOrHeight) {
return image.upload(contact)
}
val rate = (maxWidthOrHeight.toFloat() / image.width.coerceAtLeast(image.height))
val newWidth = (image.width * rate).toInt()
val newHeight = (image.height * rate).toInt()
return withContext(Dispatchers.IO) {
val dimg = BufferedImage(newWidth, newHeight, image.type)
val g = dimg.createGraphics()
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
g.drawImage(image, 0, 0, newWidth, newHeight, 0, 0, image.width, image.height, null)
g.dispose()
dimg
}.upload(contact)
}
override fun onLoad() {
logger.info("loading local image data")
try {
images = getResourcesConfig("data.yml")
} catch (e: Exception) {
e.printStackTrace()
logger.info("无法加载本地图片")
}
logger.info("本地图片版本" + images.getString("version"))
r18 = images.getConfigSectionList("R18")
normal = images.getConfigSectionList("normal")
logger.info("Normal * " + normal.size)
logger.info("R18 * " + r18.size)
}
fun registerCommands() {
registerCommand {
}
}
}
\ No newline at end of file
import com.alibaba.fastjson.JSONObject
import com.google.gson.JsonObject
import net.mamoe.mirai.console.plugins.*
import net.mamoe.mirai.utils.cryptor.contentToString
import org.jsoup.Connection
import org.jsoup.Jsoup
import java.io.File
import kotlin.concurrent.thread
object Data {
val section = (File(System.getProperty("user.dir") + "/setu.yml")).loadAsConfig()
val abstract = section.getStringList("abstract").toMutableList()
val R18 = section.getConfigSectionList("R18").toMutableList()
val normal = section.getConfigSectionList("normal").toMutableList()
fun init() {
section.setIfAbsent("abstract", mutableListOf<String>())
section.setIfAbsent("R18", mutableListOf<ConfigSection>())
section.setIfAbsent("Normal", mutableListOf<ConfigSection>())
}
fun save() {
section["abstract"] = abstract
section["R18"] = R18
section["normal"] = normal
section.save()
}
}
fun main() {
val abstract_file = (File(System.getProperty("user.dir") + "/abstractSetu.yml")).loadAsConfig()
abstract_file.setIfAbsent("R18", mutableListOf<ConfigSection>())
abstract_file.setIfAbsent("normal", mutableListOf<ConfigSection>())
val r18 = abstract_file.getConfigSectionList("R18").toMutableList()
val normal = abstract_file.getConfigSectionList("normal").toMutableList()
Data.R18.forEach {
val forbid = with(it.getString("tags")) {
this.contains("初音ミク") || this.contains("VOCALOID") || this.contains("Miku")
||
this.contains("东方") || this.contains("東方")
}
if (forbid) {
println("过滤掉了一张图")
} else {
r18.add(
ConfigSectionImpl().apply {
this["pid"] = it["pid"]!!
this["author"] = it["author"]!!
this["uid"] = it["uid"]!!
this["tags"] = it["tags"]!!
this["url"] = it["url"]!!
}
)
}
}
Data.normal.forEach {
val forbid = with(it.getString("tags")) {
this.contains("初音ミク") || this.contains("VOCALOID") || this.contains("Miku")
||
this.contains("东方") || this.contains("東方")
}
if (forbid) {
println("过滤掉了一张图")
} else {
normal.add(
ConfigSectionImpl().apply {
this["pid"] = it["pid"]!!
this["author"] = it["author"]!!
this["uid"] = it["uid"]!!
this["tags"] = it["tags"]!!
this["url"] = it["url"]!!
}
)
}
}
abstract_file.set("R18", r18)
abstract_file.set("normal", normal)
abstract_file.save()
/**
Data.init()
Runtime.getRuntime().addShutdownHook(thread(start = false) {
Data.save()
})
while (true){
try {
val val0 = JSONObject.parseObject(Jsoup
.connect("https://api.lolicon.app/setu/")
.ignoreContentType(true)
.method(Connection.Method.GET)
.data("r18","1")
.data("num","10")
.execute().body())
val val1 = val0.getJSONArray("data")
for(index in 0 until val1.size - 1){
val content = val1.getJSONObject(index)
val pid = content.getString("pid")
if(Data.abstract.contains(pid)){
println("获取到了一张重复图$pid")
continue
}
val configSection = ConfigSectionImpl()
val isR18 = content.getBoolean("r18")
configSection["author"] = content.getString("author")
configSection["pid"] = pid
configSection["uid"] = content.getInteger("uid")
configSection["width"] = content.getInteger("width")
configSection["height"] = content.getInteger("height")
configSection["tags"] = content.getJSONArray("tags").map {
it.toString()
}.joinToString(",")
configSection["url"] = content.getString("url")
if(isR18){
Data.R18.add(configSection)
print("获取到了一张R18")
}else{
Data.normal.add(configSection)
print("获取到了一张Normal")
}
Data.abstract.add(pid)
println(configSection.contentToString())
}
}catch (e:Exception){
println(e.message)
}
Data.save()
println("SAVED")
Thread.sleep(1000)
}
*/
}
\ No newline at end of file
This diff is collapsed.
name: ImageSender
main: net.mamoe.mirai.imageplugin.ImageSenderMain
version: 1.0.0
author: 不想写代码
info: a demo[hso] plugin of mirai
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