Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ピン止めメッセージの読み上げ #269

Merged
merged 1 commit into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import com.jaoafa.vcspeaker.tools.discord.DiscordExtensions.autoJoinEnabled
import com.jaoafa.vcspeaker.tools.discord.DiscordExtensions.isAfk
import com.jaoafa.vcspeaker.tools.discord.VoiceExtensions.join
import com.jaoafa.vcspeaker.tts.narrators.Narrators.narrator
import dev.kord.core.event.message.MessageCreateEvent
import dev.kordex.core.checks.anyGuild
import dev.kordex.core.checks.isNotBot
import dev.kordex.core.extensions.Extension
import dev.kordex.core.extensions.event
import dev.kordex.core.utils.respond
import dev.kord.core.event.message.MessageCreateEvent
import io.github.oshai.kotlinlogging.KotlinLogging

class NewMessageEvent : Extension() {
Expand All @@ -29,6 +29,8 @@ class NewMessageEvent : Extension() {
if (!GuildStore.getTextChannels().contains(event.message.channelId)) return@action
if (event.message.content.startsWith(VCSpeaker.prefix)) return@action

logger.info { "Message Received: ${event.message.type}" }

val guild = event.getGuildOrNull() ?: return@action

val narratorActive = guild.narrator() != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import com.jaoafa.vcspeaker.tts.TrackType
import com.jaoafa.vcspeaker.tts.Voice
import com.jaoafa.vcspeaker.tts.narrators.Narrators.narrator
import com.jaoafa.vcspeaker.tts.processors.BaseProcessor
import dev.kordex.core.utils.addReaction
import dev.kordex.core.utils.deleteOwnReaction
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer
import dev.kord.common.annotation.KordVoice
import dev.kord.common.entity.Snowflake
Expand All @@ -20,6 +18,8 @@ import dev.kord.core.entity.Message
import dev.kord.core.entity.ReactionEmoji
import dev.kord.core.entity.channel.TextChannel
import dev.kord.voice.VoiceConnection
import dev.kordex.core.utils.addReaction
import dev.kordex.core.utils.deleteOwnReaction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -130,6 +130,7 @@ class Narrator @OptIn(KordVoice::class) constructor(

return processors.fold(text to voice) { (processText, processVoice), processor ->
val (processedText, processedVoice) = processor.process(message, processText, processVoice)
println("Processed by ${processor::class.simpleName}: $processedText [isCancelled=${processor.isCancelled()}, isImmediately=${processor.isImmediately()}]")
if (processor.isCancelled()) return null // キャンセルされた場合は、即座に null を返却。
if (processor.isImmediately()) return processedText to processedVoice // 即座に返す場合は、このProcessorを最後とし読み上げる

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import com.jaoafa.vcspeaker.StringUtils.substringByCodePoints
import com.jaoafa.vcspeaker.stores.VisionApiCounterStore
import com.jaoafa.vcspeaker.tools.VisionApi
import com.jaoafa.vcspeaker.tts.Voice
import dev.kordex.core.utils.download
import com.sksamuel.scrimage.ImmutableImage
import com.sksamuel.scrimage.nio.PngWriter
import dev.kord.core.behavior.reply
import dev.kord.core.entity.Message
import dev.kord.rest.builder.message.EmbedBuilder
import dev.kord.rest.builder.message.addFile
import dev.kordex.core.utils.download
import java.io.File
import java.nio.file.Path

class AttachmentProcessor : BaseProcessor() {
override val priority = 30
override val priority = 40

override suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice> {
val attachments = message?.attachments ?: return content to voice
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.jaoafa.vcspeaker.tts.Voice
import dev.kord.core.entity.Message

class CharLimitProcessor : BaseProcessor() {
override val priority = 110
override val priority = 120

override suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice> {
val limit = 180
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.jaoafa.vcspeaker.tts.Voice
import dev.kord.core.entity.Message

class IgnoreAfterReplaceProcessor : BaseProcessor() {
override val priority = 70
override val priority = 80

override suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice> {
val guildId = message?.getGuild()?.id ?: return content to voice
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.jaoafa.vcspeaker.tts.Voice
import dev.kord.core.entity.Message

class IgnoreBeforeReplaceProcessor : BaseProcessor() {
override val priority = 50
override val priority = 60

override suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice> {
val guildId = message?.getGuild()?.id ?: return content to voice
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package com.jaoafa.vcspeaker.tts.processors
import com.jaoafa.vcspeaker.tts.Voice
import com.jaoafa.vcspeaker.tts.api.Emotion
import com.jaoafa.vcspeaker.tts.api.Speaker
import dev.kordex.core.utils.capitalizeWords
import dev.kord.core.entity.Message
import dev.kordex.core.utils.capitalizeWords

class InlineVoiceProcessor : BaseProcessor() {
override val priority = 40
override val priority = 50

private val syntax = Regex("(speaker|emotion|emotion_level|pitch|speed|volume):\\w+")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.jaoafa.vcspeaker.tts.markdown.toMarkdown
import dev.kord.core.entity.Message

class MarkdownFormatProcessor : BaseProcessor() {
override val priority = 80
override val priority = 90

override suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice> {
val markdown = content.toMarkdown()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.jaoafa.vcspeaker.tts.processors

import com.jaoafa.vcspeaker.tts.Voice
import dev.kord.common.entity.MessageType
import dev.kord.core.entity.Message
import dev.kord.core.entity.effectiveName

class PinMessageProcessor : BaseProcessor() {
override val priority = 20

override suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice> {
if (message?.type != MessageType.ChannelPinnedMessage) return content to voice

val executorUserName = message.author?.effectiveName ?: "だれか"

val messageReference = message.messageReference ?: return content to voice
val pinnedMessageId = messageReference.message?.id ?: return content to voice
val pinnedMessage = message.channel.getMessage(pinnedMessageId)
val pinnedMessageUserName = pinnedMessage.author?.effectiveName ?: "だれか"

return "$executorUserName が $pinnedMessageUserName のメッセージをピン止めしました" to voice
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.jaoafa.vcspeaker.tts.replacers.BaseReplacer
import dev.kord.core.entity.Message

class ReplacerProcessor : BaseProcessor() {
override val priority = 60
override val priority = 70

val replacers = getClassesIn<BaseReplacer>("com.jaoafa.vcspeaker.tts.replacers")
.mapNotNull {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.jaoafa.vcspeaker.tts.processors

import com.jaoafa.vcspeaker.tts.Voice
import dev.kord.common.entity.MessageType
import dev.kord.core.entity.Message
import dev.kord.core.entity.effectiveName

class ReplyProcessor : BaseProcessor() {
override val priority = 10

override suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice> {
val referencedMessage = message?.referencedMessage ?: return content to voice
if (message?.type != MessageType.Reply) return content to voice

val referencedMessage = message.referencedMessage ?: return content to voice
val replyToName = referencedMessage.author?.effectiveName ?: "だれか"
return "$replyToName への返信、$content" to voice
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.jaoafa.vcspeaker.tts.Voice
import dev.kord.core.entity.Message

class StickerProcessor : BaseProcessor() {
override val priority = 20
override val priority = 30

override suspend fun process(message: Message?, content: String, voice: Voice): Pair<String, Voice> {
val stickers = message?.stickers ?: return content to voice
Expand Down
84 changes: 84 additions & 0 deletions src/test/kotlin/processors/PinMessageProcessorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package processors

import com.jaoafa.vcspeaker.tts.Voice
import com.jaoafa.vcspeaker.tts.api.Speaker
import com.jaoafa.vcspeaker.tts.processors.PinMessageProcessor
import dev.kord.common.entity.MessageType
import dev.kord.common.entity.Snowflake
import dev.kord.core.entity.Message
import dev.kord.core.entity.MessageReference
import dev.kord.core.entity.effectiveName
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk

/**
* PinMessageProcessor のテスト
*/
class PinMessageProcessorTest : FunSpec({
// テスト後にモックを削除
afterTest {
clearAllMocks()
}

// メッセージにピン止めするとき、ピン止めした旨を読み上げる
test("If the message is a pinned notify message, read that the message is pinned.") {
val message = mockk<Message>()

every { message.type } returns MessageType.ChannelPinnedMessage

val messageReference = mockk<MessageReference>()
every { messageReference.message?.id } returns Snowflake(123)

every { message.author?.effectiveName } returns "User"
every { message.messageReference } returns messageReference

val pinnedMessage = mockk<Message>()
every { pinnedMessage.author?.effectiveName } returns "PinnedUser"
coEvery { message.channel.getMessage(Snowflake(123)) } returns pinnedMessage

val voice = Voice(speaker = Speaker.Hikari)

val (processedText, processedVoice) = PinMessageProcessor().process(message, "", voice)
processedText shouldBe "User が PinnedUser のメッセージをピン止めしました"
processedVoice shouldBe voice
}

// 返信メッセージでない場合、そのまま読み上げる
test("If the message is not a pinned notify message, read it as is.") {
val message = mockk<Message>()
every { message.type } returns MessageType.Default
every { message.referencedMessage } returns null

val voice = Voice(speaker = Speaker.Hikari)
val (processedText, processedVoice) = PinMessageProcessor().process(message, "", voice)
processedText shouldBe ""
processedVoice shouldBe voice
}

// 作者のないメッセージをピン止めすると、不明なメッセージをピン止めしたものとして読み上げる
test("If the message is a pinned notify message but the author of the pinned message is unknown, read it as a pinned to an unknown author.") {
val message = mockk<Message>()

every { message.type } returns MessageType.ChannelPinnedMessage

val messageReference = mockk<MessageReference>()
every { messageReference.message?.id } returns Snowflake(123)

every { message.author?.effectiveName } returns "User"
every { message.messageReference } returns messageReference

val pinnedMessage = mockk<Message>()
every { pinnedMessage.author } returns null
coEvery { message.channel.getMessage(Snowflake(123)) } returns pinnedMessage

val voice = Voice(speaker = Speaker.Hikari)

val (processedText, processedVoice) = PinMessageProcessor().process(message, "", voice)
processedText shouldBe "User が だれか のメッセージをピン止めしました"
processedVoice shouldBe voice
}
})
4 changes: 4 additions & 0 deletions src/test/kotlin/processors/ReplyProcessorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package processors
import com.jaoafa.vcspeaker.tts.Voice
import com.jaoafa.vcspeaker.tts.api.Speaker
import com.jaoafa.vcspeaker.tts.processors.ReplyProcessor
import dev.kord.common.entity.MessageType
import dev.kord.core.entity.Message
import dev.kord.core.entity.effectiveName
import io.kotest.core.spec.style.FunSpec
Expand All @@ -23,6 +24,7 @@ class ReplyProcessorTest : FunSpec({
// メッセージに返信するとき、返信先のユーザー名を読み上げる
test("If the message is a reply, read the author name of the replied message.") {
val message = mockk<Message>()
every { message.type } returns MessageType.Reply
every { message.referencedMessage } returns mockk {
every { author?.effectiveName } returns "User"
}
Expand All @@ -37,6 +39,7 @@ class ReplyProcessorTest : FunSpec({
// 返信メッセージでない場合、そのまま読み上げる
test("If the message is not a reply, read it as is.") {
val message = mockk<Message>()
every { message.type } returns MessageType.Default
every { message.referencedMessage } returns null

val voice = Voice(speaker = Speaker.Hikari)
Expand All @@ -48,6 +51,7 @@ class ReplyProcessorTest : FunSpec({
// 作者のないメッセージに返信すると、不明な返信として読み上げる
test("If the message is a reply but the author of the replied message is unknown, read it as a reply to an unknown author.") {
val message = mockk<Message>()
every { message.type } returns MessageType.Reply
every { message.referencedMessage } returns mockk {
every { author?.globalName } returns null
every { author?.username } returns null
Expand Down
Loading