about/internal/plugins/meme.go

339 lines
7.9 KiB
Go

package plugins
import (
"context"
"fmt"
"html/template"
"math/rand"
"strings"
"sync"
"time"
"github.com/Alexander-D-Karpov/about/internal/storage"
"github.com/Alexander-D-Karpov/about/internal/stream"
)
type MemePlugin struct {
storage *storage.Storage
hub *stream.Hub
currentMeme *Meme
lastUpdate time.Time
rng *rand.Rand
shownMemes map[string]bool
mutex sync.RWMutex
}
type Meme struct {
Text string `json:"text"`
Image string `json:"image"`
Type string `json:"type"`
Source string `json:"source"`
Category string `json:"category"`
}
func NewMemePlugin(storage *storage.Storage, hub *stream.Hub) *MemePlugin {
source := rand.NewSource(time.Now().UnixNano())
plugin := &MemePlugin{
storage: storage,
hub: hub,
rng: rand.New(source),
shownMemes: make(map[string]bool),
}
plugin.selectRandomMeme()
plugin.lastUpdate = time.Now()
return plugin
}
func (p *MemePlugin) Name() string { return "meme" }
func (p *MemePlugin) Render(ctx context.Context) (string, error) {
config := p.storage.GetPluginConfig(p.Name())
settings := config.Settings
showMeme := p.getConfigBool(settings, "ui.showMeme", true)
if !showMeme || p.currentMeme == nil {
return "", nil
}
sectionTitle := p.getConfigValue(settings, "ui.sectionTitle", "Random Meme")
tmpl := `
<section class="meme-section section plugin" id="meme-section" data-w="1">
<header class="plugin-header meme-header">
<h3 class="plugin-title">{{.SectionTitle}}</h3>
<button type="button" class="btn btn-sm meme-refresh-btn" onclick="refreshMeme()">🎲</button>
</header>
<div class="plugin__inner">
<div class="meme-content">
{{if eq .Meme.Type "image"}}
<div class="meme-image">
<img src="{{.Meme.Image}}" alt="{{.Meme.Text}}" loading="lazy">
{{if .Meme.Text}}<p class="meme-caption">{{.Meme.Text}}</p>{{end}}
</div>
{{else if eq .Meme.Type "gif"}}
<div class="meme-gif">
<img src="{{.Meme.Image}}" alt="{{.Meme.Text}}" loading="lazy">
{{if .Meme.Text}}<p class="meme-caption">{{.Meme.Text}}</p>{{end}}
</div>
{{else}}
<div class="meme-text">
<p class="meme-quote">{{.Meme.Text}}</p>
{{if .Meme.Source}}<p class="meme-source">— {{.Meme.Source}}</p>{{end}}
</div>
{{end}}
</div>
</div>
</section>`
data := struct {
SectionTitle string
Meme *Meme
}{
SectionTitle: sectionTitle,
Meme: p.currentMeme,
}
t, err := template.New("meme").Parse(tmpl)
if err != nil {
return "", err
}
var buf strings.Builder
if err := t.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
func (p *MemePlugin) UpdateData(ctx context.Context) error {
config := p.storage.GetPluginConfig(p.Name())
settings := config.Settings
autoRefresh := p.getConfigBool(settings, "ui.autoRefresh", false)
refreshInterval := p.getConfigInt(settings, "ui.refreshInterval", 300)
if autoRefresh && time.Since(p.lastUpdate) > time.Duration(refreshInterval)*time.Second {
p.selectRandomMeme()
p.lastUpdate = time.Now()
if p.currentMeme != nil {
p.hub.Broadcast("meme_update", map[string]interface{}{
"meme": *p.currentMeme,
"timestamp": time.Now().Unix(),
})
}
}
return nil
}
func (p *MemePlugin) selectRandomMeme() {
config := p.storage.GetPluginConfig(p.Name())
settings := config.Settings
memes, ok := settings["memes"].([]interface{})
if !ok || len(memes) == 0 {
memes = p.getDefaultMemes()
}
if len(memes) == 0 {
return
}
p.mutex.Lock()
defer p.mutex.Unlock()
unshownMemes := []interface{}{}
for _, meme := range memes {
memeMap, ok := meme.(map[string]interface{})
if !ok {
continue
}
memeKey := fmt.Sprintf("%v-%v", memeMap["text"], memeMap["image"])
if !p.shownMemes[memeKey] {
unshownMemes = append(unshownMemes, meme)
}
}
if len(unshownMemes) == 0 {
p.shownMemes = make(map[string]bool)
unshownMemes = memes
}
memeIndex := p.rng.Intn(len(unshownMemes))
memeData := unshownMemes[memeIndex]
memeMap, ok := memeData.(map[string]interface{})
if !ok {
return
}
memeKey := fmt.Sprintf("%v-%v", memeMap["text"], memeMap["image"])
p.shownMemes[memeKey] = true
p.currentMeme = &Meme{
Text: p.getStringFromMap(memeMap, "text", ""),
Image: p.getStringFromMap(memeMap, "image", ""),
Type: p.getStringFromMap(memeMap, "type", "image"),
Source: p.getStringFromMap(memeMap, "source", ""),
Category: p.getStringFromMap(memeMap, "category", "general"),
}
}
func (p *MemePlugin) RefreshMeme() *Meme {
p.selectRandomMeme()
p.lastUpdate = time.Now()
if p.currentMeme != nil {
p.hub.Broadcast("meme_update", map[string]interface{}{
"meme": *p.currentMeme,
"timestamp": time.Now().Unix(),
})
}
return p.currentMeme
}
func (p *MemePlugin) getDefaultMemes() []interface{} {
return []interface{}{
map[string]interface{}{"type": "image", "image": "/static/memes/test.webp", "text": "really cool", "category": "test"},
map[string]interface{}{"type": "image", "image": "/static/memes/test2.jpg", "text": "that says a lot about our society", "category": "test"},
}
}
func (p *MemePlugin) GetSettings() map[string]interface{} {
config := p.storage.GetPluginConfig(p.Name())
return config.Settings
}
func (p *MemePlugin) SetSettings(settings map[string]interface{}) error {
config := p.storage.GetPluginConfig(p.Name())
config.Settings = settings
if err := p.storage.SetPluginConfig(p.Name(), config); err != nil {
return err
}
p.selectRandomMeme()
p.hub.Broadcast("plugin_update", map[string]interface{}{
"plugin": p.Name(),
"action": "settings_changed",
})
return nil
}
func (p *MemePlugin) getConfigValue(settings map[string]interface{}, key string, defaultValue string) string {
keys := strings.Split(key, ".")
current := settings
for i, k := range keys {
if i == len(keys)-1 {
if value, ok := current[k].(string); ok {
return value
}
return defaultValue
}
next, ok := current[k].(map[string]interface{})
if !ok {
return defaultValue
}
current = next
}
return defaultValue
}
func (p *MemePlugin) getConfigBool(settings map[string]interface{}, key string, defaultValue bool) bool {
keys := strings.Split(key, ".")
current := settings
for i, k := range keys {
if i == len(keys)-1 {
if value, ok := current[k].(bool); ok {
return value
}
return defaultValue
}
next, ok := current[k].(map[string]interface{})
if !ok {
return defaultValue
}
current = next
}
return defaultValue
}
func (p *MemePlugin) getConfigInt(settings map[string]interface{}, key string, defaultValue int) int {
keys := strings.Split(key, ".")
current := settings
for i, k := range keys {
if i == len(keys)-1 {
if v, ok := current[k].(float64); ok {
return int(v)
}
if v, ok := current[k].(int); ok {
return v
}
return defaultValue
}
next, ok := current[k].(map[string]interface{})
if !ok {
return defaultValue
}
current = next
}
return defaultValue
}
func (p *MemePlugin) getStringFromMap(m map[string]interface{}, key string, defaultValue string) string {
if v, ok := m[key].(string); ok {
return v
}
return defaultValue
}
func (p *MemePlugin) RenderText(ctx context.Context) (string, error) {
if p.currentMeme == nil {
return "Meme: No meme available", nil
}
memeText := p.currentMeme.Text
if memeText == "" {
memeText = "Image meme"
}
if len(memeText) > 50 {
memeText = memeText[:47] + "..."
}
return fmt.Sprintf("Meme: %s", memeText), nil
}
func (p *MemePlugin) GetCurrentMeme() *Meme {
return p.currentMeme
}
func (p *MemePlugin) GetMetrics() map[string]interface{} {
config := p.storage.GetPluginConfig(p.Name())
memes, ok := config.Settings["memes"].([]interface{})
metrics := map[string]interface{}{
"total_memes": 0,
"shown_memes": 0,
"has_current": 0,
}
if ok {
metrics["total_memes"] = len(memes)
}
p.mutex.RLock()
metrics["shown_memes"] = len(p.shownMemes)
if p.currentMeme != nil {
metrics["has_current"] = 1
}
p.mutex.RUnlock()
return metrics
}