GroupImpl.kt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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/dev/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.contact.announcement.Announcements
  16. import net.mamoe.mirai.contact.file.RemoteFiles
  17. import net.mamoe.mirai.data.GroupInfo
  18. import net.mamoe.mirai.data.MemberInfo
  19. import net.mamoe.mirai.event.broadcast
  20. import net.mamoe.mirai.event.events.*
  21. import net.mamoe.mirai.internal.QQAndroidBot
  22. import net.mamoe.mirai.internal.contact.announcement.AnnouncementsImpl
  23. import net.mamoe.mirai.internal.contact.file.RemoteFilesImpl
  24. import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
  25. import net.mamoe.mirai.internal.message.*
  26. import net.mamoe.mirai.internal.network.components.BdhSession
  27. import net.mamoe.mirai.internal.network.handler.NetworkHandler
  28. import net.mamoe.mirai.internal.network.handler.logger
  29. import net.mamoe.mirai.internal.network.highway.ChannelKind
  30. import net.mamoe.mirai.internal.network.highway.Highway
  31. import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_AUDIO
  32. import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_IMAGE
  33. import net.mamoe.mirai.internal.network.highway.postPtt
  34. import net.mamoe.mirai.internal.network.highway.tryServersUpload
  35. import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x388
  36. import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopEssenceMsgManager
  37. import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
  38. import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
  39. import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.audioCodec
  40. import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
  41. import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
  42. import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
  43. import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
  44. import net.mamoe.mirai.internal.utils.ImagePatcher
  45. import net.mamoe.mirai.internal.utils.RemoteFileImpl
  46. import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
  47. import net.mamoe.mirai.internal.utils.subLogger
  48. import net.mamoe.mirai.message.MessageReceipt
  49. import net.mamoe.mirai.message.data.*
  50. import net.mamoe.mirai.spi.AudioToSilkService
  51. import net.mamoe.mirai.utils.*
  52. import java.util.concurrent.ConcurrentLinkedQueue
  53. import kotlin.contracts.contract
  54. import kotlin.coroutines.CoroutineContext
  55. import kotlin.time.ExperimentalTime
  56. internal fun GroupImpl.Companion.checkIsInstance(instance: Group) {
  57. contract { returns() implies (instance is GroupImpl) }
  58. check(instance is GroupImpl) { "group is not an instanceof GroupImpl!! DO NOT interlace two or more protocol implementations!!" }
  59. }
  60. internal fun Group.checkIsGroupImpl(): GroupImpl {
  61. contract { returns() implies (this@checkIsGroupImpl is GroupImpl) }
  62. GroupImpl.checkIsInstance(this)
  63. return this
  64. }
  65. internal fun GroupImpl(
  66. bot: QQAndroidBot,
  67. parentCoroutineContext: CoroutineContext,
  68. id: Long,
  69. groupInfo: GroupInfo,
  70. members: Sequence<MemberInfo>,
  71. ): GroupImpl {
  72. return GroupImpl(bot, parentCoroutineContext, id, groupInfo, ContactList(ConcurrentLinkedQueue())).apply Group@{
  73. members.forEach { info ->
  74. if (info.uin == bot.id) {
  75. botAsMember = newNormalMember(info)
  76. if (info.permission == MemberPermission.OWNER) {
  77. owner = botAsMember
  78. }
  79. } else newNormalMember(info).let { member ->
  80. if (member.permission == MemberPermission.OWNER) {
  81. owner = member
  82. }
  83. [email protected](member)
  84. }
  85. }
  86. }
  87. }
  88. @Suppress("PropertyName")
  89. internal class GroupImpl constructor(
  90. bot: QQAndroidBot,
  91. parentCoroutineContext: CoroutineContext,
  92. override val id: Long,
  93. groupInfo: GroupInfo,
  94. override val members: ContactList<NormalMemberImpl>,
  95. ) : Group, AbstractContact(bot, parentCoroutineContext) {
  96. companion object
  97. val uin: Long = groupInfo.uin
  98. override val settings: GroupSettingsImpl = GroupSettingsImpl(this, groupInfo)
  99. override var name: String by settings::name
  100. override lateinit var owner: NormalMemberImpl
  101. override lateinit var botAsMember: NormalMemberImpl
  102. @Suppress("DEPRECATION")
  103. @Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"))
  104. override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") }
  105. override val files: RemoteFiles by lazy { RemoteFilesImpl(this) }
  106. override val announcements: Announcements by lazy {
  107. AnnouncementsImpl(
  108. this,
  109. bot.network.logger.subLogger("Group $id")
  110. )
  111. }
  112. val groupPkgMsgParsingCache = GroupPkgMsgParsingCache()
  113. override suspend fun quit(): Boolean {
  114. check(botPermission != MemberPermission.OWNER) { "An owner cannot quit from a owning group" }
  115. if (!bot.groups.delegate.remove(this)) {
  116. return false
  117. }
  118. bot.network.run {
  119. val response: ProfileService.GroupMngReq.GroupMngReqResponse = ProfileService.GroupMngReq(
  120. bot.client,
  121. [email protected]
  122. ).sendAndExpect()
  123. check(response.errorCode == 0) {
  124. "Group.quit failed: $response".also {
  125. bot.groups.delegate.add(this@GroupImpl)
  126. }
  127. }
  128. }
  129. BotLeaveEvent.Active(this).broadcast()
  130. return true
  131. }
  132. override operator fun get(id: Long): NormalMemberImpl? {
  133. if (id == bot.id) return botAsMember
  134. return members.firstOrNull { it.id == id }
  135. }
  136. override fun contains(id: Long): Boolean {
  137. return bot.id == id || members.firstOrNull { it.id == id } != null
  138. }
  139. override suspend fun sendMessage(message: Message): MessageReceipt<Group> {
  140. val isMiraiInternal = if (message is MessageChain) {
  141. message.anyIsInstance<MiraiInternalMessageFlag>()
  142. } else false
  143. require(isMiraiInternal || !message.isContentEmpty()) { "message is empty" }
  144. check(!isBotMuted) { throw BotIsBeingMutedException(this, message) }
  145. val chain = broadcastMessagePreSendEvent(message, isMiraiInternal, ::GroupMessagePreSendEvent)
  146. val result = GroupSendMessageHandler(this)
  147. .runCatching { sendMessage(message, chain, isMiraiInternal, SendMessageStep.FIRST) }
  148. if (result.isSuccess) {
  149. // logMessageSent(result.getOrNull()?.source?.plus(chain) ?: chain) // log with source
  150. logMessageSent(chain)
  151. }
  152. if (!isMiraiInternal) {
  153. GroupMessagePostSendEvent(this, chain, result.exceptionOrNull(), result.getOrNull()).broadcast()
  154. }
  155. return result.getOrThrow()
  156. }
  157. @OptIn(ExperimentalTime::class)
  158. override suspend fun uploadImage(resource: ExternalResource): Image = resource.withAutoClose {
  159. if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
  160. throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
  161. }
  162. fun OfflineGroupImage.putIntoCache() {
  163. // We can't understand wny Image(group.uploadImage().imageId)
  164. bot.components[ImagePatcher].putCache(this)
  165. }
  166. val imageInfo = runBIO { resource.calculateImageInfo() }
  167. bot.network.run<NetworkHandler, Image> {
  168. val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
  169. bot.client,
  170. uin = bot.id,
  171. groupCode = id,
  172. md5 = resource.md5,
  173. size = resource.size,
  174. filename = "${resource.md5.toUHexString("")}.${resource.formatName}",
  175. picWidth = imageInfo.width,
  176. picHeight = imageInfo.height,
  177. picType = getIdByImageType(imageInfo.imageType),
  178. originalPic = 1
  179. ).sendAndExpect()
  180. when (response) {
  181. is ImgStore.GroupPicUp.Response.Failed -> {
  182. ImageUploadEvent.Failed(this@GroupImpl, resource, response.resultCode, response.message).broadcast()
  183. if (response.message == "over file size max") throw OverFileSizeMaxException()
  184. error("upload group image failed with reason ${response.message}")
  185. }
  186. is ImgStore.GroupPicUp.Response.FileExists -> {
  187. val resourceId = resource.calculateResourceId()
  188. return response.fileInfo.run {
  189. OfflineGroupImage(
  190. imageId = resourceId,
  191. height = fileHeight,
  192. width = fileWidth,
  193. imageType = getImageTypeById(fileType),
  194. size = resource.size
  195. )
  196. }
  197. .also {
  198. it.fileId = response.fileId.toInt()
  199. }
  200. .also { it.putIntoCache() }
  201. .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
  202. }
  203. is ImgStore.GroupPicUp.Response.RequireUpload -> {
  204. // val servers = response.uploadIpList.zip(response.uploadPortList)
  205. Highway.uploadResourceBdh(
  206. bot = bot,
  207. resource = resource,
  208. kind = GROUP_IMAGE,
  209. commandId = 2,
  210. initialTicket = response.uKey,
  211. noBdhAwait = true,
  212. fallbackSession = {
  213. BdhSession(
  214. EMPTY_BYTE_ARRAY, EMPTY_BYTE_ARRAY,
  215. ssoAddresses = response.uploadIpList.zip(response.uploadPortList).toMutableSet(),
  216. )
  217. },
  218. )
  219. return imageInfo.run {
  220. OfflineGroupImage(
  221. imageId = resource.calculateResourceId(),
  222. width = width,
  223. height = height,
  224. imageType = imageType,
  225. size = resource.size
  226. )
  227. }.also { it.fileId = response.fileId.toInt() }
  228. .also { it.putIntoCache() }
  229. .also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
  230. }
  231. }
  232. }
  233. }
  234. @Suppress("OverridingDeprecatedMember", "DEPRECATION")
  235. override suspend fun uploadVoice(resource: ExternalResource): Voice = AudioToSilkService.convert(
  236. resource
  237. ).useAutoClose { res ->
  238. return bot.network.run {
  239. uploadAudioResource(res)
  240. // val body = resp?.loadAs(Cmd0x388.RspBody.serializer())
  241. // ?.msgTryupPttRsp
  242. // ?.singleOrNull()?.fileKey ?: error("Group voice highway transfer succeed but failed to find fileKey")
  243. Voice(
  244. "${res.md5.toUHexString("")}.amr",
  245. res.md5,
  246. res.size,
  247. res.voiceCodec,
  248. ""
  249. )
  250. }
  251. }
  252. private suspend fun uploadAudioResource(resource: ExternalResource) {
  253. kotlin.runCatching {
  254. val (_) = Highway.uploadResourceBdh(
  255. bot = bot,
  256. resource = resource,
  257. kind = GROUP_AUDIO,
  258. commandId = 29,
  259. extendInfo = PttStore.GroupPttUp.createTryUpPttPack(bot.id, id, resource)
  260. .toByteArray(Cmd0x388.ReqBody.serializer()),
  261. )
  262. }.recoverCatchingSuppressed {
  263. when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect(bot)) {
  264. is PttStore.GroupPttUp.Response.RequireUpload -> {
  265. tryServersUpload(
  266. bot,
  267. resp.uploadIpList.zip(resp.uploadPortList),
  268. resource.size,
  269. GROUP_AUDIO,
  270. ChannelKind.HTTP
  271. ) { ip, port ->
  272. Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey)
  273. }
  274. }
  275. }
  276. }.getOrThrow()
  277. }
  278. override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio = AudioToSilkService.convert(
  279. resource
  280. ).useAutoClose { res ->
  281. return bot.network.run {
  282. uploadAudioResource(res)
  283. // val body = resp?.loadAs(Cmd0x388.RspBody.serializer())
  284. // ?.msgTryupPttRsp
  285. // ?.singleOrNull()?.fileKey ?: error("Group voice highway transfer succeed but failed to find fileKey")
  286. OfflineAudioImpl(
  287. filename = "${res.md5.toUHexString("")}.amr",
  288. fileMd5 = res.md5,
  289. fileSize = res.size,
  290. codec = res.audioCodec,
  291. originalPtt = null,
  292. )
  293. }
  294. }
  295. override suspend fun setEssenceMessage(source: MessageSource): Boolean {
  296. checkBotPermission(MemberPermission.ADMINISTRATOR)
  297. val result = bot.network.run {
  298. TroopEssenceMsgManager.SetEssence(
  299. bot.client,
  300. [email protected],
  301. source.internalIds.first(),
  302. source.ids.first()
  303. ).sendAndExpect()
  304. }
  305. return result.success
  306. }
  307. override fun toString(): String = "Group($id)"
  308. }
  309. internal fun Group.addNewNormalMember(memberInfo: MemberInfo): NormalMemberImpl? {
  310. if (members.contains(memberInfo.uin)) return null
  311. return newNormalMember(memberInfo).also {
  312. members.delegate.add(it)
  313. }
  314. }
  315. internal fun Group.newNormalMember(memberInfo: MemberInfo): NormalMemberImpl {
  316. this.checkIsGroupImpl()
  317. return NormalMemberImpl(
  318. this,
  319. this.coroutineContext,
  320. memberInfo
  321. )
  322. }
  323. internal fun GroupImpl.newAnonymous(name: String, id: String): AnonymousMemberImpl {
  324. return AnonymousMemberImpl(
  325. this, this.coroutineContext,
  326. MemberInfoImpl(
  327. uin = 80000000L,
  328. nick = name,
  329. permission = MemberPermission.MEMBER,
  330. remark = "匿名",
  331. nameCard = name,
  332. specialTitle = "匿名",
  333. muteTimestamp = 0,
  334. anonymousId = id,
  335. )
  336. )
  337. }