Commit 3eab26a5 authored by jiahua.liu's avatar jiahua.liu

Merge remote-tracking branch 'origin/master'

parents b7fd77b6 35e9138c
......@@ -2,11 +2,34 @@
开发版本. 频繁更新, 不保证高稳定性
## `0.15.0` 2020/2/14
### mirai-core
- 新增事件: `BotReloginEvent``BotOfflineEvent.Dropped`
- `AtAll` 现在实现 `Message.Key`
- 新增 `BotConfiguration` DSL, 支持自动将设备信息存储在文件系统等
- 新增 `MessageSource.quote(Member)`
- 更好的网络层连接逻辑
- 密码错误后不再重试登录
- 掉线后尝试快速重连, 失败则普通重连 (#47)
- 有原因的登录失败时将抛出特定异常: `LoginFailedException`
- 默认心跳时间调整为 60s
### mirai-core-qqandroid
- 解决一些验证码无法识别的问题
- 忽略一些不需要处理的事件(机器人主动操作触发的事件)
## `0.14.0` 2020/2/13
### mirai-core
- **支持 at 全体成员: `AtAll`**
### mirai-core-qqandroid
- **支持 `AtAll` 的发送和解析**
- **修复某些情况下禁言处理异常**
......
# style guide
kotlin.code.style=official
# config
mirai_version=0.14.0
mirai_version=0.15.0
kotlin.incremental.multiplatform=true
kotlin.parallel.tasks.in.project=true
# kotlin
......
#Thu Feb 06 14:10:33 CST 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
......
......@@ -220,6 +220,44 @@ fun main() {
### 发送引用回复消息(仅支持群消息)
```
[POST] /sendQuoteMessage
```
使用此方法向指定的消息进行引用回复
#### 请求
```json5
{
"sessionKey": "YourSession",
"target": 987654321,
"messageChain": [
{ "type": "Plain", "text":"hello\n" },
{ "type": "Plain", "text":"world" }
]
}
```
| 名字 | 类型 | 可选 | 举例 | 说明 |
| ------------ | ------ | ----- | ----------- | -------------------------------- |
| sessionKey | String | false | YourSession | 已经激活的Session |
| target | Long | false | 987654321 | 引用消息的Message Source的Uid |
| messageChain | Array | false | [] | 消息链,是一个消息对象构成的数组 |
#### 响应: 返回统一状态码
```json5
{
"code": 0,
"msg": "success"
}
```
### 发送图片消息(通过URL)
```
......@@ -308,6 +346,9 @@ Content-Type:multipart/form-data
[{
"type": "GroupMessage", // 消息类型:GroupMessage或FriendMessage
"messageChain": [{ // 消息链,是一个消息对象构成的数组
"type": "Source",
"uid": 123456
},{
"type": "Plain",
"text": "Miral牛逼"
}],
......@@ -343,12 +384,26 @@ Content-Type:multipart/form-data
#### 消息是构成消息链的基本对象,目前支持的消息类型有
+ [x] At,@消息
+ [x] AtAll,@全体成员
+ [x] Face,表情消息
+ [x] Plain,文字消息
+ [ ] Image,图片消息
+ [x] Image,图片消息
+ [ ] Xml,Xml卡片消息
+ [ ] 敬请期待
#### Source
```json5
{
"type": "Source",
"uid": 123456
}
```
| 名字 | 类型 | 说明 |
| ---- | ---- | ------------------------------------------------------------ |
| uid | Long | 消息的识别号,用于引用回复(Source类型只在群消息中返回,且永远为chain的第一个元素) |
#### At
```json5
......@@ -364,6 +419,18 @@ Content-Type:multipart/form-data
| target | Long | 群员QQ号 |
| display | String | @时显示的文本如:"@Mirai" |
#### AtAll
```json5
{
"type": "AtAll"
}
```
| 名字 | 类型 | 说明 |
| ------- | ------ | ------------------------- |
| - | - | - |
#### Face
```json5
......
......@@ -36,9 +36,15 @@ data class UnKnownMessagePacketDTO(val msg: String) : MessagePacketDTO()
// Message
@Serializable
@SerialName("Source")
data class MessageSourceDTO(val uid: 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
......@@ -82,7 +88,9 @@ fun MessageChainDTO.toMessageChain() =
@UseExperimental(ExperimentalUnsignedTypes::class)
fun Message.toDTO() = when (this) {
is MessageSource -> MessageSourceDTO(messageUid)
is At -> AtDTO(target, display)
is AtAll -> AtAllDTO(0L)
is Face -> FaceDTO(id.value.toInt())
is PlainText -> PlainDTO(stringValue)
is Image -> ImageDTO(imageId)
......@@ -93,11 +101,12 @@ fun Message.toDTO() = when (this) {
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
fun MessageDTO.toMessage() = when (this) {
is AtDTO -> At(target, display)
is AtAllDTO -> AtAll
is FaceDTO -> Face(FaceId(faceId.toUByte()))
is PlainDTO -> PlainText(text)
is ImageDTO -> Image(imageId)
is XmlDTO -> XMLMessage(xml)
is UnknownMessageDTO -> PlainText("assert cannot reach")
is MessageSourceDTO, is UnknownMessageDTO -> PlainText("assert cannot reach")
}
......
......@@ -9,17 +9,32 @@
package net.mamoe.mirai.api.http.queue
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.message.data.MessageSource
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
class MessageQueue : ConcurrentLinkedDeque<MessagePacket<*, *>>() {
val quoteCache = ConcurrentHashMap<Long, GroupMessage>()
fun fetch(size: Int): List<MessagePacket<*, *>> {
var count = size
quoteCache.clear()
val ret = ArrayList<MessagePacket<*, *>>(count)
while (!this.isEmpty() && count-- > 0) {
ret.add(this.pop())
val packet = pop()
ret.add(packet)
if (packet is GroupMessage) {
addCache(packet)
}
}
return ret
}
private fun addCache(msg: GroupMessage) {
quoteCache[msg.message[MessageSource].messageUid] = msg
}
}
\ No newline at end of file
......@@ -52,6 +52,12 @@ fun Application.messageModule() {
call.respondStateCode(StateCode.Success)
}
miraiVerify<SendDTO>("/quoteMessage") {
it.session.messageQueue.quoteCache[it.target]?.quoteReply(it.messageChain.toMessageChain())
?: throw NoSuchElementException()
call.respondStateCode(StateCode.Success)
}
miraiVerify<SendImageDTO>("sendImageMessage") {
val bot = it.session.bot
val contact = when {
......@@ -72,12 +78,14 @@ fun Application.messageModule() {
if (!SessionManager.containSession(sessionKey)) throw IllegalSessionException
val session = try {
SessionManager[sessionKey] as AuthedSession
} catch (e: TypeCastException) { throw NotVerifiedSessionException }
} catch (e: TypeCastException) {
throw NotVerifiedSessionException
}
val type = parts.value("type")
parts.file("img")?.apply {
val image = streamProvider().use {
when(type) {
when (type) {
"group" -> session.bot.groups.toList().random().uploadImage(it)
"friend" -> session.bot.qqs.toList().random().uploadImage(it)
else -> null
......
......@@ -13,6 +13,7 @@ import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import net.mamoe.mirai.api.http.data.common.*
import net.mamoe.mirai.message.data.MessageSource
// 解析失败时直接返回null,由路由判断响应400状态
@UseExperimental(ImplicitReflectionSerializer::class)
......@@ -50,7 +51,9 @@ object MiraiJson {
UnKnownMessagePacketDTO::class with UnKnownMessagePacketDTO.serializer()
}
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()
......
......@@ -20,9 +20,13 @@ import kotlinx.io.core.buildPacket
import kotlinx.io.core.use
import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.*
import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.event.CancellableEvent
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.WrongPasswordException
import net.mamoe.mirai.qqandroid.FriendInfoImpl
import net.mamoe.mirai.qqandroid.GroupImpl
import net.mamoe.mirai.qqandroid.QQAndroidBot
......@@ -37,7 +41,10 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.*
import net.mamoe.mirai.utils.io.ByteArrayPool
import net.mamoe.mirai.utils.io.PlatformSocket
import net.mamoe.mirai.utils.io.readPacket
import net.mamoe.mirai.utils.io.useBytes
import kotlin.coroutines.CoroutineContext
import kotlin.jvm.Volatile
import kotlin.time.ExperimentalTime
......@@ -55,13 +62,48 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
private lateinit var channel: PlatformSocket
override suspend fun login() {
private var _packetReceiverJob: Job? = null
private var heartbeatJob: Job? = null
private val packetReceiveLock: Mutex = Mutex()
private fun startPacketReceiverJobOrKill(cancelCause: CancellationException? = null): Job {
_packetReceiverJob?.cancel(cancelCause)
return this.launch(CoroutineName("Incoming Packet Receiver")) {
while (channel.isOpen) {
val rawInput = try {
channel.read()
} catch (e: CancellationException) {
return@launch
} catch (e: Throwable) {
BotOfflineEvent.Dropped(bot).broadcast()
return@launch
}
packetReceiveLock.withLock {
processPacket(rawInput)
}
}
}.also { _packetReceiverJob = it }
}
override suspend fun relogin() {
heartbeatJob?.cancel()
if (::channel.isInitialized) {
if (channel.isOpen) {
kotlin.runCatching {
registerClientOnline()
}.exceptionOrNull() ?: return
logger.info("Cannot do fast relogin. Trying slow relogin")
}
channel.close()
}
channel = PlatformSocket()
channel.connect("113.96.13.208", 8080)
this.launch(CoroutineName("Incoming Packet Receiver")) { processReceive() }
// TODO: 2020/2/14 连接多个服务器
withTimeoutOrNull(3000) {
channel.connect("113.96.13.208", 8080)
} ?: error("timeout connecting server")
startPacketReceiverJobOrKill(CancellationException("reconnect"))
// logger.info("Trying login")
var response: WtLogin.Login.LoginPacketResponse = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
......@@ -94,7 +136,8 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}
}
is WtLogin.Login.LoginPacketResponse.Error -> error(response.toString())
is WtLogin.Login.LoginPacketResponse.Error ->
throw WrongPasswordException(response.toString())
is WtLogin.Login.LoginPacketResponse.DeviceLockLogin -> {
response = WtLogin.Login.SubCommand20(
......@@ -112,18 +155,15 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}
// println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(6000) // it's slow
registerClientOnline()
}
private suspend fun registerClientOnline() {
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>()
}
@UseExperimental(MiraiExperimentalAPI::class, ExperimentalTime::class)
override suspend fun init(): Unit = coroutineScope {
this@QQAndroidBotNetworkHandler.subscribeAlways<BotOfflineEvent> {
if (this@QQAndroidBotNetworkHandler.bot == this.bot) {
logger.error("被挤下线")
close()
}
}
MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendWithoutExpect()
bot.qqs.delegate.clear()
......@@ -172,6 +212,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
launch {
try {
bot.groups.delegate.addLast(
@Suppress("DuplicatedCode")
GroupImpl(
bot = bot,
coroutineContext = bot.coroutineContext,
......@@ -211,14 +252,14 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
joinAll(friendListJob, groupJob)
this@QQAndroidBotNetworkHandler.launch(CoroutineName("Heartbeat")) {
heartbeatJob = this@QQAndroidBotNetworkHandler.launch(CoroutineName("Heartbeat")) {
while (this.isActive) {
delay(bot.configuration.heartbeatPeriodMillis)
val failException = doHeartBeat()
if (failException != null) {
delay(bot.configuration.firstReconnectDelayMillis)
close()
bot.tryReinitializeNetworkHandler(failException)
BotOfflineEvent.Dropped(bot).broadcast()
}
}
}
......@@ -408,33 +449,6 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}
@UseExperimental(ExperimentalCoroutinesApi::class)
private suspend fun processReceive() {
while (channel.isOpen) {
val rawInput = try {
channel.read()
} catch (e: ClosedChannelException) {
bot.tryReinitializeNetworkHandler(e)
return
} catch (e: ReadPacketInternalException) {
logger.error("Socket channel read failed: ${e.message}")
bot.tryReinitializeNetworkHandler(e)
return
} catch (e: CancellationException) {
return
} catch (e: Throwable) {
logger.error("Caught unexpected exceptions", e)
bot.tryReinitializeNetworkHandler(e)
return
}
packetReceiveLock.withLock {
processPacket(rawInput)
}
}
}
private val packetReceiveLock: Mutex = Mutex()
/**
* 发送一个包, 但不期待任何返回.
*/
......
......@@ -13,7 +13,7 @@ import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.io.JceStruct
class OnlinePushPack {
internal class OnlinePushPack {
@Serializable
internal class DelMsgInfo(
@SerialId(0) val fromUin: Long,
......
......@@ -35,6 +35,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.IncomingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildResponseUniPacket
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.debug
import net.mamoe.mirai.utils.io.discardExact
import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.readString
......@@ -157,6 +158,8 @@ internal class OnlinePush {
val reqPushMsg = decodeUniPacket(OnlinePushPack.SvcReqPushMsg.serializer(), "req")
reqPushMsg.vMsgInfos.forEach { msgInfo: MsgInfo ->
msgInfo.vMsg!!.read {
// TODO: 2020/2/13 可能会同时收到多个事件. 使用 map 而不要直接 return
when {
msgInfo.shMsgType.toInt() == 732 -> {
val group = bot.getGroup(this.readUInt().toLong())
......@@ -164,7 +167,11 @@ internal class OnlinePush {
when (val internalType = this.readShort().toInt()) {
3073 -> { // mute
val operator = group[this.readUInt().toLong()]
val operatorUin = this.readUInt().toLong()
if (operatorUin == bot.uin) {
return NoPacket
}
val operator = group[operatorUin]
this.readUInt().toLong() // time
this.discardExact(2)
val target = this.readUInt().toLong()
......@@ -215,7 +222,7 @@ internal class OnlinePush {
4096 -> {
val dataBytes = this.readBytes(26)
val message = this.readString(this.readByte().toInt())
println(dataBytes.toUHexString())
// println(dataBytes.toUHexString())
if (dataBytes[0].toInt() != 59) {
return GroupNameChangeEvent(
......@@ -244,7 +251,7 @@ internal class OnlinePush {
)
}
else -> {
println("Unknown server messages $message")
bot.network.logger.debug { "Unknown server messages $message" }
return NoPacket
}
}
......@@ -255,17 +262,17 @@ internal class OnlinePush {
// println(msgInfo.vMsg.toUHexString())
// }
else -> {
println("unknown group internal type $internalType , data: " + this.readBytes().toUHexString() + " ")
bot.network.logger.debug { "unknown group internal type $internalType , data: " + this.readBytes().toUHexString() + " " }
}
}
}
msgInfo.shMsgType.toInt() == 528 -> {
println("unknown shtype ${msgInfo.shMsgType.toInt()}")
bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" }
// val content = msgInfo.vMsg.loadAs(OnlinePushPack.MsgType0x210.serializer())
// println(content.contentToString())
}
else -> {
println("unknown shtype ${msgInfo.shMsgType.toInt()}")
bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" }
}
}
}
......
......@@ -124,7 +124,7 @@ fun ByteReadPacket.decodeMultiClientToServerPackets() {
}
fun Map<Int, ByteArray>.printTLVMap(name: String = "", keyLength: Int = 2) =
debugPrintln("TLVMap $name= " + this.mapValues { (_, value) -> value.toUHexString() }.mapKeys {
DebugLogger.debug("TLVMap $name= " + this.mapValues { (_, value) -> value.toUHexString() }.mapKeys {
when (keyLength) {
1 -> it.key.toUByte().contentToString()
2 -> it.key.toUShort().contentToString()
......
/*
* 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.utils
import kotlinx.io.core.IoBuffer
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.BotNetworkHandler
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* 在各平台实现的默认的验证码处理器.
*/
actual var defaultLoginSolver: LoginSolver = object : LoginSolver() {
override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? {
error("should be implemented manually by you")
}
override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? {
error("should be implemented manually by you")
}
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
error("should be implemented manually by you")
}
}
@Suppress("ClassName", "PropertyName")
actual open class BotConfiguration actual constructor() {
/**
* 日志记录器
*/
actual var botLoggerSupplier: ((Bot) -> MiraiLogger) = { DefaultLogger("Bot(${it.uin})") }
/**
* 网络层日志构造器
*/
actual var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger) = { DefaultLogger("Network(${it.bot.uin})") }
/**
* 设备信息覆盖. 默认使用随机的设备信息.
*/
actual var deviceInfo: ((Context) -> DeviceInfo)? = null
/**
* 父 [CoroutineContext]
*/
actual var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
/**
* 心跳周期. 过长会导致被服务器断开连接.
*/
actual var heartbeatPeriodMillis: Long = 60.secondsToMillis
/**
* 每次心跳时等待结果的时间.
* 一旦心跳超时, 整个网络服务将会重启 (将消耗约 5s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
*/
actual var heartbeatTimeoutMillis: Long = 2.secondsToMillis
/**
* 心跳失败后的第一次重连前的等待时间.
*/
actual var firstReconnectDelayMillis: Long = 5.secondsToMillis
/**
* 重连失败后, 继续尝试的每次等待时间
*/
actual var reconnectPeriodMillis: Long = 60.secondsToMillis
/**
* 最多尝试多少次重连
*/
actual var reconnectionRetryTimes: Int = 3
/**
* 验证码处理器
*/
actual var loginSolver: LoginSolver = defaultLoginSolver
actual companion object {
/**
* 默认的配置实例
*/
@JvmStatic
actual val Default = BotConfiguration()
}
actual operator fun _NoNetworkLog.unaryPlus() {
networkLoggerSupplier = supplier
}
/**
* 不记录网络层的 log.
* 网络层 log 包含包接收, 包发送, 和一些调试用的记录.
*/
@BotConfigurationDsl
actual val NoNetworkLog: _NoNetworkLog
get() = _NoNetworkLog
@BotConfigurationDsl
actual object _NoNetworkLog {
internal val supplier = { _: BotNetworkHandler -> SilentLogger }
}
}
/**
* 使用文件系统存储设备信息.
*/
@BotConfigurationDsl
inline class FileBasedDeviceInfo @BotConfigurationDsl constructor(val filepath: String) {
/**
* 使用 "device.json" 存储设备信息
*/
@BotConfigurationDsl
companion object ByDeviceDotJson
}
\ No newline at end of file
......@@ -16,40 +16,40 @@ import android.util.Log
* 不应该直接构造这个类的实例. 需使用 [DefaultLogger]
*/
actual open class PlatformLogger actual constructor(override val identity: String?) : MiraiLoggerPlatformBase() {
override fun verbose0(any: Any?) {
Log.v(identity, any?.toString() ?: "")
override fun verbose0(message: String?) {
Log.v(identity, message ?: "")
}
override fun verbose0(message: String?, e: Throwable?) {
Log.v(identity, message ?: "", e)
}
override fun debug0(any: Any?) {
Log.d(identity, any?.toString() ?: "")
override fun debug0(message: String?) {
Log.d(identity, message ?: "")
}
override fun debug0(message: String?, e: Throwable?) {
Log.d(identity, message ?: "", e)
}
override fun info0(any: Any?) {
Log.i(identity, any?.toString() ?: "")
override fun info0(message: String?) {
Log.i(identity, message ?: "")
}
override fun info0(message: String?, e: Throwable?) {
Log.i(identity, message ?: "", e)
}
override fun warning0(any: Any?) {
Log.w(identity, any?.toString() ?: "")
override fun warning0(message: String?) {
Log.w(identity, message ?: "")
}
override fun warning0(message: String?, e: Throwable?) {
Log.w(identity, message ?: "", e)
}
override fun error0(any: Any?) {
Log.e(identity, any?.toString() ?: "")
override fun error0(message: String?) {
Log.e(identity, message ?: "")
}
override fun error0(message: String?, e: Throwable?) {
......
......@@ -14,13 +14,40 @@ import android.net.wifi.WifiManager
import android.os.Build
import android.telephony.TelephonyManager
import kotlinx.io.core.toByteArray
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json
import java.io.File
/**
* 加载一个设备信息. 若文件不存在或为空则随机并创建一个设备信息保存.
*/
@UseExperimental(UnstableDefault::class)
fun File.loadAsDeviceInfo(context: Context): DeviceInfo {
if (!this.exists() || this.length() == 0L) {
return SystemDeviceInfo(context).also {
this.writeText(Json.plain.stringify(SystemDeviceInfo.serializer(), it))
}
}
return Json.nonstrict.parse(DeviceInfoData.serializer(), this.readText()).also {
it.context = context
}
}
/**
* 部分引用指向 [Build].
* 部分需要权限, 若无权限则会使用默认值.
*/
actual open class SystemDeviceInfo actual constructor(context: Context) : DeviceInfo(context) {
@Serializable
actual open class SystemDeviceInfo actual constructor() : DeviceInfo() {
actual constructor(context: Context) : this() {
this.context = context
}
@Transient
final override lateinit var context: Context
override val display: ByteArray get() = Build.DISPLAY.toByteArray()
override val product: ByteArray get() = Build.PRODUCT.toByteArray()
override val device: ByteArray get() = Build.DEVICE.toByteArray()
......@@ -88,7 +115,8 @@ actual open class SystemDeviceInfo actual constructor(context: Context) : Device
override val androidId: ByteArray get() = Build.ID.toByteArray()
override val apn: ByteArray get() = "wifi".toByteArray()
object Version : DeviceInfo.Version {
@Serializable
actual object Version : DeviceInfo.Version {
override val incremental: ByteArray get() = Build.VERSION.INCREMENTAL.toByteArray()
override val release: ByteArray get() = Build.VERSION.RELEASE.toByteArray()
override val codename: ByteArray get() = Build.VERSION.CODENAME.toByteArray()
......
......@@ -32,7 +32,7 @@ actual class PlatformSocket : Closeable {
actual val isOpen: Boolean
get() = socket.isConnected
override fun close() = socket.close()
actual override fun close() = socket.close()
@PublishedApi
internal lateinit var writeChannel: BufferedOutputStream
......
......@@ -24,6 +24,7 @@ import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.transferTo
......@@ -175,9 +176,11 @@ abstract class Bot : CoroutineScope {
/**
* 登录, 或重新登录.
* 不建议调用这个函数.
* 重新登录时不会再次拉取联系人列表.
*
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.login]
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin]
*
* @throws LoginFailedException
*/
abstract suspend fun login()
// endregion
......
......@@ -12,10 +12,15 @@
package net.mamoe.mirai
import kotlinx.coroutines.*
import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.BotReloginEvent
import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.ForceOfflineException
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.closeAndJoin
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.logStacktrace
......@@ -33,7 +38,7 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
private val botJob = SupervisorJob(configuration.parentCoroutineContext[Job])
override val coroutineContext: CoroutineContext =
configuration.parentCoroutineContext + botJob + (configuration.parentCoroutineContext[CoroutineExceptionHandler]
?: CoroutineExceptionHandler { _, e -> e.logStacktrace("An exception was thrown under a coroutine of Bot") })
?: CoroutineExceptionHandler { _, e -> logger.error("An exception was thrown under a coroutine of Bot", e) })
@Suppress("CanBePrimaryConstructorProperty") // for logger
final override val account: BotAccount = account
......@@ -78,60 +83,70 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
@Suppress("PropertyName")
internal lateinit var _network: N
final override suspend fun login() = reinitializeNetworkHandler(null)
// shouldn't be suspend!! This function MUST NOT inherit the context from the caller because the caller(NetworkHandler) is going to close
fun tryReinitializeNetworkHandler(
cause: Throwable?
): Job = launch {
var lastFailedException: Throwable? = null
repeat(configuration.reconnectionRetryTimes) {
try {
reinitializeNetworkHandler(cause)
logger.info("Reconnected successfully")
return@launch
} catch (e: Throwable) {
lastFailedException = e
delay(configuration.reconnectPeriodMillis)
@Suppress("unused")
private val offlineListener: Listener<BotOfflineEvent> = this.subscribeAlways { event ->
when (event) {
is BotOfflineEvent.Dropped -> {
bot.logger.info("Connection dropped or lost by server, retrying login")
var lastFailedException: Throwable? = null
repeat(configuration.reconnectionRetryTimes) {
try {
network.relogin()
logger.info("Reconnected successfully")
return@subscribeAlways
} catch (e: Throwable) {
lastFailedException = e
delay(configuration.reconnectPeriodMillis)
}
}
if (lastFailedException != null) {
throw lastFailedException!!
}
}
is BotOfflineEvent.Active -> {
val msg = if (event.cause == null) {
""
} else {
" with exception: " + event.cause.message
}
bot.logger.info("Bot is closed manually$msg")
close(CancellationException(event.toString()))
}
is BotOfflineEvent.Force -> {
bot.logger.info("Connection occupied by another android device: ${event.message}")
close(ForceOfflineException(event.toString()))
}
}
if (lastFailedException != null) {
throw lastFailedException!!
}
}
final override suspend fun login() = reinitializeNetworkHandler(null)
private suspend fun reinitializeNetworkHandler(
cause: Throwable?
) {
logger.info("BotAccount: $uin")
logger.info("Initializing BotNetworkHandler")
try {
if (::_network.isInitialized) {
BotOfflineEvent.Active(this, cause).broadcast()
_network.closeAndJoin(cause)
}
} catch (e: Exception) {
logger.error("Cannot close network handler", e)
}
loginLoop@ while (true) {
_network = createNetworkHandler(this.coroutineContext)
try {
_network.login()
break@loginLoop
} catch (e: Exception) {
e.logStacktrace()
_network.closeAndJoin(e)
suspend fun doRelogin() {
while (true) {
_network = createNetworkHandler(this.coroutineContext)
try {
_network.relogin()
return
} catch (e: LoginFailedException) {
throw e
} catch (e: Exception) {
network.logger.error(e)
_network.closeAndJoin(e)
}
logger.warning("Login failed. Retrying in 3s...")
delay(3000)
}
logger.warning("Login failed. Retrying in 3s...")
delay(3000)
}
repeat(1) block@{
suspend fun doInit() {
repeat(2) {
try {
_network.init()
return@block
return
} catch (e: Exception) {
e.logStacktrace()
}
......@@ -141,6 +156,16 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
logger.error("cannot init. some features may be affected")
}
logger.info("Initializing BotNetworkHandler")
if (::_network.isInitialized) {
BotReloginEvent(this, cause).broadcast()
doRelogin()
return
}
doRelogin()
doInit()
}
protected abstract fun createNetworkHandler(coroutineContext: CoroutineContext): N
......@@ -153,9 +178,11 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
if (cause == null) {
network.close()
this.botJob.complete()
offlineListener.complete()
} else {
network.close(cause)
this.botJob.completeExceptionally(cause)
offlineListener.completeExceptionally(cause)
}
}
groups.delegate.clear()
......
......@@ -98,4 +98,6 @@ suspend inline fun Member.mute(duration: Duration): Boolean {
require(duration.inDays <= 30) { "duration must be at most 1 month" }
require(duration.inSeconds > 0) { "duration must be greater than 0 second" }
return this.mute(duration.inSeconds.toInt())
}
\ No newline at end of file
}
suspend inline fun Member.mute(durationSeconds: Long) = this.mute(durationSeconds.toInt())
\ No newline at end of file
......@@ -631,4 +631,4 @@ class MessageSubscribersBuilder<T : MessagePacket<*, *>>(
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS, AnnotationTarget.TYPE)
@DslMarker
internal annotation class MessageDsl
\ No newline at end of file
annotation class MessageDsl
\ No newline at end of file
......@@ -16,6 +16,7 @@ import kotlinx.coroutines.GlobalScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.internal.Handler
import net.mamoe.mirai.event.internal.subscribeInternal
import net.mamoe.mirai.utils.MiraiInternalAPI
import kotlin.coroutines.CoroutineContext
/*
......@@ -96,6 +97,7 @@ interface Listener<in E : Event> : CompletableJob {
* @see subscribeGroupMessages 监听群消息 DSL
* @see subscribeFriendMessages 监听好友消息 DSL
*/
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event> CoroutineScope.subscribe(crossinline handler: suspend E.(E) -> ListeningStatus): Listener<E> =
E::class.subscribeInternal(Handler { it.handler(it); })
......@@ -107,6 +109,7 @@ inline fun <reified E : Event> CoroutineScope.subscribe(crossinline handler: sus
*
* @see subscribe 获取更多说明
*/
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event> CoroutineScope.subscribeAlways(crossinline listener: suspend E.(E) -> Unit): Listener<E> =
E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.LISTENING })
......@@ -118,6 +121,7 @@ inline fun <reified E : Event> CoroutineScope.subscribeAlways(crossinline listen
*
* @see subscribe 获取更多说明
*/
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event> CoroutineScope.subscribeOnce(crossinline listener: suspend E.(E) -> Unit): Listener<E> =
E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.STOPPED })
......@@ -129,6 +133,7 @@ inline fun <reified E : Event> CoroutineScope.subscribeOnce(crossinline listener
*
* @see subscribe 获取更多说明
*/
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event, T> CoroutineScope.subscribeUntil(valueIfStop: T, crossinline listener: suspend E.(E) -> T): Listener<E> =
E::class.subscribeInternal(Handler { if (it.listener(it) == valueIfStop) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
......@@ -141,6 +146,7 @@ inline fun <reified E : Event, T> CoroutineScope.subscribeUntil(valueIfStop: T,
*
* @see subscribe 获取更多说明
*/
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event, T> CoroutineScope.subscribeWhile(valueIfContinue: T, crossinline listener: suspend E.(E) -> T): Listener<E> =
E::class.subscribeInternal(Handler { if (it.listener(it) != valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
......
......@@ -42,19 +42,32 @@ data class BotOnlineEvent(override val bot: Bot) : BotActiveEvent
/**
* [Bot] 离线.
*/
sealed class BotOfflineEvent : BotActiveEvent {
sealed class BotOfflineEvent : BotEvent {
/**
* 主动离线
*/
data class Active(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent()
data class Active(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), BotActiveEvent
/**
* 被挤下线
*/
data class Force(override val bot: Bot, val title: String, val message: String) : BotOfflineEvent(), Packet
data class Force(override val bot: Bot, val title: String, val message: String) : BotOfflineEvent(), Packet, BotPassiveEvent
/**
* 被服务器断开或因网络问题而掉线
*/
data class Dropped(override val bot: Bot) : BotOfflineEvent(), Packet, BotPassiveEvent
}
/**
* [Bot] 主动重新登录.
*/
data class BotReloginEvent(
override val bot: Bot,
val cause: Throwable?
) : BotEvent, BotActiveEvent
// endregion
// region 消息
......
......@@ -23,8 +23,8 @@ import kotlin.reflect.KClass
val EventLogger: MiraiLoggerWithSwitch = DefaultLogger("Event").withSwitch(false)
@PublishedApi
internal fun <L : Listener<E>, E : Event> KClass<out E>.subscribeInternal(listener: L): L {
@MiraiInternalAPI
fun <L : Listener<E>, E : Event> KClass<out E>.subscribeInternal(listener: L): L {
this.listeners().addLast(listener)
return listener
}
......
......@@ -14,7 +14,7 @@ package net.mamoe.mirai.message.data
*
* @see At at 单个群成员
*/
object AtAll : Message {
object AtAll : Message, Message.Key<AtAll> {
override fun toString(): String = "@全体成员"
// 自动为消息补充 " "
......
......@@ -155,6 +155,7 @@ inline fun <reified M : Message> MessageChain.any(): Boolean = this.any { it is
@Suppress("UNCHECKED_CAST")
fun <M : Message> MessageChain.firstOrNull(key: Message.Key<M>): M? = when (key) {
At -> first<At>()
AtAll -> first<AtAll>()
PlainText -> first<PlainText>()
Image -> first<Image>()
Face -> first<Face>()
......
......@@ -14,6 +14,8 @@ package net.mamoe.mirai.message.data
* 消息源只用于 [QuoteReply]
*
* `mirai-core-qqandroid`: `net.mamoe.mirai.qqandroid.message.MessageSourceFromMsg`
*
* @see MessageSource.quote 引用这条消息, 创建 [MessageChain]
*/
interface MessageSource : Message {
companion object : Message.Key<MessageSource>
......
......@@ -33,4 +33,13 @@ fun MessageChain.quote(sender: Member): MessageChain {
return QuoteReply(it) + sender.at() + " " // required
}
error("cannot find MessageSource")
}
/**
* 引用这条消息.
* 返回 `[QuoteReply] + [At] + [PlainText]`(必要的结构)
*/
fun MessageSource.quote(sender: Member): MessageChain {
@UseExperimental(MiraiInternalAPI::class)
return QuoteReply(this) + sender.at() + " " // required
}
\ No newline at end of file
......@@ -55,12 +55,20 @@ abstract class BotNetworkHandler : CoroutineScope {
/**
* 依次尝试登录到可用的服务器. 在任一服务器登录完成后返回.
* 本函数将挂起直到登录成功.
*
* - 会断开连接并重新登录.
* - 不会停止网络层的 [Job].
* - 重新登录时不会再次拉取联系人列表.
* - 挂起直到登录成功.
*
* 不要使用这个 API. 请使用 [Bot.login]
*
* @throws LoginFailedException 登录失败时
* @throws WrongPasswordException 密码错误时
*/
@Suppress("SpellCheckingInspection")
@MiraiInternalAPI
abstract suspend fun login()
abstract suspend fun relogin()
/**
* 初始化获取好友列表等值.
......@@ -92,6 +100,7 @@ abstract class BotNetworkHandler : CoroutineScope {
}
}
@UseExperimental(MiraiInternalAPI::class)
suspend fun BotNetworkHandler.closeAndJoin(cause: Throwable? = null) {
this.close(cause)
this.supervisor.join()
......
package net.mamoe.mirai.network
class ForceOfflineException(override val message: String?) : RuntimeException()
\ No newline at end of file
......@@ -7,24 +7,16 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.utils
import kotlinx.io.core.IoBuffer
import net.mamoe.mirai.Bot
package net.mamoe.mirai.network
/**
* 在各平台实现的默认的验证码处理器.
* 正常登录失败时抛出
*/
actual var defaultLoginSolver: LoginSolver = object : LoginSolver() {
override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? {
error("should be implemented manually by you")
}
override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? {
error("should be implemented manually by you")
}
sealed class LoginFailedException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
}
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
error("should be implemented manually by you")
}
}
\ No newline at end of file
class WrongPasswordException(message: String?) : LoginFailedException(message)
\ No newline at end of file
......@@ -30,7 +30,7 @@ annotation class MiraiInternalAPI(
)
/**
* 标记这个类, 类型, 函数, 属性, 字段, 或构造器为实验性的.
* 标记这个类, 类型, 函数, 属性, 字段, 或构造器为实验性的 API.
*
* 这些 API 不具有稳定性, 且可能会在任意时刻更改.
* 不建议在发行版本中使用这些 API.
......@@ -56,7 +56,7 @@ annotation class MiraiDebugAPI(
)
/**
* 标记这个 API 是自 Mirai 某个版本起才受支持.
* 标记一个自 Mirai 某个版本起才支持的 API.
*/
@Target(CLASS, PROPERTY, FIELD, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPEALIAS)
@Retention(AnnotationRetention.BINARY)
......
......@@ -13,7 +13,6 @@ import kotlinx.io.core.IoBuffer
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.BotNetworkHandler
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmStatic
/**
......@@ -33,58 +32,74 @@ abstract class LoginSolver {
expect var defaultLoginSolver: LoginSolver
/**
* 网络和连接配置
* [Bot] 配置
*/
class BotConfiguration {
@Suppress("PropertyName")
expect open class BotConfiguration() {
/**
* 日志记录器
*/
var botLoggerSupplier: ((Bot) -> MiraiLogger) = { DefaultLogger("Bot(${it.uin})") }
var botLoggerSupplier: ((Bot) -> MiraiLogger)
/**
* 网络层日志构造器
*/
var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger) = { DefaultLogger("Network(${it.bot.uin})") }
var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger)
/**
* 设备信息覆盖
* 设备信息覆盖. 默认使用随机的设备信息.
*/
var deviceInfo: ((Context) -> DeviceInfo)? = null
var deviceInfo: ((Context) -> DeviceInfo)?
/**
* 父 [CoroutineContext]
*/
var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
var parentCoroutineContext: CoroutineContext
/**
* 心跳周期. 过长会导致被服务器断开连接.
*/
var heartbeatPeriodMillis: Long = 30.secondsToMillis
var heartbeatPeriodMillis: Long
/**
* 每次心跳时等待结果的时间.
* 一旦心跳超时, 整个网络服务将会重启 (将消耗约 5s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
*/
var heartbeatTimeoutMillis: Long = 2.secondsToMillis
var heartbeatTimeoutMillis: Long
/**
* 心跳失败后的第一次重连前的等待时间.
*/
var firstReconnectDelayMillis: Long = 5.secondsToMillis
var firstReconnectDelayMillis: Long
/**
* 重连失败后, 继续尝试的每次等待时间
*/
var reconnectPeriodMillis: Long = 60.secondsToMillis
var reconnectPeriodMillis: Long
/**
* 最多尝试多少次重连
*/
var reconnectionRetryTimes: Int = 3
var reconnectionRetryTimes: Int
/**
* 验证码处理器
*/
var loginSolver: LoginSolver = defaultLoginSolver
var loginSolver: LoginSolver
companion object {
/**
* 默认的配置实例
*/
@JvmStatic
val Default = BotConfiguration()
val Default: BotConfiguration
}
}
\ No newline at end of file
operator fun _NoNetworkLog.unaryPlus()
/**
* 不记录网络层的 log.
* 网络层 log 包含包接收, 包发送, 和一些调试用的记录.
*/
@BotConfigurationDsl
val NoNetworkLog: _NoNetworkLog
@Suppress("ClassName")
object _NoNetworkLog
}
@DslMarker
annotation class BotConfigurationDsl
\ No newline at end of file
......@@ -11,16 +11,15 @@ package net.mamoe.mirai.utils
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.protobuf.ProtoBuf
import net.mamoe.mirai.utils.cryptor.contentToString
/**
* 设备信息. 可通过继承 [SystemDeviceInfo] 来在默认的基础上修改
*/
abstract class DeviceInfo internal constructor(
context: Context
) {
val context: Context by context.unsafeWeakRef()
abstract class DeviceInfo {
@Transient
abstract val context: Context
abstract val display: ByteArray
abstract val product: ByteArray
......@@ -95,6 +94,45 @@ abstract class DeviceInfo internal constructor(
}
}
@Serializable
class DeviceInfoData(
override val display: ByteArray,
override val product: ByteArray,
override val device: ByteArray,
override val board: ByteArray,
override val brand: ByteArray,
override val model: ByteArray,
override val bootloader: ByteArray,
override val fingerprint: ByteArray,
override val bootId: ByteArray,
override val procVersion: ByteArray,
override val baseBand: ByteArray,
override val version: VersionData,
override val simInfo: ByteArray,
override val osType: ByteArray,
override val macAddress: ByteArray,
override val wifiBSSID: ByteArray?,
override val wifiSSID: ByteArray?,
override val imsiMd5: ByteArray,
override val imei: String,
override val apn: ByteArray
) : DeviceInfo() {
@Transient
override lateinit var context: Context
@UseExperimental(ExperimentalUnsignedTypes::class)
override val ipAddress: ByteArray
get() = localIpAddress().split(".").map { it.toUByte().toByte() }.takeIf { it.size == 4 }?.toByteArray() ?: byteArrayOf()
override val androidId: ByteArray get() = display
@Serializable
class VersionData(
override val incremental: ByteArray = SystemDeviceInfo.Version.incremental,
override val release: ByteArray = SystemDeviceInfo.Version.release,
override val codename: ByteArray = SystemDeviceInfo.Version.codename,
override val sdk: Int = SystemDeviceInfo.Version.sdk
) : Version
}
/**
* Defaults "%4;7t>;28<fc.5*6".toByteArray()
*/
......
......@@ -9,13 +9,15 @@
package net.mamoe.mirai.utils
import net.mamoe.mirai.utils.Context
import net.mamoe.mirai.utils.DeviceInfo
/**
* 通过本机信息来获取设备信息.
*
* Android: 获取手机信息, 与 QQ 官方相同.
* JVM: 部分为常量, 部分为随机
*/
open expect class SystemDeviceInfo(context: Context) : DeviceInfo
\ No newline at end of file
expect open class SystemDeviceInfo : DeviceInfo {
constructor()
constructor(context: Context)
object Version : DeviceInfo.Version
}
\ No newline at end of file
......@@ -15,11 +15,13 @@ package net.mamoe.mirai.utils.io
import kotlinx.io.core.*
import kotlinx.io.pool.useInstance
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.DefaultLogger
import net.mamoe.mirai.utils.MiraiDebugAPI
import net.mamoe.mirai.utils.MiraiLoggerWithSwitch
import net.mamoe.mirai.utils.withSwitch
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.js.JsName
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
......@@ -30,9 +32,6 @@ val DebugLogger : MiraiLoggerWithSwitch = DefaultLogger("Packet Debug").withSwit
@MiraiDebugAPI("Unstable")
inline fun Throwable.logStacktrace(message: String? = null) = DebugLogger.error(message, this)
@MiraiDebugAPI("Low efficiency.")
inline fun debugPrintln(any: Any?) = DebugLogger.debug(any)
@MiraiDebugAPI("Low efficiency.")
inline fun String.debugPrintThis(name: String): String {
DebugLogger.debug("$name=$this")
......
......@@ -81,7 +81,7 @@ inline fun TlvMap.getOrFail(tag: Int, lazyMessage: (tag: Int) -> String): ByteAr
return this[tag] ?: error(lazyMessage(tag))
}
@MiraiDebugAPI
@MiraiInternalAPI
inline fun Input.readTLVMap(tagSize: Int = 2, suppressDuplication: Boolean = true): TlvMap = readTLVMap(true, tagSize, suppressDuplication)
@MiraiDebugAPI
......
......@@ -11,7 +11,6 @@ package net.mamoe.mirai.utils.io
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.Closeable
import kotlinx.io.errors.IOException
import net.mamoe.mirai.utils.MiraiInternalAPI
/**
......@@ -37,4 +36,6 @@ expect class PlatformSocket() : Closeable {
suspend fun read(): ByteReadPacket
val isOpen: Boolean
override fun close()
}
\ 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.event.internal
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.ListeningStatus
import net.mamoe.mirai.utils.MiraiInternalAPI
import java.util.function.Consumer
import java.util.function.Function
@MiraiInternalAPI
@Suppress("FunctionName")
fun <E : Event> Class<E>._subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Function<E, ListeningStatus>): Listener<E> {
return this.kotlin.subscribeInternal(scope.Handler { onEvent.apply(it) })
}
@MiraiInternalAPI
@Suppress("FunctionName")
fun <E : Event> Class<E>._subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Consumer<E>): Listener<E> {
return this.kotlin.subscribeInternal(scope.Handler { onEvent.accept(it); ListeningStatus.LISTENING; })
}
\ No newline at end of file
......@@ -22,12 +22,14 @@ import kotlinx.coroutines.withContext
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.use
import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.BotNetworkHandler
import java.awt.Image
import java.awt.image.BufferedImage
import java.io.File
import java.io.RandomAccessFile
import javax.imageio.ImageIO
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* 平台默认的验证码识别器.
......@@ -157,3 +159,98 @@ private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Doub
}
}
@Suppress("ClassName", "PropertyName")
actual open class BotConfiguration actual constructor() {
/**
* 日志记录器
*/
actual var botLoggerSupplier: ((Bot) -> MiraiLogger) = { DefaultLogger("Bot(${it.uin})") }
/**
* 网络层日志构造器
*/
actual var networkLoggerSupplier: ((BotNetworkHandler) -> MiraiLogger) = { DefaultLogger("Network(${it.bot.uin})") }
/**
* 设备信息覆盖. 默认使用随机的设备信息.
*/
actual var deviceInfo: ((Context) -> DeviceInfo)? = null
/**
* 父 [CoroutineContext]
*/
actual var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
/**
* 心跳周期. 过长会导致被服务器断开连接.
*/
actual var heartbeatPeriodMillis: Long = 60.secondsToMillis
/**
* 每次心跳时等待结果的时间.
* 一旦心跳超时, 整个网络服务将会重启 (将消耗约 5s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
*/
actual var heartbeatTimeoutMillis: Long = 2.secondsToMillis
/**
* 心跳失败后的第一次重连前的等待时间.
*/
actual var firstReconnectDelayMillis: Long = 5.secondsToMillis
/**
* 重连失败后, 继续尝试的每次等待时间
*/
actual var reconnectPeriodMillis: Long = 60.secondsToMillis
/**
* 最多尝试多少次重连
*/
actual var reconnectionRetryTimes: Int = 3
/**
* 验证码处理器
*/
actual var loginSolver: LoginSolver = defaultLoginSolver
actual companion object {
/**
* 默认的配置实例
*/
@JvmStatic
actual val Default = BotConfiguration()
}
@Suppress("NOTHING_TO_INLINE")
@BotConfigurationDsl
inline operator fun FileBasedDeviceInfo.unaryPlus() {
deviceInfo = { File(filepath).loadAsDeviceInfo(it) }
}
@Suppress("NOTHING_TO_INLINE")
@BotConfigurationDsl
inline operator fun FileBasedDeviceInfo.ByDeviceDotJson.unaryPlus() {
deviceInfo = { File("device.json").loadAsDeviceInfo(it) }
}
actual operator fun _NoNetworkLog.unaryPlus() {
networkLoggerSupplier = supplier
}
/**
* 不记录网络层的 log.
* 网络层 log 包含包接收, 包发送, 和一些调试用的记录.
*/
@BotConfigurationDsl
actual val NoNetworkLog: _NoNetworkLog
get() = _NoNetworkLog
@BotConfigurationDsl
actual object _NoNetworkLog {
internal val supplier = { _: BotNetworkHandler -> SilentLogger }
}
}
/**
* 使用文件系统存储设备信息.
*/
@BotConfigurationDsl
inline class FileBasedDeviceInfo @BotConfigurationDsl constructor(val filepath: String) {
/**
* 使用 "device.json" 存储设备信息
*/
@BotConfigurationDsl
companion object ByDeviceDotJson
}
\ No newline at end of file
......@@ -18,37 +18,37 @@ import java.util.*
actual open class PlatformLogger @JvmOverloads internal actual constructor(
override val identity: String?
) : MiraiLoggerPlatformBase() {
override fun verbose0(any: Any?) = println(any, LoggerTextFormat.RESET)
override fun verbose0(message: String?) = println(message, LoggerTextFormat.RESET)
override fun verbose0(message: String?, e: Throwable?) {
if (message != null) verbose(message.toString())
e?.printStackTrace()
}
override fun info0(any: Any?) = println(any, LoggerTextFormat.LIGHT_GREEN)
override fun info0(message: String?) = println(message, LoggerTextFormat.LIGHT_GREEN)
override fun info0(message: String?, e: Throwable?) {
if (message != null) info(message.toString())
e?.printStackTrace()
}
override fun warning0(any: Any?) = println(any, LoggerTextFormat.LIGHT_RED)
override fun warning0(message: String?) = println(message, LoggerTextFormat.LIGHT_RED)
override fun warning0(message: String?, e: Throwable?) {
if (message != null) warning(message.toString())
e?.printStackTrace()
}
override fun error0(any: Any?) = println(any, LoggerTextFormat.RED)
override fun error0(message: String?) = println(message, LoggerTextFormat.RED)
override fun error0(message: String?, e: Throwable?) {
if (message != null) error(message.toString())
e?.printStackTrace()
}
override fun debug0(any: Any?) = println(any, LoggerTextFormat.LIGHT_CYAN)
override fun debug0(message: String?) = println(message, LoggerTextFormat.LIGHT_CYAN)
override fun debug0(message: String?, e: Throwable?) {
if (message != null) debug(message.toString())
e?.printStackTrace()
}
private fun println(value: Any?, color: LoggerTextFormat) {
private fun println(value: String?, color: LoggerTextFormat) {
val time = SimpleDateFormat("HH:mm:ss", Locale.SIMPLIFIED_CHINESE).format(Date())
if (identity == null) {
......@@ -62,6 +62,7 @@ actual open class PlatformLogger @JvmOverloads internal actual constructor(
/**
* @author NaturalHG
*/
@Suppress("unused")
internal enum class LoggerTextFormat(private val format: String) {
RESET("\u001b[0m"),
......
......@@ -10,38 +10,68 @@
package net.mamoe.mirai.utils
import kotlinx.io.core.toByteArray
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json
import net.mamoe.mirai.utils.io.getRandomByteArray
import net.mamoe.mirai.utils.io.getRandomString
import java.io.File
/**
* 加载一个设备信息. 若文件不存在或为空则随机并创建一个设备信息保存.
*/
@UseExperimental(UnstableDefault::class)
fun File.loadAsDeviceInfo(context: Context = ContextImpl()): DeviceInfo {
if (!this.exists() || this.length() == 0L) {
return SystemDeviceInfo(context).also {
this.writeText(Json.plain.stringify(SystemDeviceInfo.serializer(), it))
}
}
return Json.nonstrict.parse(DeviceInfoData.serializer(), this.readText()).also {
it.context = context
}
}
@Serializable
@UseExperimental(ExperimentalUnsignedTypes::class)
actual open class SystemDeviceInfo actual constructor(context: Context) : DeviceInfo(context) {
override val display: ByteArray get() = "MIRAI.200122.001".toByteArray()
override val product: ByteArray get() = "mirai".toByteArray()
override val device: ByteArray get() = "mirai".toByteArray()
override val board: ByteArray get() = "mirai".toByteArray()
override val brand: ByteArray get() = "mamoe".toByteArray()
override val model: ByteArray get() = "mirai".toByteArray()
override val bootloader: ByteArray get() = "unknown".toByteArray()
override val fingerprint: ByteArray get() = "mamoe/mirai/mirai:10/MIRAI.200122.001/5891938:user/release-keys".toByteArray()
actual open class SystemDeviceInfo actual constructor() : DeviceInfo() {
actual constructor(context: Context) : this() {
this.context = context
}
@Transient
final override lateinit var context: Context
override val display: ByteArray = "MIRAI.200122.001".toByteArray()
override val product: ByteArray = "mirai".toByteArray()
override val device: ByteArray = "mirai".toByteArray()
override val board: ByteArray = "mirai".toByteArray()
override val brand: ByteArray = "mamoe".toByteArray()
override val model: ByteArray = "mirai".toByteArray()
override val bootloader: ByteArray = "unknown".toByteArray()
override val fingerprint: ByteArray = "mamoe/mirai/mirai:10/MIRAI.200122.001/${getRandomString(7, '0'..'9')}:user/release-keys".toByteArray()
override val bootId: ByteArray = ExternalImage.generateUUID(md5(getRandomByteArray(16))).toByteArray()
override val procVersion: ByteArray get() = "Linux version 3.0.31-g6fb96c9 (android-build@xxx.xxx.xxx.xxx.com)".toByteArray()
override val baseBand: ByteArray get() = byteArrayOf()
override val version: DeviceInfo.Version get() = Version
override val simInfo: ByteArray get() = "T-Mobile".toByteArray()
override val osType: ByteArray get() = "android".toByteArray()
override val macAddress: ByteArray get() = "02:00:00:00:00:00".toByteArray()
override val wifiBSSID: ByteArray? get() = "02:00:00:00:00:00".toByteArray()
override val wifiSSID: ByteArray? get() = "<unknown ssid>".toByteArray()
override val imsiMd5: ByteArray get() = md5(getRandomByteArray(16))
override val imei: String get() = getRandomString(15, '0'..'9')
override val procVersion: ByteArray = "Linux version 3.0.31-${getRandomString(8)} (android-build@xxx.xxx.xxx.xxx.com)".toByteArray()
override val baseBand: ByteArray = byteArrayOf()
override val version: Version = Version
override val simInfo: ByteArray = "T-Mobile".toByteArray()
override val osType: ByteArray = "android".toByteArray()
override val macAddress: ByteArray = "02:00:00:00:00:00".toByteArray()
override val wifiBSSID: ByteArray? = "02:00:00:00:00:00".toByteArray()
override val wifiSSID: ByteArray? = "<unknown ssid>".toByteArray()
override val imsiMd5: ByteArray = md5(getRandomByteArray(16))
override val imei: String = getRandomString(15, '0'..'9')
override val ipAddress: ByteArray get() = localIpAddress().split(".").map { it.toUByte().toByte() }.takeIf { it.size == 4 }?.toByteArray() ?: byteArrayOf()
override val androidId: ByteArray get() = display
override val apn: ByteArray get() = "wifi".toByteArray()
override val apn: ByteArray = "wifi".toByteArray()
object Version : DeviceInfo.Version {
override val incremental: ByteArray get() = "5891938".toByteArray()
override val release: ByteArray get() = "10".toByteArray()
override val codename: ByteArray get() = "REL".toByteArray()
override val sdk: Int get() = 29
@Serializable
actual object Version : DeviceInfo.Version {
override val incremental: ByteArray = "5891938".toByteArray()
override val release: ByteArray = "10".toByteArray()
override val codename: ByteArray = "REL".toByteArray()
override val sdk: Int = 29
}
}
\ No newline at end of file
......@@ -32,7 +32,7 @@ actual class PlatformSocket : Closeable {
actual val isOpen: Boolean
get() = socket.isConnected
override fun close() {
actual override fun close() {
if (::socket.isInitialized) {
socket.close()
}
......
......@@ -27,6 +27,7 @@ import net.mamoe.mirai.message.data.firstOrNull
import net.mamoe.mirai.message.sendAsImageTo
import net.mamoe.mirai.qqandroid.Bot
import net.mamoe.mirai.qqandroid.QQAndroid
import net.mamoe.mirai.utils.FileBasedDeviceInfo
import java.io.File
private fun readTestAccount(): BotAccount? {
......@@ -51,7 +52,7 @@ suspend fun main() {
"123456"
) {
// 覆盖默认的配置
+FileBasedDeviceInfo // 使用 "device.json" 保存设备信息
// networkLoggerSupplier = { SilentLogger } // 禁用网络层输出
}.alsoLogin()
......
/*
* 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.japt;
import kotlinx.coroutines.GlobalScope;
import net.mamoe.mirai.event.Event;
import net.mamoe.mirai.event.Listener;
import net.mamoe.mirai.event.ListeningStatus;
import net.mamoe.mirai.event.internal.EventInternalJvmKt;
import net.mamoe.mirai.japt.internal.EventsImplKt;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
import java.util.function.Function;
public final class Events {
@NotNull
public static <E extends Event> Listener<E> subscribe(@NotNull Class<E> eventClass, @NotNull Function<E, ListeningStatus> onEvent) {
return EventInternalJvmKt._subscribeEventForJaptOnly(eventClass, GlobalScope.INSTANCE, onEvent);
}
@NotNull
public static <E extends Event> Listener<E> subscribeAlways(@NotNull Class<E> eventClass, @NotNull Consumer<E> onEvent) {
return EventInternalJvmKt._subscribeEventForJaptOnly(eventClass, GlobalScope.INSTANCE, onEvent);
}
@NotNull
public static <E extends Event> E broadcast(@NotNull E event) {
return EventsImplKt.broadcast(event);
}
}
\ No newline at end of file
......@@ -7,16 +7,10 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.event
package net.mamoe.mirai.japt.internal
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.broadcast
// TODO 添加更多
/**
* Jvm 调用实现(阻塞)
*/
object Events {
/*
@JvmStatic
fun <E : Event> subscribe(type: Class<E>, handler: suspend (E) -> ListeningStatus) =
runBlocking { type.kotlin.subscribe(handler) }*/
}
internal fun <E : Event> broadcast(e: E): E = runBlocking { e.broadcast() }
\ No newline at end of file
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