GroupImpl.kt 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /*
  2. * Copyright 2019-2021 Mamoe Technologies and contributors.
  3. *
  4. * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  5. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
  6. *
  7. * https://github.com/mamoe/mirai/blob/master/LICENSE
  8. */
  9. @file:Suppress("INAPPLICABLE_JVM_NAME", "DEPRECATION_ERROR", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
  10. @file:OptIn(LowLevelApi::class)
  11. package net.mamoe.mirai.internal.contact
  12. import net.mamoe.mirai.LowLevelApi
  13. import net.mamoe.mirai.Mirai
  14. import net.mamoe.mirai.contact.*
  15. import net.mamoe.mirai.data.GroupInfo
  16. import net.mamoe.mirai.data.MemberInfo
  17. import net.mamoe.mirai.event.broadcast
  18. import net.mamoe.mirai.event.events.*
  19. import net.mamoe.mirai.internal.QQAndroidBot
  20. import net.mamoe.mirai.internal.message.*
  21. import net.mamoe.mirai.internal.network.QQAndroidBotNetworkHandler
  22. import net.mamoe.mirai.internal.network.highway.*
  23. import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_IMAGE
  24. import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_VOICE
  25. import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x388
  26. import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopEssenceMsgManager
  27. import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
  28. import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
  29. import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
  30. import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
  31. import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
  32. import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
  33. import net.mamoe.mirai.message.MessageReceipt
  34. import net.mamoe.mirai.message.data.*
  35. import net.mamoe.mirai.utils.*
  36. import java.util.concurrent.ConcurrentLinkedQueue
  37. import kotlin.contracts.contract
  38. import kotlin.coroutines.CoroutineContext
  39. import kotlin.time.ExperimentalTime
  40. internal fun GroupImpl.Companion.checkIsInstance(instance: Group) {
  41. contract { returns() implies (instance is GroupImpl) }
  42. check(instance is GroupImpl) { "group is not an instanceof GroupImpl!! DO NOT interlace two or more protocol implementations!!" }
  43. }
  44. internal fun Group.checkIsGroupImpl(): GroupImpl {
  45. contract { returns() implies (this@checkIsGroupImpl is GroupImpl) }
  46. GroupImpl.checkIsInstance(this)
  47. return this
  48. }
  49. @Suppress("PropertyName")
  50. internal class GroupImpl(
  51. bot: QQAndroidBot,
  52. coroutineContext: CoroutineContext,
  53. override val id: Long,
  54. groupInfo: GroupInfo,
  55. members: Sequence<MemberInfo>
  56. ) : Group, AbstractContact(bot, coroutineContext) {
  57. companion object
  58. val uin: Long = groupInfo.uin
  59. override val settings: GroupSettingsImpl = GroupSettingsImpl(this, groupInfo)
  60. override var name: String by settings::name
  61. override lateinit var owner: NormalMember
  62. override lateinit var botAsMember: NormalMember
  63. override val members: ContactList<NormalMember> = ContactList(members.mapNotNullTo(ConcurrentLinkedQueue()) {
  64. if (it.uin == bot.id) {
  65. botAsMember = newMember(it).cast()
  66. if (it.permission == MemberPermission.OWNER) {
  67. owner = botAsMember
  68. }
  69. null
  70. } else newMember(it).cast<NormalMember>().also { member ->
  71. if (member.permission == MemberPermission.OWNER) {
  72. owner = member
  73. }
  74. }
  75. })
  76. val groupPkgMsgParsingCache = GroupPkgMsgParsingCache()
  77. override suspend fun quit(): Boolean {
  78. check(botPermission != MemberPermission.OWNER) { "An owner cannot quit from a owning group" }
  79. if (!bot.groups.delegate.remove(this)) {
  80. return false
  81. }
  82. bot.network.run {
  83. val response: ProfileService.GroupMngReq.GroupMngReqResponse = ProfileService.GroupMngReq(
  84. bot.client,
  85. [email protected]
  86. ).sendAndExpect()
  87. check(response.errorCode == 0) {
  88. "Group.quit failed: $response".also {
  89. bot.groups.delegate.add(this@GroupImpl)
  90. }
  91. }
  92. }
  93. BotLeaveEvent.Active(this).broadcast()
  94. return true
  95. }
  96. override operator fun get(id: Long): NormalMember? {
  97. if (id == bot.id) {
  98. return botAsMember
  99. }
  100. return members.firstOrNull { it.id == id }
  101. }
  102. override fun contains(id: Long): Boolean {
  103. return bot.id == id || members.firstOrNull { it.id == id } != null
  104. }
  105. override suspend fun sendMessage(message: Message): MessageReceipt<Group> {
  106. require(!message.isContentEmpty()) { "message is empty" }
  107. check(!isBotMuted) { throw BotIsBeingMutedException(this) }
  108. val chain = broadcastMessagePreSendEvent(message, ::GroupMessagePreSendEvent)
  109. val result = GroupSendMessageHandler(this)
  110. .runCatching { sendMessage(message, chain, SendMessageStep.FIRST) }
  111. // logMessageSent(result.getOrNull()?.source?.plus(chain) ?: chain) // log with source
  112. logMessageSent(chain)
  113. GroupMessagePostSendEvent(this, chain, result.exceptionOrNull(), result.getOrNull()).broadcast()
  114. return result.getOrThrow()
  115. }
  116. @OptIn(ExperimentalTime::class)
  117. override suspend fun uploadImage(resource: ExternalResource): Image {
  118. if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
  119. throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
  120. }
  121. bot.network.run<QQAndroidBotNetworkHandler, Image> {
  122. val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
  123. bot.client,
  124. uin = bot.id,
  125. groupCode = id,
  126. md5 = resource.md5,
  127. size = resource.size,
  128. ).sendAndExpect()
  129. when (response) {
  130. is ImgStore.GroupPicUp.Response.Failed -> {
  131. ImageUploadEvent.Failed(this@GroupImpl, resource, response.resultCode, response.message).broadcast()
  132. if (response.message == "over file size max") throw OverFileSizeMaxException()
  133. error("upload group image failed with reason ${response.message}")
  134. }
  135. is ImgStore.GroupPicUp.Response.FileExists -> {
  136. val resourceId = resource.calculateResourceId()
  137. return OfflineGroupImage(imageId = resourceId)
  138. .also { it.fileId = response.fileId.toInt() }
  139. .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
  140. }
  141. is ImgStore.GroupPicUp.Response.RequireUpload -> {
  142. // val servers = response.uploadIpList.zip(response.uploadPortList)
  143. Highway.uploadResourceBdh(
  144. bot = bot,
  145. resource = resource,
  146. kind = GROUP_IMAGE,
  147. commandId = 2,
  148. initialTicket = response.uKey
  149. )
  150. return OfflineGroupImage(imageId = resource.calculateResourceId())
  151. .also { it.fileId = response.fileId.toInt() }
  152. .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
  153. }
  154. }
  155. }
  156. }
  157. override suspend fun uploadVoice(resource: ExternalResource): Voice {
  158. return bot.network.run {
  159. kotlin.runCatching {
  160. val (_) = Highway.uploadResourceBdh(
  161. bot = bot,
  162. resource = resource,
  163. kind = GROUP_VOICE,
  164. commandId = 29,
  165. extendInfo = PttStore.GroupPttUp.createTryUpPttPack(bot.id, id, resource)
  166. .toByteArray(Cmd0x388.ReqBody.serializer()),
  167. )
  168. }.recoverCatchingSuppressed {
  169. when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect()) {
  170. is PttStore.GroupPttUp.Response.RequireUpload -> {
  171. tryServers(
  172. bot,
  173. resp.uploadIpList.zip(resp.uploadPortList),
  174. resource.size,
  175. GROUP_VOICE,
  176. ChannelKind.HTTP
  177. ) { ip, port ->
  178. Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey)
  179. }
  180. }
  181. }
  182. }.getOrThrow()
  183. // val body = resp?.loadAs(Cmd0x388.RspBody.serializer())
  184. // ?.msgTryupPttRsp
  185. // ?.singleOrNull()?.fileKey ?: error("Group voice highway transfer succeed but failed to find fileKey")
  186. Voice(
  187. "${resource.md5.toUHexString("")}.amr",
  188. resource.md5,
  189. resource.size,
  190. resource.voiceCodec,
  191. ""
  192. )
  193. }
  194. }
  195. override suspend fun setEssenceMessage(source: MessageSource): Boolean {
  196. checkBotPermission(MemberPermission.ADMINISTRATOR)
  197. val result = bot.network.run {
  198. TroopEssenceMsgManager.SetEssence(
  199. bot.client,
  200. [email protected],
  201. source.internalIds.first(),
  202. source.ids.first()
  203. ).sendAndExpect()
  204. }
  205. return result.success
  206. }
  207. override fun toString(): String = "Group($id)"
  208. }
  209. internal fun Group.newMember(memberInfo: MemberInfo): Member {
  210. this.checkIsGroupImpl()
  211. memberInfo.anonymousId?.let { anId ->
  212. return AnonymousMemberImpl(
  213. this, this.coroutineContext,
  214. memberInfo, anId
  215. )
  216. }
  217. return NormalMemberImpl(
  218. this,
  219. this.coroutineContext,
  220. memberInfo
  221. )
  222. }
  223. internal fun GroupImpl.newAnonymous(name: String, id: String): AnonymousMemberImpl = newMember(
  224. MemberInfoImpl(
  225. uin = 80000000L,
  226. nick = name,
  227. permission = MemberPermission.MEMBER,
  228. remark = "匿名",
  229. nameCard = name,
  230. specialTitle = "匿名",
  231. muteTimestamp = 0,
  232. anonymousId = id,
  233. )
  234. ) as AnonymousMemberImpl