Skip to content
Closed
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
10 changes: 9 additions & 1 deletion cmd/service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func main() {

newGreetingDetector,
newFloodDetector,
newShortVoiceDetector,

newOwnerWhitelist,
newBusinessConnectionStore,
Expand Down Expand Up @@ -100,6 +101,12 @@ func newFloodDetector(cfg *config.Config, store repository.MessageWindowStore) *
}, store)
}

func newShortVoiceDetector(cfg *config.Config) *service.ShortVoiceDetector {
return service.NewShortVoiceDetector(service.ShortVoiceDetectorConfig{
MaxDuration: cfg.ShortVoice.MaxDuration,
})
}

func newOwnerWhitelist(cfg *config.Config) repository.OwnerWhitelist {
return memory.NewOwnerWhitelist(cfg.AllowedOwners)
}
Expand Down Expand Up @@ -135,6 +142,7 @@ func newLLMClient(c deepseek.Config) repository.LLMClient {

func newHandleBusinessMessageConfig(cfg *config.Config) handle_business_message.Config {
return handle_business_message.Config{
SystemPrompt: cfg.Bot.SystemPrompt,
SystemPrompt: cfg.Bot.SystemPrompt,
ShortVoicePrompt: cfg.Bot.ShortVoicePrompt,
}
}
1 change: 1 addition & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ DEEPSEEK_API_KEY=guesswhatisit!
DEEPSEEK_MODEL=deepseek-v4-pro

BOT_SYSTEM_PROMPT="You are breathtaking!"
BOT_SHORT_VOICE_PROMPT="Short voice messages is corruption of mankind!"

ALLOWED_OWNERS=2281489
GREETINGS=hi,hello,sup,yo
Expand Down
9 changes: 9 additions & 0 deletions internal/domain/model/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ package model

import "time"

type MessageKind string

const (
MessageKindText MessageKind = "text"
MessageKindVoice MessageKind = "voice"
)

type IncomingMessage struct {
BusinessConnectionID string
OwnerID int64
GuestID int64
Kind MessageKind
Text string
VoiceDuration time.Duration
ReceivedAt time.Time
}

Expand Down
7 changes: 4 additions & 3 deletions internal/domain/model/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package model
type TriggerKind string

const (
TriggerKindNone TriggerKind = "none"
TriggerKindGreeting TriggerKind = "greeting"
TriggerKindFlood TriggerKind = "flood"
TriggerKindNone TriggerKind = "none"
TriggerKindGreeting TriggerKind = "greeting"
TriggerKindFlood TriggerKind = "flood"
TriggerKindShortVoice TriggerKind = "short_voice"
)

type TriggerDecision struct {
Expand Down
35 changes: 35 additions & 0 deletions internal/domain/service/short_voice_detector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package service

import (
"noirbot/internal/domain/model"
"time"
)

type ShortVoiceDetectorConfig struct {
MaxDuration time.Duration
}

type ShortVoiceDetector struct {
cfg ShortVoiceDetectorConfig
}

func NewShortVoiceDetector(cfg ShortVoiceDetectorConfig) *ShortVoiceDetector {
return &ShortVoiceDetector{
cfg: cfg,
}
}

func (d *ShortVoiceDetector) Detect(msg model.IncomingMessage) model.TriggerDecision {
if msg.Kind != model.MessageKindVoice {
return model.TriggerDecision{Kind: model.TriggerKindNone}
}

if msg.VoiceDuration > d.cfg.MaxDuration {
return model.TriggerDecision{Kind: model.TriggerKindNone}
}

return model.TriggerDecision{
Kind: model.TriggerKindShortVoice,
Reason: "voice " + msg.VoiceDuration.String() + " <= " + d.cfg.MaxDuration.String(),
}
}
69 changes: 69 additions & 0 deletions internal/domain/service/short_voice_detector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package service_test

import (
"noirbot/internal/domain/model"
"noirbot/internal/domain/service"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestShortVoiceDetector_Detect(t *testing.T) {
const maxDuration = 10 * time.Second

tests := []struct {
name string
msg model.IncomingMessage
wantKind model.TriggerKind
}{
{
name: "voice короче порога — триггер short_voice",
msg: model.IncomingMessage{
Kind: model.MessageKindVoice,
VoiceDuration: 5 * time.Second,
},
wantKind: model.TriggerKindShortVoice,
},
{
name: "voice ровно на пороге — триггер (граничный случай)",
msg: model.IncomingMessage{
Kind: model.MessageKindVoice,
VoiceDuration: maxDuration,
},
wantKind: model.TriggerKindShortVoice,
},
{
name: "voice длиннее порога — пропускаем",
msg: model.IncomingMessage{
Kind: model.MessageKindVoice,
VoiceDuration: 30 * time.Second,
},
wantKind: model.TriggerKindNone,
},
{
name: "текстовое сообщение — детектор не реагирует",
msg: model.IncomingMessage{
Kind: model.MessageKindText,
Text: "привет",
},
wantKind: model.TriggerKindNone,
},
{
name: "пустой Kind — детектор не реагирует",
msg: model.IncomingMessage{},
wantKind: model.TriggerKindNone,
},
}

detector := service.NewShortVoiceDetector(service.ShortVoiceDetectorConfig{
MaxDuration: maxDuration,
})

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := detector.Detect(tt.msg)
require.Equal(t, tt.wantKind, got.Kind)
})
}
}
24 changes: 19 additions & 5 deletions internal/gateways/telegram/inbound/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,30 @@ func NewUpdateMapper() *UpdateMapper {
}

