Skip to content

Commit b8392c4

Browse files
authored
Refactor resource-related API to stream-based (#58)
* refactor(core): make resource process streaming * refactor(core): remove file size limitation of Int.MAX_VALUE * chore(core): arrange code * fix(core): correctly handle source close * refactor(milky): move Bot injection to MilkyContext init block * refactor: make `resolveUri` return MediaSource * refactor: make MediaSource abstract * feat: implement contextual, disposable scope * update(core): optimize ByteArrayMediaSource * refactor: use streaming API in Codec and command execution * docs: add warning for ffmpegPath config * feat: add more rich logging for MediaSource
1 parent 885715a commit b8392c4

43 files changed

Lines changed: 1297 additions & 613 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

acidify-core/src/commonMain/kotlin/org/ntqqrev/acidify/FileOps.kt

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package org.ntqqrev.acidify
22

3+
import org.ntqqrev.acidify.common.MediaSource
4+
import org.ntqqrev.acidify.common.MediaSource.Companion.toMediaSource
35
import org.ntqqrev.acidify.internal.service.file.*
4-
import org.ntqqrev.acidify.internal.util.md5
5-
import org.ntqqrev.acidify.internal.util.sha1
6-
import org.ntqqrev.acidify.internal.util.triSha1
6+
import org.ntqqrev.acidify.internal.util.MediaSourceMetadata
77
import org.ntqqrev.acidify.struct.BotGroupFileEntry
88
import org.ntqqrev.acidify.struct.BotGroupFileSystemList
99
import org.ntqqrev.acidify.struct.BotGroupFolderEntry
@@ -13,24 +13,25 @@ import org.ntqqrev.acidify.struct.BotGroupFolderEntry
1313
* 上传群文件
1414
* @param groupUin 群号
1515
* @param fileName 文件名
16-
* @param fileData 文件数据
17-
* @param parentFolderId 父文件夹 ID,默认为根目录 "/"
16+
* @param fileSource 文件数据源
17+
* @param parentFolderId 父文件夹 ID,默认为根目录 `/`
1818
* @return 文件 ID
1919
*/
2020
suspend fun AbstractBot.uploadGroupFile(
2121
groupUin: Long,
2222
fileName: String,
23-
fileData: ByteArray,
23+
fileSource: MediaSource,
2424
parentFolderId: String = "/"
2525
): String {
26+
val metadata = MediaSourceMetadata.from(fileSource)
2627
val uploadResp = client.callService(
2728
UploadGroupFile, UploadGroupFile.Req(
2829
groupUin = groupUin,
2930
fileName = fileName,
30-
fileSize = fileData.size.toLong(),
31-
fileMd5 = fileData.md5(),
32-
fileSha1 = fileData.sha1(),
33-
fileTriSha1 = fileData.triSha1(),
31+
fileSize = metadata.size,
32+
fileMd5 = metadata.md5,
33+
fileSha1 = metadata.sha1,
34+
fileTriSha1 = metadata.triSha1,
3435
parentFolderId = parentFolderId
3536
)
3637
)
@@ -40,7 +41,10 @@ suspend fun AbstractBot.uploadGroupFile(
4041
senderUin = uin,
4142
groupUin = groupUin,
4243
fileName = fileName,
43-
fileData = fileData,
44+
fileSource = fileSource,
45+
fileSize = metadata.size,
46+
fileMd5 = metadata.md5,
47+
md510M = metadata.md510M,
4448
fileId = uploadResp.fileId,
4549
fileKey = uploadResp.fileKey,
4650
checkKey = uploadResp.checkKey,
@@ -58,47 +62,66 @@ suspend fun AbstractBot.uploadGroupFile(
5862
return uploadResp.fileId
5963
}
6064

65+
/**
66+
* 上传群文件
67+
* @param groupUin 群号
68+
* @param fileName 文件名
69+
* @param fileData 文件原始字节数据
70+
* @param parentFolderId 父文件夹 ID,默认为根目录 `/`
71+
* @return 文件 ID
72+
*/
73+
suspend fun AbstractBot.uploadGroupFile(
74+
groupUin: Long,
75+
fileName: String,
76+
fileData: ByteArray,
77+
parentFolderId: String = "/"
78+
): String = uploadGroupFile(
79+
groupUin = groupUin,
80+
fileName = fileName,
81+
fileSource = fileData.toMediaSource(),
82+
parentFolderId = parentFolderId,
83+
)
84+
6185
/**
6286
* 上传私聊文件
6387
* @param friendUin 好友 QQ 号
6488
* @param fileName 文件名
65-
* @param fileData 文件数据
89+
* @param fileSource 文件数据源
6690
* @return 文件 ID
6791
*/
6892
suspend fun AbstractBot.uploadPrivateFile(
6993
friendUin: Long,
7094
fileName: String,
71-
fileData: ByteArray
95+
fileSource: MediaSource
7296
): String {
7397
val friendUid = getUidByUin(friendUin)
74-
val fileMd5 = fileData.md5()
75-
val fileSha1 = fileData.sha1()
76-
val md510M = fileData.copyOfRange(0, minOf(10002432, fileData.size)).md5()
77-
val fileTriSha1 = fileData.triSha1()
98+
val metadata = MediaSourceMetadata.from(fileSource)
99+
val md510M = metadata.md510M
78100

79101
val uploadResp = client.callService(
80102
UploadPrivateFile,
81103
UploadPrivateFile.Req(
82104
senderUid = uid,
83105
receiverUid = friendUid,
84106
fileName = fileName,
85-
fileSize = fileData.size,
86-
fileMd5 = fileMd5,
87-
fileSha1 = fileSha1,
107+
fileSize = metadata.size,
108+
fileMd5 = metadata.md5,
109+
fileSha1 = metadata.sha1,
88110
md510M = md510M,
89-
fileTriSha1 = fileTriSha1
111+
fileTriSha1 = metadata.triSha1
90112
)
91113
)
92114

93115
if (!uploadResp.fileExist) {
94116
client.highwayContext.uploadPrivateFile(
95117
receiverUin = friendUin,
96118
fileName = fileName,
97-
fileData = fileData,
98-
fileMd5 = fileMd5,
99-
fileSha1 = fileSha1,
119+
fileSource = fileSource,
120+
fileSize = metadata.size,
121+
fileMd5 = metadata.md5,
122+
fileSha1 = metadata.sha1,
100123
md510M = md510M,
101-
fileTriSha1 = fileTriSha1,
124+
fileTriSha1 = metadata.triSha1,
102125
fileId = uploadResp.fileId,
103126
uploadKey = uploadResp.uploadKey,
104127
uploadIpAndPorts = uploadResp.ipAndPorts
@@ -113,14 +136,31 @@ suspend fun AbstractBot.uploadPrivateFile(
113136
fileId = uploadResp.fileId,
114137
fileMd510M = md510M,
115138
fileName = fileName,
116-
fileSize = fileData.size.toLong(),
139+
fileSize = metadata.size,
117140
crcMedia = uploadResp.fileCrcMedia
118141
)
119142
)
120143

121144
return uploadResp.fileId
122145
}
123146

147+
/**
148+
* 上传私聊文件
149+
* @param friendUin 好友 QQ 号
150+
* @param fileName 文件名
151+
* @param fileData 文件完整字节数据
152+
* @return 文件 ID
153+
*/
154+
suspend fun AbstractBot.uploadPrivateFile(
155+
friendUin: Long,
156+
fileName: String,
157+
fileData: ByteArray
158+
): String = uploadPrivateFile(
159+
friendUin = friendUin,
160+
fileName = fileName,
161+
fileSource = fileData.toMediaSource(),
162+
)
163+
124164
/**
125165
* 获取私聊文件下载链接
126166
* @param friendUin 好友 QQ 号
@@ -290,4 +330,4 @@ suspend fun AbstractBot.deleteGroupFolder(
290330
) = client.callService(
291331
DeleteGroupFolder,
292332
DeleteGroupFolder.Req(groupUin, folderId)
293-
)
333+
)

acidify-core/src/commonMain/kotlin/org/ntqqrev/acidify/GroupOps.kt

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ import io.ktor.client.request.forms.*
66
import io.ktor.http.*
77
import kotlinx.coroutines.async
88
import kotlinx.coroutines.awaitAll
9+
import kotlinx.io.buffered
10+
import kotlinx.io.readByteArray
911
import kotlinx.serialization.json.Json
1012
import kotlinx.serialization.json.buildJsonObject
1113
import kotlinx.serialization.json.put
14+
import org.ntqqrev.acidify.common.MediaSource
15+
import org.ntqqrev.acidify.common.MediaSource.Companion.toMediaSource
1216
import org.ntqqrev.acidify.exception.WebApiException
1317
import org.ntqqrev.acidify.internal.json.*
1418
import org.ntqqrev.acidify.internal.service.group.*
19+
import org.ntqqrev.acidify.internal.util.MediaSourceMetadata
1520
import org.ntqqrev.acidify.internal.util.unescapeHttp
1621
import org.ntqqrev.acidify.message.BotEssenceMessageResult
1722
import org.ntqqrev.acidify.message.ImageFormat
@@ -33,6 +38,19 @@ suspend fun AbstractBot.setGroupName(
3338
SetGroupName.Req(groupUin, groupName)
3439
)
3540

41+
/**
42+
* 设置群头像
43+
* @param groupUin 群号
44+
* @param imageSource 群头像数据源
45+
*/
46+
suspend fun AbstractBot.setGroupAvatar(
47+
groupUin: Long,
48+
imageSource: MediaSource
49+
) {
50+
val metadata = MediaSourceMetadata.from(imageSource)
51+
client.highwayContext.uploadGroupAvatar(groupUin, imageSource, metadata.md5)
52+
}
53+
3654
/**
3755
* 设置群头像
3856
* @param groupUin 群号
@@ -41,7 +59,7 @@ suspend fun AbstractBot.setGroupName(
4159
suspend fun AbstractBot.setGroupAvatar(
4260
groupUin: Long,
4361
imageData: ByteArray
44-
) = client.highwayContext.uploadGroupAvatar(groupUin, imageData)
62+
) = setGroupAvatar(groupUin, imageData.toMediaSource())
4563

4664
/**
4765
* 设置群成员的群名片
@@ -199,10 +217,16 @@ suspend fun AbstractBot.getGroupAnnouncements(groupUin: Long): List<BotGroupAnno
199217
}
200218

201219
/**
202-
* 发送群公告
220+
* 发送群公告。
221+
*
222+
* 可选图片参数改为 [MediaSource],以便与其他媒体上传接口保持一致。
223+
* 若提供 [imageSource],则必须同时提供 [imageFormat]。
224+
* 当前群公告图片上传最终仍会走表单上传,因此内部会在发送前一次性读取图片内容。
225+
*
203226
* @param groupUin 群号
204227
* @param content 公告内容
205-
* @param imageData 公告图片数据(字节数组,可选,暂不支持)
228+
* @param imageSource 公告图片数据源,可为 `null`
229+
* @param imageFormat 公告图片格式;当 [imageSource] 不为 `null` 时必填
206230
* @param showEditCard 是否显示编辑名片提示
207231
* @param showTipWindow 是否显示提示窗口
208232
* @param confirmRequired 是否需要确认
@@ -212,16 +236,16 @@ suspend fun AbstractBot.getGroupAnnouncements(groupUin: Long): List<BotGroupAnno
212236
suspend fun AbstractBot.sendGroupAnnouncement(
213237
groupUin: Long,
214238
content: String,
215-
imageData: ByteArray? = null,
239+
imageSource: MediaSource?,
216240
imageFormat: ImageFormat? = null,
217241
showEditCard: Boolean = false,
218242
showTipWindow: Boolean = true,
219243
confirmRequired: Boolean = true,
220244
isPinned: Boolean = false,
221245
): String {
222-
val announceImage = if (imageData != null) {
246+
val announceImage = if (imageSource != null) {
223247
requireNotNull(imageFormat) { "imageFormat is required when imageData is provided" }
224-
uploadGroupAnnouncementImage(imageData, imageFormat)
248+
uploadGroupAnnouncementImage(imageSource.readByteArray(), imageFormat)
225249
} else {
226250
null
227251
}
@@ -268,6 +292,38 @@ suspend fun AbstractBot.sendGroupAnnouncement(
268292
return sendResp.noticeId
269293
}
270294

295+
/**
296+
* 发送群公告
297+
* @param groupUin 群号
298+
* @param content 公告内容
299+
* @param imageData 公告图片完整字节数据,可为 `null`
300+
* @param imageFormat 公告图片格式;当 [imageData] 不为 `null` 时必填
301+
* @param showEditCard 是否显示编辑名片提示
302+
* @param showTipWindow 是否显示提示窗口
303+
* @param confirmRequired 是否需要确认
304+
* @param isPinned 是否置顶
305+
* @return 公告 ID
306+
*/
307+
suspend fun AbstractBot.sendGroupAnnouncement(
308+
groupUin: Long,
309+
content: String,
310+
imageData: ByteArray? = null,
311+
imageFormat: ImageFormat? = null,
312+
showEditCard: Boolean = false,
313+
showTipWindow: Boolean = true,
314+
confirmRequired: Boolean = true,
315+
isPinned: Boolean = false,
316+
): String = sendGroupAnnouncement(
317+
groupUin = groupUin,
318+
content = content,
319+
imageSource = imageData?.toMediaSource(),
320+
imageFormat = imageFormat,
321+
showEditCard = showEditCard,
322+
showTipWindow = showTipWindow,
323+
confirmRequired = confirmRequired,
324+
isPinned = isPinned,
325+
)
326+
271327
private suspend fun AbstractBot.uploadGroupAnnouncementImage(
272328
imageData: ByteArray,
273329
imageFormat: ImageFormat

acidify-core/src/commonMain/kotlin/org/ntqqrev/acidify/SystemOps.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package org.ntqqrev.acidify
22

33
import kotlinx.coroutines.*
44
import kotlinx.coroutines.sync.withLock
5+
import org.ntqqrev.acidify.common.MediaSource
6+
import org.ntqqrev.acidify.common.MediaSource.Companion.toMediaSource
57
import org.ntqqrev.acidify.internal.proto.misc.UserInfoKey
68
import org.ntqqrev.acidify.internal.service.friend.FetchFriends
79
import org.ntqqrev.acidify.internal.service.group.FetchGroupMembers
810
import org.ntqqrev.acidify.internal.service.group.FetchGroups
911
import org.ntqqrev.acidify.internal.service.system.*
12+
import org.ntqqrev.acidify.internal.util.MediaSourceMetadata
1013
import org.ntqqrev.acidify.struct.*
1114

1215
/**
@@ -237,11 +240,20 @@ suspend fun AbstractBot.setFriendPin(friendUin: Long, isPinned: Boolean) =
237240
suspend fun AbstractBot.setGroupPin(groupUin: Long, isPinned: Boolean) =
238241
client.callService(SetGroupPin, SetGroupPin.Req(groupUin, isPinned))
239242

243+
/**
244+
* 设置账号头像
245+
* @param imageSource 头像数据源
246+
*/
247+
suspend fun AbstractBot.setAvatar(imageSource: MediaSource) {
248+
val metadata = MediaSourceMetadata.from(imageSource)
249+
client.highwayContext.uploadAvatar(imageSource, metadata.md5)
250+
}
251+
240252
/**
241253
* 设置账号头像
242254
* @param imageData 头像原始字节数据
243255
*/
244-
suspend fun AbstractBot.setAvatar(imageData: ByteArray) = client.highwayContext.uploadAvatar(imageData)
256+
suspend fun AbstractBot.setAvatar(imageData: ByteArray) = setAvatar(imageData.toMediaSource())
245257

246258
/**
247259
* 设置账号昵称
@@ -290,4 +302,4 @@ suspend fun AbstractBot.getCookies(domain: String) = mapOf(
290302
/**
291303
* 获取 CSRF Token。
292304
*/
293-
suspend fun AbstractBot.getCsrfToken() = client.ticketContext.getCsrfToken()
305+
suspend fun AbstractBot.getCsrfToken() = client.ticketContext.getCsrfToken()

0 commit comments

Comments
 (0)