private Mono<Map<String, String>> queryMessageAttachmentUploadInfo(
            Long requesterId,
            @Nullable Long userId,
            @Nullable Long groupId,
            @Nullable String name,
            @Nullable MediaType mediaType) {
        if (!isServing) {
            return ResponseExceptionPublisherPool.serverUnavailable();
        }
        if (StringUtil.isBlank(name)) {
            return Mono.error(ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
                    "The resource name must not be null or blank"));
        }
        if (mediaType == null || !mediaType.isConcrete()) {
            return Mono.error(ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
                    "The resource media type must not be null and must be a concrete media type"));
        }
        long id = node.nextLargeGapId(ServiceType.STORAGE_MESSAGE_ATTACHMENT);
        StorageItemProperties itemProperties = storageProperties.getMessageAttachment();
        List<Long> sharedWithUserIds;
        List<Long> associatedGroupIds;
        Mono<Void> authorize;
        if (groupId == null) {
            if (userId == null) {
                return Mono.error(ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
                        "The user ID or group ID to share with must not both be null"));
            }
            sharedWithUserIds = List.of(userId);
            associatedGroupIds = null;
            authorize = userRelationshipService
                    .hasRelationshipAndNotBlocked(userId, requesterId, false)
                    .flatMap(hasRelationshipAndNotBlocked -> hasRelationshipAndNotBlocked
                            ? Mono.empty()
                            : Mono.error(ResponseException.get(
                                    ResponseStatusCode.NOT_FRIEND_TO_UPLOAD_MESSAGE_ATTACHMENT_IN_PRIVATE_CONVERSATION)));
        } else {
            sharedWithUserIds = null;
            associatedGroupIds = List.of(groupId);
            // TODO: check mute status
            authorize = groupMemberService.isGroupMember(groupId, requesterId, false)
                    .flatMap(isGroupMember -> isGroupMember
                            ? Mono.empty()
                            : Mono.error(ResponseException.get(
                                    ResponseStatusCode.NOT_GROUP_MEMBER_TO_UPLOAD_MESSAGE_ATTACHMENT_IN_GROUP_CONVERSATION)));
        }
        return authorize.then(Mono.defer(() -> {
            Map<String, String> uploadInfo =
                    getResourceUploadInfo(getBucketName(MESSAGE_ATTACHMENT),
                            Long.toString(id),
                            itemProperties.getAllowedContentType(),
                            itemProperties.getMinSizeBytes(),
                            itemProperties.getMaxSizeBytes(),
                            itemProperties.getUploadUrlExpireAfterSeconds());
            MessageAttachment attachment = new MessageAttachment(
                    id,
                    name,
                    mediaType.toString(),
                    requesterId,
                    new Date(),
                    sharedWithUserIds,
                    associatedGroupIds);
            return messageAttachmentRepository.insert(attachment)
                    .thenReturn(uploadInfo);
        }));
    }
@implNote 1. We use HMAC(key, message) instead of a HASH(key + message) to avoid the length
          extension attack. To put simply, if a hacker knows the signature of the resource
          "1", and he can also know the signature of resource "12", "13", "123", and so on
          without knowledge of the key.
          <p>
          2. Use MD5 because its output size (128 bits) is small, and it is a 22-character
          Base62-encoded string.
turms-plugins/turms-plugin-minio/src/main/java/im/turms/plugin/minio/MinioStorageServiceProvider.java