func (m *UpdateMapper) ToIncomingMessage(src *tgmodels.Message) (model.IncomingMessage, bool) {
if src == nil || src.From == nil || src.Text == "" {
if src == nil || src.From == nil {
return model.IncomingMessage{}, false
}

return model.IncomingMessage{
base := model.IncomingMessage{
BusinessConnectionID: src.BusinessConnectionID,
GuestID: src.From.ID,
Text: src.Text,
ReceivedAt: time.Unix(int64(src.Date), 0),
}, true
ReceivedAt: time.Now().UTC(),
}

switch {
case src.Text != "":
base.Kind = model.MessageKindText
base.Text = src.Text

return base, true
case src.Voice != nil:
base.Kind = model.MessageKindVoice
base.VoiceDuration = time.Duration(src.Voice.Duration) * time.Second

return base, true
default:
return model.IncomingMessage{}, false
}
}

func (m *UpdateMapper) ToBusinessConnection(src *tgmodels.BusinessConnection) model.BusinessConnection {
Expand Down
88 changes: 88 additions & 0 deletions internal/gateways/telegram/inbound/mapper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package inbound_test

import (
"noirbot/internal/domain/model"
"noirbot/internal/gateways/telegram/inbound"
"testing"
"time"

tgmodels "github.com/go-telegram/bot/models"
"github.com/stretchr/testify/require"
)

func TestUpdateMapper_ToIncomingMessage(t *testing.T) {
mapper := inbound.NewUpdateMapper()
from := &tgmodels.User{ID: 999}

tests := []struct {
name string
src *tgmodels.Message
wantOK bool
wantKind model.MessageKind
wantText string
wantDur time.Duration
}{
{
name: "text сообщение → MessageKindText",
src: &tgmodels.Message{
BusinessConnectionID: "conn-1",
From: from,
Text: "привет",
},
wantOK: true,
wantKind: model.MessageKindText,
wantText: "привет",
},
{
name: "voice сообщение → MessageKindVoice + длительность",
src: &tgmodels.Message{
BusinessConnectionID: "conn-1",
From: from,
Voice: &tgmodels.Voice{Duration: 7},
},
wantOK: true,
wantKind: model.MessageKindVoice,
wantDur: 7 * time.Second,
},
{
name: "audio file (не voice) → пропускаем как неподдерживаемый тип",
src: &tgmodels.Message{
BusinessConnectionID: "conn-1",
From: from,
Audio: &tgmodels.Audio{Duration: 5},
},
wantOK: false,
},
{
name: "ни text, ни voice → пропускаем",
src: &tgmodels.Message{From: from},
wantOK: false,
},
{
name: "From == nil → пропускаем",
src: &tgmodels.Message{Text: "x"},
wantOK: false,
},
{
name: "src == nil → пропускаем",
src: nil,
wantOK: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := mapper.ToIncomingMessage(tt.src)

require.Equal(t, tt.wantOK, ok)

if !tt.wantOK {
return
}

require.Equal(t, tt.wantKind, got.Kind)
require.Equal(t, tt.wantText, got.Text)
require.Equal(t, tt.wantDur, got.VoiceDuration)
})
}
}
Loading