@ExtensionPointMethod
    @Override
    public Mono<Map<String, String>> queryMessageAttachmentDownloadInfo(
            Long requesterId,
            @Nullable Long messageAttachmentIdNum,
            @Nullable String messageAttachmentIdStr,
            @NotNull List<Value> customAttributes) {
        if (!isServing) {
            return ResponseExceptionPublisherPool.serverUnavailable();
        }
        if (messageAttachmentIdNum == null) {
            return Mono.error(ResponseException.get(ResponseStatusCode.ILLEGAL_ARGUMENT,
                    "The message attachment ID must not be null"));
        }
        return messageAttachmentRepository
                .findUploaderIdAndSharedWithUserIdsAndGroupIds(messageAttachmentIdNum)
                .flatMap(attachment -> {
                    if (!attachment.getUploaderId()
                            .equals(requesterId)
                            && !CollectionUtil.contains(attachment.getSharedWithUserIds(),
                                    requesterId)) {
                        List<Long> sharedWithGroupIds = attachment.getSharedWithGroupIds();
                        if (sharedWithGroupIds == null || sharedWithGroupIds.isEmpty()) {
                            return ERROR_NOT_UPLOADER_OR_SHARED_WITH_USER_TO_DOWNLOAD_MESSAGE_ATTACHMENT;
                        }
                        return groupMemberService
                                .isGroupMember(CollectionUtil.toSet(sharedWithGroupIds),
                                        requesterId)
                                .flatMap(isGroupMember -> {
                                    if (!isGroupMember) {
                                        return ERROR_NOT_UPLOADER_OR_SHARED_WITH_USER_TO_DOWNLOAD_MESSAGE_ATTACHMENT;
                                    }
                                    return getPresignedDownloadUrl(
                                            getBucketName(MESSAGE_ATTACHMENT),
                                            getMessageAttachmentObjectKey(messageAttachmentIdNum),
                                            storageProperties.getMessageAttachment()
                                                    .getDownloadUrlExpireAfterSeconds())
                                            .map(url -> Map.of(RESOURCE_URL, url));
                                });
                    }
                    return getPresignedDownloadUrl(getBucketName(MESSAGE_ATTACHMENT),
                            getMessageAttachmentObjectKey(messageAttachmentIdNum),
                            storageProperties.getMessageAttachment()
                                    .getDownloadUrlExpireAfterSeconds())
                            .map(url -> Map.of(RESOURCE_URL, url));
                })
                .switchIfEmpty(
                        ERROR_NOT_UPLOADER_OR_SHARED_WITH_USER_TO_DOWNLOAD_MESSAGE_ATTACHMENT);
    }
@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