mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-18 01:50:50 +00:00
feat: add rudimentary template checking on chat commands
This commit is contained in:
parent
d37c7ff1b7
commit
4ca7b4c6be
11 changed files with 115 additions and 19 deletions
|
@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
- Custom chat commands can now be sent as replies, whispers and announcements. Due to some API shenanigans yet to be solved, the latter two will always be sent from your main account, not the bot account (if they are different)
|
- Custom chat commands can now be sent as replies, whispers and announcements. Due to some API shenanigans yet to be solved, the latter two will always be sent from your main account, not the bot account (if they are different)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed some minor bugs regarding backup file sorting and migration procedures
|
||||||
|
|
||||||
## [3.2.1] - 2023-05-17
|
## [3.2.1] - 2023-05-17
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
12
app.go
12
app.go
|
@ -290,6 +290,18 @@ func (a *App) GetAppVersion() VersionInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) TestTemplate(message string, data any) error {
|
||||||
|
tpl, err := a.twitchManager.Client().Bot.MakeTemplate(message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tpl.Execute(io.Discard, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) TestCommandTemplate(message string) error {
|
||||||
|
return a.TestTemplate(message, twitch.TestMessageData)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
|
func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
|
||||||
callbackID := fmt.Sprintf("auth-callback-%d", client.UID())
|
callbackID := fmt.Sprintf("auth-callback-%d", client.UID())
|
||||||
authResult := make(chan bool)
|
authResult := make(chan bool)
|
||||||
|
|
|
@ -131,7 +131,8 @@
|
||||||
},
|
},
|
||||||
"remove-command-title": "Remove command {{name}}?",
|
"remove-command-title": "Remove command {{name}}?",
|
||||||
"no-commands": "Chatbot has no commands configured",
|
"no-commands": "Chatbot has no commands configured",
|
||||||
"command-already-in-use": "Command name already in use"
|
"command-already-in-use": "Command name already in use",
|
||||||
|
"command-invalid-format": "The response template contains errors"
|
||||||
},
|
},
|
||||||
"bottimers": {
|
"bottimers": {
|
||||||
"title": "Bot timers",
|
"title": "Bot timers",
|
||||||
|
|
|
@ -118,7 +118,8 @@
|
||||||
"chat": "Canale",
|
"chat": "Canale",
|
||||||
"reply": "Risposta",
|
"reply": "Risposta",
|
||||||
"whisper": "Chat privata"
|
"whisper": "Chat privata"
|
||||||
}
|
},
|
||||||
|
"command-invalid-format": "Il messaggio contiene errori"
|
||||||
},
|
},
|
||||||
"bottimers": {
|
"bottimers": {
|
||||||
"add-button": "Nuovo timer",
|
"add-button": "Nuovo timer",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PlusIcon } from '@radix-ui/react-icons';
|
import { PlusIcon } from '@radix-ui/react-icons';
|
||||||
import React, { useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useModule } from '~/lib/react';
|
import { useModule } from '~/lib/react';
|
||||||
import { useAppDispatch } from '~/store';
|
import { useAppDispatch } from '~/store';
|
||||||
|
@ -35,6 +35,7 @@ import {
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../theme';
|
} from '../theme';
|
||||||
import { Alert, AlertTrigger } from '../theme/alert';
|
import { Alert, AlertTrigger } from '../theme/alert';
|
||||||
|
import { TestCommandTemplate } from '@wailsapp/go/main/App';
|
||||||
|
|
||||||
const CommandList = styled('div', { marginTop: '1rem' });
|
const CommandList = styled('div', { marginTop: '1rem' });
|
||||||
const CommandItemContainer = styled('article', {
|
const CommandItemContainer = styled('article', {
|
||||||
|
@ -227,9 +228,11 @@ function CommandDialog({
|
||||||
item?.response_type ?? 'chat',
|
item?.response_type ?? 'chat',
|
||||||
);
|
);
|
||||||
const [response, setResponse] = useState(item?.response ?? '');
|
const [response, setResponse] = useState(item?.response ?? '');
|
||||||
|
const responseRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [accessLevel, setAccessLevel] = useState(
|
const [accessLevel, setAccessLevel] = useState(
|
||||||
item?.access_level ?? 'everyone',
|
item?.access_level ?? 'everyone',
|
||||||
);
|
);
|
||||||
|
const [responseError, setResponseError] = useState<string | null>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const replyTypes: ReplyType[] = ['chat', 'reply', 'whisper', 'announce'];
|
const replyTypes: ReplyType[] = ['chat', 'reply', 'whisper', 'announce'];
|
||||||
|
|
||||||
|
@ -239,20 +242,29 @@ function CommandDialog({
|
||||||
closeButton={true}
|
closeButton={true}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={async (e) => {
|
||||||
if (!(e.target as HTMLFormElement).checkValidity()) {
|
if (!(e.target as HTMLFormElement).checkValidity()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (onSubmit) {
|
try {
|
||||||
onSubmit(commandName, {
|
await TestCommandTemplate(response);
|
||||||
...item,
|
if (onSubmit) {
|
||||||
description,
|
onSubmit(commandName, {
|
||||||
response,
|
...item,
|
||||||
response_type: responseType,
|
description,
|
||||||
access_level: accessLevel,
|
response,
|
||||||
});
|
response_type: responseType,
|
||||||
|
access_level: accessLevel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setResponseError(e);
|
||||||
|
responseRef.current?.setCustomValidity(
|
||||||
|
t('pages.botcommands.command-invalid-format'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Field spacing="narrow" size="fullWidth">
|
<Field spacing="narrow" size="fullWidth">
|
||||||
|
@ -309,12 +321,21 @@ function CommandDialog({
|
||||||
<Textarea
|
<Textarea
|
||||||
value={response}
|
value={response}
|
||||||
required={true}
|
required={true}
|
||||||
onChange={(e) => setResponse(e.target.value)}
|
onChange={(e) => {
|
||||||
|
responseRef.current?.setCustomValidity('');
|
||||||
|
setResponse(e.target.value)
|
||||||
|
}}
|
||||||
id="command-response"
|
id="command-response"
|
||||||
|
ref={responseRef}
|
||||||
placeholder={t('pages.botcommands.command-response-placeholder')}
|
placeholder={t('pages.botcommands.command-response-placeholder')}
|
||||||
>
|
>
|
||||||
{item?.response}
|
{item?.response}
|
||||||
</Textarea>
|
</Textarea>
|
||||||
|
{responseError && (
|
||||||
|
<FieldNote css={{
|
||||||
|
color: '$red10'
|
||||||
|
}}>{responseError}</FieldNote>
|
||||||
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field spacing="narrow" size="fullWidth">
|
<Field spacing="narrow" size="fullWidth">
|
||||||
<Label htmlFor="command-acl">
|
<Label htmlFor="command-acl">
|
||||||
|
|
4
frontend/wailsjs/go/main/App.d.ts
vendored
4
frontend/wailsjs/go/main/App.d.ts
vendored
|
@ -27,3 +27,7 @@ export function IsServerReady():Promise<boolean>;
|
||||||
export function RestoreBackup(arg1:string):Promise<void>;
|
export function RestoreBackup(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function SendCrashReport(arg1:string,arg2:string):Promise<string>;
|
export function SendCrashReport(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
|
export function TestCommandTemplate(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function TestTemplate(arg1:string,arg2:any):Promise<void>;
|
||||||
|
|
|
@ -49,3 +49,11 @@ export function RestoreBackup(arg1) {
|
||||||
export function SendCrashReport(arg1, arg2) {
|
export function SendCrashReport(arg1, arg2) {
|
||||||
return window['go']['main']['App']['SendCrashReport'](arg1, arg2);
|
return window['go']['main']['App']['SendCrashReport'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function TestCommandTemplate(arg1) {
|
||||||
|
return window['go']['main']['App']['TestCommandTemplate'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TestTemplate(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['TestTemplate'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/sprig/v3"
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
@ -474,7 +473,7 @@ func (m *BotAlertsModule) compileTemplates() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *BotAlertsModule) addTemplate(templateList templateCache, message string) {
|
func (m *BotAlertsModule) addTemplate(templateList templateCache, message string) {
|
||||||
tpl, err := template.New("").Funcs(m.bot.customFunctions).Funcs(sprig.TxtFuncMap()).Parse(message)
|
tpl, err := m.bot.MakeTemplate(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.bot.logger.Error("Error compiling alert template", zap.Error(err))
|
m.bot.logger.Error("Error compiling alert template", zap.Error(err))
|
||||||
return
|
return
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~hamcha/containers/sync"
|
"git.sr.ht/~hamcha/containers/sync"
|
||||||
"github.com/Masterminds/sprig/v3"
|
|
||||||
irc "github.com/gempir/go-twitch-irc/v4"
|
irc "github.com/gempir/go-twitch-irc/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
@ -251,7 +250,7 @@ func (b *Bot) handleWriteMessageRPC(value string) {
|
||||||
func (b *Bot) updateTemplates() error {
|
func (b *Bot) updateTemplates() error {
|
||||||
b.customTemplates.Set(make(map[string]*template.Template))
|
b.customTemplates.Set(make(map[string]*template.Template))
|
||||||
for cmd, tmpl := range b.customCommands.Copy() {
|
for cmd, tmpl := range b.customCommands.Copy() {
|
||||||
tpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Funcs(b.customFunctions).Parse(tmpl.Response)
|
tpl, err := b.MakeTemplate(tmpl.Response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ type Manager struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(db *database.LocalDBClient, server *http.Server, logger *zap.Logger) (*Manager, error) {
|
func NewManager(db *database.LocalDBClient, server *http.Server, logger *zap.Logger) (*Manager, error) {
|
||||||
// Get Twitch Config
|
// Get Twitch config
|
||||||
var config Config
|
var config Config
|
||||||
if err := db.GetJSON(ConfigKey, &config); err != nil {
|
if err := db.GetJSON(ConfigKey, &config); err != nil {
|
||||||
if !errors.Is(err, database.ErrEmptyKey) {
|
if !errors.Is(err, database.ErrEmptyKey) {
|
||||||
|
@ -33,7 +33,7 @@ func NewManager(db *database.LocalDBClient, server *http.Server, logger *zap.Log
|
||||||
config.Enabled = false
|
config.Enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Twitch bot Config
|
// Get Twitch bot config
|
||||||
var botConfig BotConfig
|
var botConfig BotConfig
|
||||||
if err := db.GetJSON(BotConfigKey, &botConfig); err != nil {
|
if err := db.GetJSON(BotConfigKey, &botConfig); err != nil {
|
||||||
if !errors.Is(err, database.ErrEmptyKey) {
|
if !errors.Is(err, database.ErrEmptyKey) {
|
||||||
|
|
|
@ -2,10 +2,12 @@ package twitch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"github.com/Masterminds/sprig/v3"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
irc "github.com/gempir/go-twitch-irc/v4"
|
irc "github.com/gempir/go-twitch-irc/v4"
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
@ -94,6 +96,51 @@ func cmdCustom(bot *Bot, cmd string, data BotCustomCommand, message irc.PrivateM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bot) MakeTemplate(message string) (*template.Template, error) {
|
||||||
|
return template.New("").Funcs(sprig.TxtFuncMap()).Funcs(b.customFunctions).Parse(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
var TestMessageData = irc.PrivateMessage{
|
||||||
|
User: irc.User{
|
||||||
|
ID: "603448316",
|
||||||
|
Name: "ashkeelvt",
|
||||||
|
DisplayName: "AshKeelVT",
|
||||||
|
Color: "#EC2B87",
|
||||||
|
Badges: map[string]int{
|
||||||
|
"subscriber": 0,
|
||||||
|
"moments": 1,
|
||||||
|
"broadcaster": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Type: 1,
|
||||||
|
Tags: map[string]string{
|
||||||
|
"emotes": "",
|
||||||
|
"first-msg": "0",
|
||||||
|
"id": "e6b80ab3-d068-4226-83b2-a991da9c0cc3",
|
||||||
|
"turbo": "0",
|
||||||
|
"user-id": "603448316",
|
||||||
|
"badges": "broadcaster/1,subscriber/0,moments/1",
|
||||||
|
"color": "#EC2B87",
|
||||||
|
"user-type": "",
|
||||||
|
"room-id": "603448316",
|
||||||
|
"tmi-sent-ts": "1684345559394",
|
||||||
|
"flags": "",
|
||||||
|
"mod": "0",
|
||||||
|
"returning-chatter": "0",
|
||||||
|
"badge-info": "subscriber/21",
|
||||||
|
"display-name": "AshKeelVT",
|
||||||
|
"subscriber": "1",
|
||||||
|
},
|
||||||
|
Message: "!test",
|
||||||
|
Channel: "ashkeelvt",
|
||||||
|
RoomID: "603448316",
|
||||||
|
ID: "e6b80ab3-d068-4226-83b2-a991da9c0cc3",
|
||||||
|
Time: time.Now(),
|
||||||
|
Emotes: nil,
|
||||||
|
Bits: 0,
|
||||||
|
Action: false,
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bot) setupFunctions() {
|
func (b *Bot) setupFunctions() {
|
||||||
b.customFunctions = template.FuncMap{
|
b.customFunctions = template.FuncMap{
|
||||||
"user": func(message irc.PrivateMessage) string {
|
"user": func(message irc.PrivateMessage) string {
|
||||||
|
|
Loading…
Reference in a new issue