return $channel->toJson();
}
+ public function deleteCommand(Channel $channel, $command) {
+ $this->authorize('editRestream', $channel);
+ if (isset($channel->chat_commands[$command])) {
+ $cmds = $channel->chat_commands;
+ unset($cmds[$command]);
+ $channel->chat_commands = $cmds;
+ $channel->save();
+ }
+ return $channel->toJson();
+ }
+
+ public function saveCommand(Request $request, Channel $channel, $command) {
+ $this->authorize('editRestream', $channel);
+
+ $validatedData = $request->validate([
+ 'command' => 'string',
+ 'restrict' => 'string',
+ 'type' => 'string',
+ ]);
+
+ $cmds = $channel->chat_commands;
+ $cmds[$command] = $validatedData;
+ $channel->chat_commands = $cmds;
+ $channel->save();
+
+ return $channel->toJson();
+ }
+
+ public function saveGuessingGame(Request $request, Channel $channel, $name) {
+ $this->authorize('editRestream', $channel);
+
+ $validatedData = $request->validate([
+ 'active_message' => 'string',
+ 'cancel_message' => 'string',
+ 'invalid_solution_message' => 'string',
+ 'no_winners_message' => 'string',
+ 'not_active_message' => 'string',
+ 'points_exact_first' => 'numeric|min:1|max:5',
+ 'points_exact_other' => 'numeric|min:0|max:5',
+ 'points_close_first' => 'numeric|min:0|max:5',
+ 'points_close_min' => 'integer|min:0',
+ 'points_close_other' => 'numeric|min:0|max:5',
+ 'start_message' => 'string',
+ 'stop_message' => 'string',
+ 'winners_message' => 'string',
+ ]);
+
+ $settings = $channel->guessing_settings;
+ $settings[$name] = $validatedData;
+ $channel->guessing_settings = $settings;
+ $channel->save();
+
+ return $channel->toJson();
+ }
+
}
$this->save();
}
+ public function getGuessingSetting($name, $default = null) {
+ if (empty($this->guessing_settings) ||
+ empty($this->guessing_type) ||
+ !array_key_exists($this->guessing_type, $this->guessing_settings) ||
+ !array_key_exists($name, $this->guessing_settings[$this->guessing_type])
+ ) {
+ return $default;
+ }
+ return $this->guessing_settings[$this->guessing_type][$name];
+ }
+
public function solveGuessing($solution) {
$start = $this->guessing_start;
$end = is_null($this->guessing_end) ? now() : $this->guessing_end;
}
public function scoreGuessing($solution, $guess, $first) {
- return 1;
+ if ($guess == $solution) {
+ if ($first) {
+ return $this->getGuessingSetting('points_exact_first', 1);
+ }
+ return $this->getGuessingSetting('points_exact_other', 1);
+ }
+ $distance = abs(intval($guess) - intval($solution));
+ if ($distance <= $this->getGuessingSetting('points_close_max', 3)) {
+ if ($first) {
+ return $this->getGuessingSetting('points_close_first', 1);
+ }
+ return $this->getGuessingSetting('points_close_other', 1);
+ }
+ return 0;
}
public function isValidGuess($solution) {
'chat' => 'boolean',
'chat_commands' => 'array',
'chat_settings' => 'array',
+ 'guessing_settings' => 'array',
'guessing_start' => 'datetime',
'guessing_end' => 'datetime',
'languages' => 'array',
}
protected function listAnd($entries) {
+ $lang = empty($this->channels->languages) ? 'en' : $this->channel->languages[0];
+ if ($lang == 'de') {
+ return Arr::join($entries, ', ', ' und ');
+ }
return Arr::join($entries, ', ', ' and ');
}
protected function messageChannel($str) {
+ if (empty($str)) return;
$msg = IRCMessage::privmsg($this->channel->twitch_chat, $str);
$this->bot->sendIRCMessage($msg);
}
public function execute($args) {
if ($this->chanel->hasActiveGuessing()) {
$this->channel->clearGuessing();
- $this->messageChannel('Guessing game cancelled');
}
+ $msg = $this->channel->getGuessingSetting('cancel_message');
+ $this->messageChannel($msg);
}
}
public function execute($args) {
if (!$this->channel->hasActiveGuessing()) {
- $this->messageChannel('Channel has no active guessing game');
+ $msg = $this->channel->getGuessingSetting('not_active_message');
+ $this->messageChannel($msg);
return;
}
- if (empty($args)) {
- $this->messageChannel('Please provide a solution to the guessing game');
- return;
- }
- if (!$this->channel->isValidGuess($args)) {
- $this->messageChannel('Please provide a valid solution to the guessing game');
+ if (empty($args) || !$this->channel->isValidGuess($args)) {
+ $msg = $this->channel->getGuessingSetting('invalid_solution_message');
+ $this->messageChannel($msg);
return;
}
$winners = $this->channel->solveGuessing($args);
}
}
if (empty($names)) {
- $this->messageChannel('nobody wins :(');
+ $msg = $this->channel->getGuessingSetting('no_winners_message');
+ $this->messageChannel($msg);
} else {
- $this->messageChannel('Congrats '.$this->listAnd($names));
+ $msg = $this->channel->getGuessingSetting('winners_message');
+ $msg = str_replace('{names}', $this->listAnd($names), $msg);
+ $this->messageChannel($msg);
}
$this->channel->clearGuessing();
}
public function execute($args) {
if ($this->channel->hasActiveGuessing()) {
- $this->messageChannel('Channel already has an active guessing game');
+ $msg = $this->channel->getGuessingSetting('active_message');
+ $this->messageChannel($msg);
return;
}
$type = $this->getStringConfig('type', 'gtbk');
$this->channel->startGuessing($type);
- $this->messageChannel('Get your guesses in');
+ $msg = $this->channel->getGuessingSetting('start_message');
+ $this->messageChannel($msg);
}
}
public function execute($args) {
if (!$this->channel->hasActiveGuessing()) {
- $this->messageChannel('Channel has no active guessing game');
+ $msg = $this->channel->getGuessingSetting('not_active_message');
+ $this->messageChannel($msg);
return;
}
$this->channel->stopGuessing();
- $this->messageChannel('Guessing closed');
+ $msg = $this->channel->getGuessingSetting('stop_message');
+ $this->messageChannel($msg);
}
}
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table('channels', function (Blueprint $table) {
+ $table->text('guessing_settings')->default('{}');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('channels', function (Blueprint $table) {
+ $table->dropColumn('guessing_settings');
+ });
+ }
+};
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import Icon from '../common/Icon';
+
+const Command = ({
+ name,
+ onEditCommand,
+ onRemoveCommand,
+ settings,
+}) => {
+ const { t } = useTranslation();
+
+ const type = (settings && settings.command) || 'none';
+
+ return <tr>
+ <td>{`!${name}`}</td>
+ <td>{t(`twitchBot.commandRestrictions.${(settings && settings.restrict) || 'none'}`)}</td>
+ <td>{t(`twitchBot.commandTypes.${type}`)}</td>
+ <td className="text-end">
+ <div className="button-bar">
+ {onEditCommand ?
+ <Button
+ onClick={() => onEditCommand(name, settings)}
+ title={t('button.edit')}
+ variant="outline-secondary"
+ >
+ <Icon.EDIT title="" />
+ </Button>
+ : null}
+ {onRemoveCommand ?
+ <Button
+ onClick={() => onRemoveCommand(name)}
+ title={t('button.remove')}
+ variant="outline-danger"
+ >
+ <Icon.REMOVE title="" />
+ </Button>
+ : null}
+ </div>
+ </td>
+ </tr>;
+};
+
+Command.propTypes = {
+ name: PropTypes.string,
+ onEditCommand: PropTypes.func,
+ onRemoveCommand: PropTypes.func,
+ settings: PropTypes.shape({
+ command: PropTypes.string,
+ restrict: PropTypes.string,
+ }),
+};
+
+export default Command;
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Modal } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import CommandForm from './CommandForm';
+
+const CommandDialog = ({
+ name,
+ onHide,
+ onSubmit,
+ settings,
+ show,
+}) => {
+ const { t } = useTranslation();
+
+ return <Modal className="report-dialog" onHide={onHide} show={show}>
+ <Modal.Header closeButton>
+ <Modal.Title>
+ {t(name ? 'twitchBot.commandDialog' : 'twitchBot.addCommand')}
+ </Modal.Title>
+ </Modal.Header>
+ <CommandForm
+ name={name}
+ onCancel={onHide}
+ onSubmit={onSubmit}
+ settings={settings}
+ />
+ </Modal>;
+};
+
+CommandDialog.propTypes = {
+ name: PropTypes.string,
+ onHide: PropTypes.func,
+ onSubmit: PropTypes.func,
+ settings: PropTypes.shape({
+ }),
+ show: PropTypes.bool,
+};
+
+export default CommandDialog;
--- /dev/null
+import { withFormik } from 'formik';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Form, Modal } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
+import yup from '../../schema/yup';
+
+const CommandForm = ({
+ errors,
+ handleBlur,
+ handleChange,
+ handleSubmit,
+ name,
+ onCancel,
+ touched,
+ values,
+}) => {
+ const { t } = useTranslation();
+
+ const COMMANDS = [
+ 'none',
+ 'runner',
+ 'crew',
+ 'guessing-start',
+ 'guessing-stop',
+ 'guessing-solve',
+ 'guessing-cancel',
+ ];
+ const RESTRICTIONS = [
+ 'none',
+ 'mod',
+ 'owner',
+ ];
+
+ return <Form noValidate onSubmit={handleSubmit}>
+ <Modal.Body>
+ <Form.Group controlId="command.name">
+ <Form.Label>{t('twitchBot.commandName')}</Form.Label>
+ <Form.Control
+ disabled={!!name}
+ isInvalid={!!(touched.name && errors.name)}
+ name="name"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ plaintext={!!name}
+ readOnly={!!name}
+ type="text"
+ value={values.name || ''}
+ />
+ {touched.name && errors.name ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.name)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group controlId="command.restrict">
+ <Form.Label>{t('twitchBot.commandRestriction')}</Form.Label>
+ <Form.Select
+ isInvalid={!!(touched.restrict && errors.restrict)}
+ name="restrict"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ value={values.restrict || 'none'}
+ >
+ {RESTRICTIONS.map(r =>
+ <option key={r} value={r}>
+ {t(`twitchBot.commandRestrictions.${r}`)}
+ </option>
+ )}
+ </Form.Select>
+ {touched.restrict && errors.restrict ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.restrict)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group controlId="command.command">
+ <Form.Label>{t('twitchBot.commandType')}</Form.Label>
+ <Form.Select
+ isInvalid={!!(touched.command && errors.command)}
+ name="command"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ value={values.command || 'none'}
+ >
+ {COMMANDS.map(c =>
+ <option key={c} value={c}>
+ {t(`twitchBot.commandTypes.${c}`)}
+ </option>
+ )}
+ </Form.Select>
+ {touched.command && errors.command ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.command)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ </Modal.Body>
+ <Modal.Footer>
+ {onCancel ?
+ <Button onClick={onCancel} variant="secondary">
+ {t('button.cancel')}
+ </Button>
+ : null}
+ <Button type="submit" variant="primary">
+ {t('button.save')}
+ </Button>
+ </Modal.Footer>
+ </Form>;
+};
+
+CommandForm.propTypes = {
+ errors: PropTypes.shape({
+ command: PropTypes.string,
+ name: PropTypes.string,
+ restrict: PropTypes.string,
+ }),
+ handleBlur: PropTypes.func,
+ handleChange: PropTypes.func,
+ handleSubmit: PropTypes.func,
+ name: PropTypes.string,
+ onCancel: PropTypes.func,
+ touched: PropTypes.shape({
+ command: PropTypes.bool,
+ name: PropTypes.bool,
+ restrict: PropTypes.bool,
+ }),
+ values: PropTypes.shape({
+ command: PropTypes.string,
+ name: PropTypes.string,
+ restrict: PropTypes.string,
+ }),
+};
+
+export default withFormik({
+ displayName: 'CommandForm',
+ enableReinitialize: true,
+ handleSubmit: async (values, actions) => {
+ const { setErrors } = actions;
+ const { onSubmit } = actions.props;
+ try {
+ await onSubmit(values);
+ } catch (e) {
+ if (e.response && e.response.data && e.response.data.errors) {
+ setErrors(laravelErrorsToFormik(e.response.data.errors));
+ }
+ }
+ },
+ mapPropsToValues: ({ name, settings }) => {
+ return {
+ command: (settings && settings.command) || 'none',
+ name: name || '',
+ restrict: (settings && settings.restrict) || 'none',
+ };
+ },
+ validationSchema: yup.object().shape({
+ command: yup.string(),
+ name: yup.string().required(),
+ restrict: yup.string(),
+ }),
+})(CommandForm);
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Table } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import Command from './Command';
+
+const Commands = ({
+ channel,
+ onEditCommand,
+ onRemoveCommand,
+}) => {
+ const { t } = useTranslation();
+
+ return channel.chat_commands ?
+ <Table>
+ <thead>
+ <tr>
+ <th>{t('twitchBot.commandName')}</th>
+ <th>{t('twitchBot.commandRestriction')}</th>
+ <th>{t('twitchBot.commandType')}</th>
+ <th className="text-end">{t('general.actions')}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Object.entries(channel.chat_commands).map(([name, settings]) =>
+ <Command
+ key={name}
+ name={name}
+ onEditCommand={onEditCommand}
+ onRemoveCommand={onRemoveCommand}
+ settings={settings}
+ />
+ )}
+ </tbody>
+ </Table>
+ : null;
+};
+
+Commands.propTypes = {
+ channel: PropTypes.shape({
+ chat_commands: PropTypes.shape({
+ }),
+ }),
+ onEditCommand: PropTypes.func,
+ onRemoveCommand: PropTypes.func,
+};
+
+export default Commands;
import toastr from 'toastr';
import ChatSettingsForm from './ChatSettingsForm';
+import CommandDialog from './CommandDialog';
+import Commands from './Commands';
+import GuessingSettingsForm from './GuessingSettingsForm';
import ChannelSelect from '../common/ChannelSelect';
import ToggleSwitch from '../common/ToggleSwitch';
const Controls = () => {
const [channel, setChannel] = React.useState(null);
const [chatText, setChatText] = React.useState('');
+ const [editCommand, setEditCommand] = React.useState('');
+ const [editCommandSettings, setEditCommandSettings] = React.useState({});
+ const [showCommandDialog, setShowCommandDialog] = React.useState(false);
const { t } = useTranslation();
}
}, [channel, t]);
+ const onAddCommand = React.useCallback(() => {
+ setEditCommand('');
+ setEditCommandSettings({});
+ setShowCommandDialog(true);
+ }, [channel]);
+
+ const onEditCommand = React.useCallback((name, settings) => {
+ setEditCommand(name);
+ setEditCommandSettings(settings);
+ setShowCommandDialog(true);
+ }, [channel]);
+
+ const onRemoveCommand = React.useCallback(async (name) => {
+ try {
+ const rsp = await axios.delete(`/api/channels/${channel.id}/commands/${name}`);
+ setChannel(rsp.data);
+ toastr.success(t('twitchBot.saveSuccess'));
+ } catch (e) {
+ toastr.error(t('twitchBot.saveError'));
+ }
+ }, [channel]);
+
+ const saveCommand = React.useCallback(async (values) => {
+ try {
+ const rsp = await axios.put(
+ `/api/channels/${channel.id}/commands/${values.name}`,
+ values,
+ );
+ setChannel(rsp.data);
+ setShowCommandDialog(false);
+ setEditCommand('');
+ setEditCommandSettings({});
+ toastr.success(t('twitchBot.saveSuccess'));
+ } catch (e) {
+ toastr.error(t('twitchBot.saveError'));
+ throw e;
+ }
+ }, [channel]);
+
+ const saveGuessingGame = React.useCallback(async (values) => {
+ try {
+ const rsp = await axios.put(
+ `/api/channels/${channel.id}/guessing-game/${values.name}`,
+ values,
+ );
+ setChannel(rsp.data);
+ toastr.success(t('twitchBot.saveSuccess'));
+ } catch (e) {
+ toastr.error(t('twitchBot.saveError'));
+ throw e;
+ }
+ }, [channel]);
+
return <>
<Row className="mb-4">
<Form.Group as={Col} md={6}>
</Row>
{channel ?
<Row>
- <Form.Group as={Col} md={6}>
- <Form.Label>{t('twitchBot.chat')}</Form.Label>
- <Form.Control
- as="textarea"
- onChange={({ target: { value } }) => {
- setChatText(value);
+ <Col className="mt-5" md={6}>
+ <h3>{t('twitchBot.chat')}</h3>
+ <Form.Group>
+ <Form.Label>{t('twitchBot.chat')}</Form.Label>
+ <Form.Control
+ as="textarea"
+ onChange={({ target: { value } }) => {
+ setChatText(value);
+ }}
+ value={chatText}
+ />
+ <div className="button-bar">
+ <Button
+ className="mt-2"
+ disabled={!chatText || !channel.join}
+ onClick={() => {
+ if (chatText) chat(chatText, 'localhorsttv');
+ }}
+ variant="twitch"
+ >
+ {t('twitchBot.sendApp')}
+ </Button>
+ <Button
+ className="mt-2"
+ disabled={!chatText || !channel.chat}
+ onClick={() => {
+ if (chatText) chat(chatText, 'horstiebot');
+ }}
+ variant="twitch"
+ >
+ {t('twitchBot.sendChat')}
+ </Button>
+ </div>
+ </Form.Group>
+ </Col>
+ <Col className="mt-5" md={6}>
+ <h3>{t('twitchBot.chatSettings')}</h3>
+ <ChatSettingsForm channel={channel} onSubmit={saveChatSettings} />
+ </Col>
+ <Col className="mt-5" md={12}>
+ <h3>{t('twitchBot.commands')}</h3>
+ <Commands
+ channel={channel}
+ onEditCommand={onEditCommand}
+ onRemoveCommand={onRemoveCommand}
+ />
+ <CommandDialog
+ name={editCommand}
+ onHide={() => {
+ setShowCommandDialog(false);
+ setEditCommand('');
+ setEditCommandSettings({});
}}
- value={chatText}
+ onSubmit={saveCommand}
+ settings={editCommandSettings}
+ show={showCommandDialog}
/>
- <div className="button-bar">
- <Button
- className="mt-2"
- disabled={!chatText || !channel.join}
- onClick={() => {
- if (chatText) chat(chatText, 'localhorsttv');
- }}
- variant="twitch"
- >
- {t('twitchBot.sendApp')}
- </Button>
- <Button
- className="mt-2"
- disabled={!chatText || !channel.chat}
- onClick={() => {
- if (chatText) chat(chatText, 'horstiebot');
- }}
- variant="twitch"
- >
- {t('twitchBot.sendChat')}
+ <div>
+ <Button onClick={onAddCommand} variant="primary">
+ {t('twitchBot.addCommand')}
</Button>
</div>
- </Form.Group>
- <Col md={6}>
- <h3>{t('twitchBot.chatSettings')}</h3>
- <ChatSettingsForm channel={channel} onSubmit={saveChatSettings} />
+ </Col>
+ <Col className="mt-5" md={12}>
+ <h3>{t('twitchBot.guessingGame.settings')}</h3>
+ <GuessingSettingsForm
+ name="gtbk"
+ onSubmit={saveGuessingGame}
+ settings={channel.guessing_settings?.gtbk || {}}
+ />
</Col>
</Row>
:
--- /dev/null
+import { withFormik } from 'formik';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Col, Form, Row } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
+import i18n from '../../i18n';
+import yup from '../../schema/yup';
+
+const GuessingSettingsForm = ({
+ errors,
+ handleBlur,
+ handleChange,
+ handleSubmit,
+ onCancel,
+ touched,
+ values,
+}) => {
+ const { t } = useTranslation();
+
+ return <Form noValidate onSubmit={handleSubmit}>
+ <Row>
+ <Form.Group as={Col} controlId="gg.points_exact_first" md={6}>
+ <Form.Label>{t('twitchBot.guessingGame.pointsExactFirst')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.points_exact_first && errors.points_exact_first)}
+ max="5"
+ min="1"
+ name="points_exact_first"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ step="1"
+ type="number"
+ value={values.points_exact_first || 0}
+ />
+ {touched.points_exact_first && errors.points_exact_first ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.points_exact_first)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group as={Col} controlId="gg.points_exact_other" md={6}>
+ <Form.Label>{t('twitchBot.guessingGame.pointsExactOther')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.points_exact_other && errors.points_exact_other)}
+ max="5"
+ min="0"
+ name="points_exact_other"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ step="1"
+ type="number"
+ value={values.points_exact_other || 0}
+ />
+ {touched.points_exact_other && errors.points_exact_other ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.points_exact_other)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group as={Col} controlId="gg.points_close_first" md={6}>
+ <Form.Label>{t('twitchBot.guessingGame.pointsCloseFirst')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.points_close_first && errors.points_close_first)}
+ max="5"
+ min="0"
+ name="points_close_first"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ step="0.5"
+ type="number"
+ value={values.points_close_first || 0}
+ />
+ {touched.points_close_first && errors.points_close_first ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.points_close_first)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group as={Col} controlId="gg.points_close_other" md={6}>
+ <Form.Label>{t('twitchBot.guessingGame.pointsCloseOther')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.points_close_other && errors.points_close_other)}
+ max="5"
+ min="0"
+ name="points_close_other"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ step="0.5"
+ type="number"
+ value={values.points_close_other || 0}
+ />
+ {touched.points_close_other && errors.points_close_other ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.points_close_other)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group as={Col} controlId="gg.points_close_max" md={6}>
+ <Form.Label>{t('twitchBot.guessingGame.pointsCloseMax')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.points_close_max && errors.points_close_max)}
+ min="0"
+ name="points_close_max"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ step="1"
+ type="number"
+ value={values.points_close_max || 0}
+ />
+ {touched.points_close_max && errors.points_close_max ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.points_close_max)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ </Row>
+ <Form.Group controlId="gg.start_message">
+ <Form.Label>{t('twitchBot.guessingGame.startMessage')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.start_message && errors.start_message)}
+ name="start_message"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.start_message || ''}
+ />
+ {touched.start_message && errors.start_message ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.start_message)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group controlId="gg.stop_message">
+ <Form.Label>{t('twitchBot.guessingGame.stopMessage')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.stop_message && errors.stop_message)}
+ name="stop_message"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.stop_message || ''}
+ />
+ {touched.stop_message && errors.stop_message ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.stop_message)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group controlId="gg.winners_message">
+ <Form.Label>{t('twitchBot.guessingGame.winnersMessage')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.winners_message && errors.winners_message)}
+ name="winners_message"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.winners_message || ''}
+ />
+ {touched.winners_message && errors.winners_message ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.winners_message)}
+ </Form.Control.Feedback>
+ :
+ <Form.Text muted>
+ {t('twitchBot.guessingGame.winnersMessageHint')}
+ </Form.Text>
+ }
+ </Form.Group>
+ <Form.Group controlId="gg.no_winners_message">
+ <Form.Label>{t('twitchBot.guessingGame.noWinnersMessage')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.no_winners_message && errors.no_winners_message)}
+ name="no_winners_message"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.no_winners_message || ''}
+ />
+ {touched.no_winners_message && errors.no_winners_message ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.no_winners_message)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group controlId="gg.cancel_message">
+ <Form.Label>{t('twitchBot.guessingGame.cancelMessage')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.cancel_message && errors.cancel_message)}
+ name="cancel_message"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.cancel_message || ''}
+ />
+ {touched.cancel_message && errors.cancel_message ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.cancel_message)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group controlId="gg.invalid_solution_message">
+ <Form.Label>{t('twitchBot.guessingGame.invalidSolutionMessage')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.invalid_solution_message && errors.invalid_solution_message)}
+ name="invalid_solution_message"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.invalid_solution_message || ''}
+ />
+ {touched.invalid_solution_message && errors.invalid_solution_message ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.invalid_solution_message)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group controlId="gg.active_message">
+ <Form.Label>{t('twitchBot.guessingGame.activeMessage')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.active_message && errors.active_message)}
+ name="active_message"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.active_message || ''}
+ />
+ {touched.active_message && errors.active_message ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.active_message)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <Form.Group controlId="gg.not_active_message">
+ <Form.Label>{t('twitchBot.guessingGame.notActiveMessage')}</Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.not_active_message && errors.not_active_message)}
+ name="not_active_message"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.not_active_message || ''}
+ />
+ {touched.not_active_message && errors.not_active_message ?
+ <Form.Control.Feedback type="invalid">
+ {t(errors.not_active_message)}
+ </Form.Control.Feedback>
+ : null}
+ </Form.Group>
+ <div className="button-bar mt-3">
+ {onCancel ?
+ <Button onClick={onCancel} variant="secondary">
+ {t('button.cancel')}
+ </Button>
+ : null}
+ <Button type="submit" variant="primary">
+ {t('button.save')}
+ </Button>
+ </div>
+ </Form>;
+};
+
+GuessingSettingsForm.propTypes = {
+ errors: PropTypes.shape({
+ active_message: PropTypes.string,
+ cancel_message: PropTypes.string,
+ invalid_solution_message: PropTypes.string,
+ name: PropTypes.string,
+ no_winners_message: PropTypes.string,
+ not_active_message: PropTypes.string,
+ points_close_first: PropTypes.string,
+ points_close_max: PropTypes.string,
+ points_close_other: PropTypes.string,
+ points_exact_first: PropTypes.string,
+ points_exact_other: PropTypes.string,
+ start_message: PropTypes.string,
+ stop_message: PropTypes.string,
+ winners_message: PropTypes.string,
+ }),
+ handleBlur: PropTypes.func,
+ handleChange: PropTypes.func,
+ handleSubmit: PropTypes.func,
+ name: PropTypes.string,
+ onCancel: PropTypes.func,
+ touched: PropTypes.shape({
+ active_message: PropTypes.bool,
+ cancel_message: PropTypes.bool,
+ invalid_solution_message: PropTypes.bool,
+ name: PropTypes.bool,
+ no_winners_message: PropTypes.bool,
+ not_active_message: PropTypes.bool,
+ points_close_first: PropTypes.bool,
+ points_close_max: PropTypes.bool,
+ points_close_other: PropTypes.bool,
+ points_exact_first: PropTypes.bool,
+ points_exact_other: PropTypes.bool,
+ start_message: PropTypes.bool,
+ stop_message: PropTypes.bool,
+ winners_message: PropTypes.bool,
+ }),
+ values: PropTypes.shape({
+ active_message: PropTypes.string,
+ cancel_message: PropTypes.string,
+ invalid_solution_message: PropTypes.string,
+ name: PropTypes.string,
+ no_winners_message: PropTypes.string,
+ not_active_message: PropTypes.string,
+ points_close_first: PropTypes.number,
+ points_close_max: PropTypes.number,
+ points_close_other: PropTypes.number,
+ points_exact_first: PropTypes.number,
+ points_exact_other: PropTypes.number,
+ start_message: PropTypes.string,
+ stop_message: PropTypes.string,
+ winners_message: PropTypes.string,
+ }),
+};
+
+export default withFormik({
+ displayName: 'GuessingSettingsForm',
+ enableReinitialize: true,
+ handleSubmit: async (values, actions) => {
+ const { setErrors } = actions;
+ const { onSubmit } = actions.props;
+ try {
+ await onSubmit(values);
+ } catch (e) {
+ if (e.response && e.response.data && e.response.data.errors) {
+ setErrors(laravelErrorsToFormik(e.response.data.errors));
+ }
+ }
+ },
+ mapPropsToValues: ({ name, settings }) => {
+ const getNumericValue = (s, n, d) => s && Object.prototype.hasOwnProperty.call(s, n)
+ ? s[n] : d;
+ const getStringValue = (s, n, d) => s && Object.prototype.hasOwnProperty.call(s, n)
+ ? s[n] : i18n.t(`twitchBot.guessingGame.default${d}`);
+ return {
+ active_message: getStringValue(settings, 'active_message', 'ActiveMessage'),
+ cancel_message: getStringValue(settings, 'cancel_message', 'CancelMessage'),
+ invalid_solution_message:
+ getStringValue(settings, 'invalid_solution_message', 'InvalidSolutionMessage'),
+ name: name || '',
+ no_winners_message: getStringValue(settings, 'no_winners_message', 'NoWinnersMessage'),
+ not_active_message: getStringValue(settings, 'not_active_message', 'NotActiveMessage'),
+ points_close_first: getNumericValue(settings, 'points_close_first', 1),
+ points_close_max: getNumericValue(settings, 'points_close_max', 3),
+ points_close_other: getNumericValue(settings, 'points_close_other', 1),
+ points_exact_first: getNumericValue(settings, 'points_exact_first', 1),
+ points_exact_other: getNumericValue(settings, 'points_exact_other', 1),
+ start_message: getStringValue(settings, 'start_message', 'StartMessage'),
+ stop_message: getStringValue(settings, 'stop_message', 'StopMessage'),
+ winners_message: getStringValue(settings, 'winners_message', 'WinnersMessage'),
+ };
+ },
+ validationSchema: yup.object().shape({
+ active_message: yup.string(),
+ cancel_message: yup.string(),
+ invalid_solution_message: yup.string(),
+ name: yup.string().required(),
+ no_winners_message: yup.string(),
+ not_active_message: yup.string(),
+ points_close_first: yup.number(),
+ points_close_max: yup.number(),
+ points_close_other: yup.number(),
+ points_exact_first: yup.number(),
+ points_exact_other: yup.number(),
+ start_message: yup.string(),
+ stop_message: yup.string(),
+ winners_message: yup.string(),
+ }),
+})(GuessingSettingsForm);
tech: 'ALttP Techniken',
},
general: {
+ actions: 'Aktionen',
anonymous: 'Anonym',
appDescription: 'Turniere und Tutorials für The Legend of Zelda: A Link to the Past Randomizer',
appName: 'ALttP',
unlockSuccess: 'Turnier entsperrt',
},
twitchBot: {
+ addCommand: 'Command hinzufügen',
channel: 'Channel',
chat: 'Chat',
chatError: 'Fehler beim Senden',
chatWaitMsgsMax: 'Max. Messages',
chatWaitTimeMin: 'Min. Zeit',
chatWaitTimeMax: 'Max. Zeit',
+ commandDialog: 'Command bearbeiten',
+ commandName: 'Name',
+ commandParameters: 'Parameter',
+ commandRestriction: 'Einschränkung',
+ commandRestrictions: {
+ mod: 'Mods',
+ none: 'keine',
+ owner: 'Owner',
+ },
+ commands: 'Commands',
+ commandType: 'Typ',
+ commandTypes: {
+ crew: 'Crew Liste',
+ 'guessing-cancel': 'Guessing Game abbrechen',
+ 'guessing-solve': 'Guessing Game auflösen',
+ 'guessing-start': 'Guessing Game starten',
+ 'guessing-stop': 'Guessing Game stoppen',
+ none: 'keiner',
+ runner: 'Runner Liste',
+ },
controls: 'Controls',
+ guessingGame: {
+ activeMessage: 'Nachricht bei bereits laufendem Spiel',
+ cancelMessage: 'Nachricht bei Spielabbruch',
+ defaultActiveMessage: 'Es läuft bereits ein Spiel auf diesem Kanal',
+ defaultCancelMessage: 'Spiel abgebrochen',
+ defaultInvalidSolutionMessage: 'Bitte eine gültige Lösung für das Guessing Game angeben',
+ defaultNoWinnersMessage: 'keiner gewonnen :(',
+ defaultNotActiveMessage: 'Es läuft gerade kein Spiel auf diesem Kanal',
+ defaultStartMessage: 'Haut jetzt eure Zahlen raus!',
+ defaultStopMessage: 'Annahme geschlossen',
+ defaultWinnersMessage: 'Glückwunsch {names}!',
+ invalidSolutionMessage: 'Nachricht bei ungültiger (oder fehlender) Lösung',
+ noWinnersMessage: 'Nachricht, falls keine Gewinner',
+ notActiveMessage: 'Nachricht, wenn kein Spiel läuft',
+ pointsCloseFirst: 'Punkte für den ersten nächsten Treffer',
+ pointsCloseMax: 'Maximaler Abstand, um als nächster Treffer gewertet zu werden',
+ pointsCloseOther: 'Punkte für weitere nächste Treffer',
+ pointsExactFirst: 'Punkte für den ersten exakten Treffer',
+ pointsExactOther: 'Punkte für weitere exakte Treffer',
+ settings: 'Guessing Game Einstellungen',
+ startMessage: 'Nachricht bei Start',
+ stopMessage: 'Nachricht bei Einsendeschluss',
+ winnersMessage: 'Ankündigung der Gewinner',
+ winnersMessageHint: '{names} wird durch die Namen der Gewinner ersetzt',
+ },
heading: 'Twitch Bot',
joinApp: 'Join als App Bot',
joinChat: 'Join als Chat Bot',
tech: 'ALttP Tech',
},
general: {
+ actions: 'Actions',
anonymous: 'Anonym',
appDescription: 'Tournaments and tutorials for The Legend of Zelda: A Link to the Past Randomizer',
appName: 'ALttP',
unlockSuccess: 'Tournament unlocked',
},
twitchBot: {
+ addCommand: 'Add command',
channel: 'Channel',
chat: 'Chat',
chatError: 'Error sending message',
chatWaitMsgsMax: 'Max. messages',
chatWaitTimeMin: 'Min. time',
chatWaitTimeMax: 'Max. time',
+ commandDialog: 'Edit command',
+ commandName: 'Name',
+ commandParameters: 'Parameters',
+ commandRestriction: 'Restriction',
+ commandRestrictions: {
+ mod: 'Mods',
+ none: 'none',
+ owner: 'Owner',
+ },
+ commands: 'Commands',
+ commandType: 'Type',
+ commandTypes: {
+ crew: 'Crew list',
+ 'guessing-cancel': 'Cancel guessing game',
+ 'guessing-solve': 'Solve guessing game',
+ 'guessing-start': 'Start guessing game',
+ 'guessing-stop': 'Stop guessing game',
+ none: 'keiner',
+ runner: 'Runner list',
+ },
controls: 'Controls',
+ guessingGame: {
+ activeMessage: 'Message when a game is already running',
+ cancelMessage: 'Game cancellation announcement',
+ defaultActiveMessage: 'Channel already has an active guessing game',
+ defaultCancelMessage: 'Guessing game cancelled',
+ defaultInvalidSolutionMessage: 'Please provide a valid solution to the guessing game',
+ defaultNoWinnersMessage: 'nobody wins :(',
+ defaultNotActiveMessage: 'Channel has no active guessing game',
+ defaultStartMessage: 'Get your guesses in',
+ defaultStopMessage: 'Guessing closed',
+ defaultWinnersMessage: 'Congrats {names}!',
+ invalidSolutionMessage: 'Message for invalid (or missing) solution',
+ noWinnersMessage: 'Announcement for no winners',
+ notActiveMessage: 'Message when no game is currently active',
+ pointsCloseFirst: 'Points for first close match',
+ pointsCloseMax: 'Maximum distance to count as close match',
+ pointsCloseOther: 'Points for further close matches',
+ pointsExactFirst: 'Points for first exact match',
+ pointsExactOther: 'Points for further exact matches',
+ settings: 'Guessing game settings',
+ startMessage: 'Starting announcement',
+ stopMessage: 'Closing announcement',
+ winnersMessage: 'Winners announcement',
+ winnersMessageHint: '{names} will be replaced with a list of winners\' names',
+ },
heading: 'Twitch Bot',
joinApp: 'Join as App Bot',
joinChat: 'Join as Chat Bot',
Route::post('channels/{channel}/chat-settings', 'App\Http\Controllers\ChannelController@chatSettings');
Route::post('channels/{channel}/join', 'App\Http\Controllers\ChannelController@join');
Route::post('channels/{channel}/part', 'App\Http\Controllers\ChannelController@part');
+Route::delete('channels/{channel}/commands/{command}', 'App\Http\Controllers\ChannelController@deleteCommand');
+Route::put('channels/{channel}/commands/{command}', 'App\Http\Controllers\ChannelController@saveCommand');
+Route::put('channels/{channel}/guessing-game/{name}', 'App\Http\Controllers\ChannelController@saveGuessingGame');
Route::get('content', 'App\Http\Controllers\TechniqueController@search');
Route::get('content/{tech:name}', 'App\Http\Controllers\TechniqueController@single');