mirror of
https://github.com/pischule/memevizor.git
synced 2025-12-19 06:56:42 +00:00
Implement videos
This commit is contained in:
@@ -4,14 +4,17 @@ import com.github.kotlintelegrambot.Bot
|
||||
import com.github.kotlintelegrambot.bot
|
||||
import com.github.kotlintelegrambot.dispatch
|
||||
import com.github.kotlintelegrambot.dispatcher.command
|
||||
import com.github.kotlintelegrambot.dispatcher.handlers.media.MediaHandlerEnvironment
|
||||
import com.github.kotlintelegrambot.dispatcher.message
|
||||
import com.github.kotlintelegrambot.dispatcher.photos
|
||||
import com.github.kotlintelegrambot.dispatcher.video
|
||||
import com.github.kotlintelegrambot.entities.ChatId
|
||||
import com.github.kotlintelegrambot.entities.Message
|
||||
import com.github.kotlintelegrambot.entities.ParseMode
|
||||
import com.pischule.memevizor.bot.handler.PhotoHandlerService
|
||||
import com.pischule.memevizor.bot.handler.MediaHandlerService
|
||||
import com.pischule.memevizor.bot.handler.ThisCommandHandlerService
|
||||
import com.pischule.memevizor.util.getMaxResPhotoId
|
||||
import com.pischule.memevizor.util.getVideoFileId
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
@@ -25,7 +28,7 @@ private val logger = KotlinLogging.logger {}
|
||||
class BotConfiguration(
|
||||
private val botProps: BotProps,
|
||||
private val thisCommandHandlerService: ThisCommandHandlerService,
|
||||
private val photoHandlerService: PhotoHandlerService,
|
||||
private val mediaHandlerService: MediaHandlerService,
|
||||
) {
|
||||
|
||||
@Bean
|
||||
@@ -46,15 +49,8 @@ class BotConfiguration(
|
||||
}
|
||||
}
|
||||
}
|
||||
photos {
|
||||
withLoggingContext(messageContext(message)) {
|
||||
try {
|
||||
photoHandlerService.create(this)
|
||||
} catch (e: Error) {
|
||||
logger.error(e) { "Error while handling photo" }
|
||||
}
|
||||
}
|
||||
}
|
||||
photos { handleMedia() }
|
||||
video { handleMedia() }
|
||||
command("whoami") {
|
||||
withLoggingContext(messageContext(message)) {
|
||||
bot.sendMessage(
|
||||
@@ -67,12 +63,22 @@ class BotConfiguration(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MediaHandlerEnvironment<*>.handleMedia() {
|
||||
withLoggingContext(messageContext(message)) {
|
||||
try {
|
||||
mediaHandlerService.create(this)
|
||||
} catch (e: Error) {
|
||||
logger.error(e) { "Error while handling photo" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun messageContext(message: Message): Map<String, String?> =
|
||||
mapOf(
|
||||
"message_id" to message.messageId.toString(),
|
||||
"chat_id" to message.chat.id.toString(),
|
||||
"from_user_id" to message.from?.id.toString(),
|
||||
"from_user_username" to message.from?.username.toString(),
|
||||
"file_id" to message.getMaxResPhotoId(),
|
||||
"file_id" to (message.getMaxResPhotoId() ?: message.getVideoFileId()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.pischule.memevizor.bot.handler
|
||||
|
||||
import com.github.kotlintelegrambot.dispatcher.handlers.media.MediaHandlerEnvironment
|
||||
import com.github.kotlintelegrambot.entities.ChatId
|
||||
import com.github.kotlintelegrambot.entities.files.PhotoSize
|
||||
import com.github.kotlintelegrambot.entities.reaction.ReactionType
|
||||
import com.pischule.memevizor.bot.BotProps
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
@@ -11,21 +10,21 @@ import org.springframework.stereotype.Service
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class PhotoHandlerService(private val botProps: BotProps) {
|
||||
class MediaHandlerService(private val botProps: BotProps) {
|
||||
|
||||
suspend fun create(env: MediaHandlerEnvironment<List<PhotoSize>>) {
|
||||
suspend fun create(env: MediaHandlerEnvironment<*>) {
|
||||
if (shouldForwardMessage(env)) {
|
||||
forwardPhotoMessage(env)
|
||||
forwardMessage(env)
|
||||
}
|
||||
|
||||
reactToMessage(env, "👀")
|
||||
}
|
||||
|
||||
private fun shouldForwardMessage(env: MediaHandlerEnvironment<List<PhotoSize>>): Boolean {
|
||||
private fun shouldForwardMessage(env: MediaHandlerEnvironment<*>): Boolean {
|
||||
return env.message.chat.id != botProps.forwardChatId
|
||||
}
|
||||
|
||||
private suspend fun forwardPhotoMessage(env: MediaHandlerEnvironment<List<PhotoSize>>) {
|
||||
private suspend fun forwardMessage(env: MediaHandlerEnvironment<*>) {
|
||||
env.bot
|
||||
.forwardMessage(
|
||||
chatId = ChatId.fromId(botProps.forwardChatId),
|
||||
@@ -38,10 +37,7 @@ class PhotoHandlerService(private val botProps: BotProps) {
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun reactToMessage(
|
||||
env: MediaHandlerEnvironment<List<PhotoSize>>,
|
||||
emoji: String,
|
||||
) {
|
||||
private suspend fun reactToMessage(env: MediaHandlerEnvironment<*>, emoji: String) {
|
||||
env.bot
|
||||
.setMessageReaction(
|
||||
chatId = ChatId.fromId(env.message.chat.id),
|
||||
@@ -2,10 +2,12 @@ package com.pischule.memevizor.bot.handler
|
||||
|
||||
import com.github.kotlintelegrambot.dispatcher.handlers.MessageHandlerEnvironment
|
||||
import com.github.kotlintelegrambot.entities.ChatId
|
||||
import com.github.kotlintelegrambot.entities.Message
|
||||
import com.github.kotlintelegrambot.entities.reaction.ReactionType
|
||||
import com.pischule.memevizor.bot.BotProps
|
||||
import com.pischule.memevizor.upload.FileUploaderService
|
||||
import com.pischule.memevizor.util.getMaxResPhotoId
|
||||
import com.pischule.memevizor.util.getVideoFileId
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||
import org.springframework.stereotype.Component
|
||||
@@ -22,24 +24,34 @@ class ThisCommandHandlerService(
|
||||
suspend fun create(env: MessageHandlerEnvironment) {
|
||||
if (!shouldHandleMessage(env)) return
|
||||
|
||||
val maxResPhotoId = env.message.replyToMessage?.getMaxResPhotoId() ?: return
|
||||
val replyToMessage = env.message.replyToMessage ?: return
|
||||
|
||||
withLoggingContext("file_id" to maxResPhotoId) {
|
||||
val fileBytes = env.bot.downloadFileBytes(maxResPhotoId) ?: return
|
||||
logger.info { "Downloaded a file from Telegram" }
|
||||
val (fileId, contentType) = getFileInfo(replyToMessage) ?: return
|
||||
|
||||
fileUploaderService.uploadFile(fileBytes, "_.jpeg", "image/jpeg")
|
||||
withLoggingContext("file_id" to fileId) {
|
||||
val fileBytes = env.bot.downloadFileBytes(fileId) ?: return
|
||||
logger.info { "Downloaded a file from Telegram, size=${fileBytes.size}" }
|
||||
|
||||
fileUploaderService.uploadFile(fileBytes, "_", contentType)
|
||||
|
||||
reactToMessage(env, "👍")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileInfo(message: Message): Pair<String, String>? {
|
||||
return message.getMaxResPhotoId()?.let { it to "image/jpeg" }
|
||||
?: message.getVideoFileId()?.let { it to "video/mp4" }
|
||||
}
|
||||
|
||||
private fun shouldHandleMessage(env: MessageHandlerEnvironment): Boolean {
|
||||
val isApprover = env.message.from?.id?.let { botProps.approverUserIds.contains(it) } == true
|
||||
val command = env.message.text?.lowercase()
|
||||
val isConfirmCommand = command in confirmCommands
|
||||
val hasPhotoReply = env.message.replyToMessage?.photo?.isNotEmpty() == true
|
||||
return isApprover && isConfirmCommand && hasPhotoReply
|
||||
val hasMediaReply =
|
||||
env.message.replyToMessage?.let {
|
||||
it.photo?.isNotEmpty() == true || it.video != null
|
||||
} == true
|
||||
return isApprover && isConfirmCommand && hasMediaReply
|
||||
}
|
||||
|
||||
private suspend fun reactToMessage(env: MessageHandlerEnvironment, emoji: String) {
|
||||
|
||||
@@ -10,7 +10,7 @@ private val logger = KotlinLogging.logger {}
|
||||
@Service
|
||||
class DummyFileUploadService() : FileUploaderService {
|
||||
|
||||
override suspend fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String) {
|
||||
override fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String) {
|
||||
logger.info { "File $filename has been successfully uploaded to nowhere" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
interface FileUploaderService {
|
||||
suspend fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String)
|
||||
fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.pischule.memevizor.upload
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.event.EventListener
|
||||
@@ -17,7 +16,7 @@ class IndexInitializer(val fileUploaderService: FileUploaderService) {
|
||||
fun applicationStartedHandler(event: ApplicationStartedEvent) {
|
||||
try {
|
||||
val fileBytes = readResourceAsByteArray("static/index.html")
|
||||
runBlocking { fileUploaderService.uploadFile(fileBytes, "index.html", "text/html") }
|
||||
fileUploaderService.uploadFile(fileBytes, "index.html", "text/html")
|
||||
} catch (e: Error) {
|
||||
logger.warn(e) { "Failed to upload " }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.pischule.memevizor.upload
|
||||
|
||||
import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
|
||||
import aws.sdk.kotlin.services.s3.S3Client
|
||||
import aws.smithy.kotlin.runtime.net.url.Url
|
||||
import io.minio.MinioClient
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
@@ -13,14 +11,10 @@ import org.springframework.context.annotation.Profile
|
||||
@Configuration
|
||||
class S3Config {
|
||||
@Bean
|
||||
fun s3Client(s3Props: S3Props): S3Client {
|
||||
return S3Client {
|
||||
endpointUrl = Url.parse("https://storage.yandexcloud.net")
|
||||
region = "ru-central1"
|
||||
credentialsProvider = StaticCredentialsProvider {
|
||||
accessKeyId = s3Props.accessKeyId
|
||||
secretAccessKey = s3Props.secretAccessKey
|
||||
}
|
||||
}
|
||||
}
|
||||
fun s3Client(s3Props: S3Props): MinioClient =
|
||||
MinioClient.builder()
|
||||
.endpoint("https://storage.yandexcloud.net")
|
||||
.region("ru-central1")
|
||||
.credentials(s3Props.accessKeyId, s3Props.secretAccessKey)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.pischule.memevizor.upload
|
||||
|
||||
import aws.sdk.kotlin.services.s3.S3Client
|
||||
import aws.sdk.kotlin.services.s3.model.PutObjectRequest
|
||||
import aws.smithy.kotlin.runtime.content.ByteStream
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.minio.MinioClient
|
||||
import io.minio.PutObjectArgs
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@@ -11,16 +10,17 @@ private val logger = KotlinLogging.logger {}
|
||||
|
||||
@ConditionalOnBean(S3Config::class)
|
||||
@Service
|
||||
class S3FileUploaderService(private val s3Client: S3Client, private val s3Props: S3Props) :
|
||||
class S3FileUploaderService(private val s3Client: MinioClient, private val s3Props: S3Props) :
|
||||
FileUploaderService {
|
||||
override suspend fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String) {
|
||||
override fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String) {
|
||||
logger.info { "Before upload" }
|
||||
s3Client.putObject(
|
||||
PutObjectRequest {
|
||||
body = ByteStream.fromBytes(fileBytes)
|
||||
bucket = s3Props.bucket
|
||||
key = filename
|
||||
this.contentType = contentType
|
||||
}
|
||||
PutObjectArgs.builder()
|
||||
.bucket(s3Props.bucket)
|
||||
.`object`(filename)
|
||||
.stream(fileBytes.inputStream(), fileBytes.size.toLong(), -1)
|
||||
.contentType(contentType)
|
||||
.build()
|
||||
)
|
||||
logger.info { "File $filename has been uploaded to S3" }
|
||||
}
|
||||
|
||||
@@ -3,3 +3,5 @@ package com.pischule.memevizor.util
|
||||
import com.github.kotlintelegrambot.entities.Message
|
||||
|
||||
fun Message.getMaxResPhotoId(): String? = this.photo?.lastOrNull()?.fileId
|
||||
|
||||
fun Message.getVideoFileId(): String? = this.video?.fileId
|
||||
|
||||
@@ -27,22 +27,6 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: var(--bg-image);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
filter: blur(20px);
|
||||
transform: scale(1.1);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -51,57 +35,52 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img id="image" alt="Смешная картинка" src="">
|
||||
<img id="qr-code" class="qr-code" alt="QR Code" src="qr.svg" onerror="this.style.display='none'">
|
||||
</body>
|
||||
<script defer>
|
||||
const imageUrl = '_.jpeg';
|
||||
const fileUrl = '_';
|
||||
const refreshIntervalMs = 30_000; // Time in milliseconds (e.g., 10000 = 10 seconds)
|
||||
|
||||
function configureQrCode() {
|
||||
// Parse query parameters for QR code size
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const qrSize = parseInt(urlParams.get('qrSize')) || 100; // Default to 100px if not specified
|
||||
const qrOffset = parseInt(urlParams.get('qrOffset')) || 20; // Default to 20px if not specified
|
||||
|
||||
const qrCodeElement = document.getElementById('qr-code');
|
||||
qrCodeElement.style.width = `${qrSize}px`;
|
||||
qrCodeElement.style.height = `${qrSize}px`;
|
||||
qrCodeElement.style.bottom = `${qrOffset}px`;
|
||||
qrCodeElement.style.right = `${qrOffset}px`;
|
||||
}
|
||||
|
||||
// Configure QR code on page load
|
||||
configureQrCode();
|
||||
|
||||
let lastModified = null;
|
||||
const imageElement = document.getElementById('image');
|
||||
let imageBlobUrl = null;
|
||||
|
||||
function updateBackgroundImage(imageUrl) {
|
||||
document.documentElement.style.setProperty(
|
||||
'--bg-image',
|
||||
`linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('${imageUrl}')`
|
||||
);
|
||||
const mediaContainer = document.querySelector("body")
|
||||
|
||||
async function refreshMediaDom(response) {
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType === "video/mp4") {
|
||||
const video = document.createElement("video");
|
||||
video.src = blobUrl;
|
||||
video.controls = true;
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.autoplay = true;
|
||||
mediaContainer.replaceChildren(video);
|
||||
} else {
|
||||
const img = document.createElement("img");
|
||||
img.src = blobUrl;
|
||||
mediaContainer.replaceChildren(img);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshImage() {
|
||||
async function refreshMedia() {
|
||||
try {
|
||||
const headers = new Headers();
|
||||
if (lastModified != null) {
|
||||
headers.append('If-Modified-Since', lastModified);
|
||||
}
|
||||
|
||||
const response = await fetch(imageUrl, {method: 'GET', headers, cache: 'no-store'});
|
||||
const response = await fetch(fileUrl, {method: 'GET', headers, cache: 'no-store'});
|
||||
const now = new Date().toLocaleTimeString();
|
||||
|
||||
if (response.status === 304) {
|
||||
@@ -113,10 +92,7 @@
|
||||
const newLastModified = response.headers.get('last-modified');
|
||||
if (newLastModified) {
|
||||
lastModified = newLastModified;
|
||||
const imageBlob = await response.blob();
|
||||
imageBlobUrl = URL.createObjectURL(imageBlob);
|
||||
imageElement.src = imageBlobUrl;
|
||||
updateBackgroundImage(imageBlobUrl)
|
||||
await refreshMediaDom(response)
|
||||
} else {
|
||||
console.warn(`No Last-Modified header found. Cannot perform conditional checks`);
|
||||
}
|
||||
@@ -129,13 +105,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
refreshImage();
|
||||
setInterval(refreshImage, refreshIntervalMs);
|
||||
document.addEventListener('click', refreshImage);
|
||||
refreshMedia();
|
||||
setInterval(refreshMedia, refreshIntervalMs);
|
||||
document.addEventListener('click', refreshMedia);
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.code === 'Space') {
|
||||
event.preventDefault();
|
||||
refreshImage();
|
||||
refreshMedia();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user