twitch-bot/editor/index.html

924 lines
33 KiB
HTML
Raw Permalink Normal View History

<html>
<title>Twitch-Bot: Config-Editor</title>
<link rel="stylesheet" href="editor/bundle.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.4/css/all.min.css">
<style>
[v-cloak] {
display: none;
}
.btn-twitch {
background-color: #6441a5;
}
</style>
<div id="app" v-cloak>
<b-navbar toggleable="lg" type="dark" variant="primary" class="mb-3">
<b-navbar-brand href="#"><i class="fas fa-fw fa-robot mr-1"></i> Twitch-Bot</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav v-if="authToken">
<b-nav-item :active="editMode === 'general'" @click="editMode = 'general'">
<i class="fas fa-fw fa-cog mr-1"></i>
General
</b-nav-item>
<b-nav-item :active="editMode === 'automessages'" @click="editMode = 'automessages'">
<i class="fas fa-fw fa-envelope-open-text mr-1"></i>
Auto-Messages
<b-badge pill>{{ autoMessages.length }}
</b-badge></b-nav-item>
<b-nav-item :active="editMode === 'rules'" @click="editMode = 'rules'">
<i class="fas fa-fw fa-inbox mr-1"></i>
Rules
<b-badge pill>{{ rules.length }}</b-badge>
</b-nav-item>
</b-navbar-nav>
<b-navbar-nav class="ml-auto">
<b-nav-text>
<span v-if="configNotifySocketConnected">
<i
class="fas fa-fw fa-ethernet mr-1 text-success"
title="Connected to Bot"
v-b-tooltip.hover
></i>
</span>
<span v-else>
<i
class="fas fa-fw fa-ethernet mr-1 text-danger"
title="Disconnected from Bot"
v-b-tooltip.hover
></i>
</span>
</b-nav-text>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<b-container>
<!-- Error display -->
<b-row v-if="error">
<b-col>
<b-alert
dismissible
@dismissed="error = null"
show
variant="danger"
>
<i class="fas fa-fw fa-exclamation-circle mr-1"></i> {{ error }}
</b-alert>
</b-col>
</b-row>
<!-- Working display -->
<b-row v-if="changePending">
<b-col>
<b-alert
show
variant="info"
>
<i class="fas fa-fw fa-spinner fa-pulse mr-1"></i>
Your change was submitted and is pending, please wait for config to be updated!
</b-alert>
</b-col>
</b-row>
<!-- Logged-out state -->
<b-row
v-if="!authToken"
>
<b-col
class="text-center"
>
<b-button
:disabled="!vars.TwitchClientID"
:href="authURL"
variant="twitch"
>
<i class="fab fa-fw fa-twitch mr-1"></i> Login with Twitch
</b-button>
</b-col>
</b-row>
<!-- Logged-in state -->
<template v-else>
<b-row v-if="editMode === 'general'">
<b-col>
<b-card-group columns>
<b-card no-body>
<b-card-header>
<i class="fas fa-fw fa-hashtag mr-1"></i> Channels
</b-card-header>
<b-list-group flush>
<b-list-group-item
class="d-flex align-items-center align-middle"
:key="channel"
v-for="channel in sortedChannels"
>
<span class="mr-auto">
<i class="fas fa-fw fa-hashtag mr-1"></i>
{{ channel }}
</span>
<b-button
@click="removeChannel(channel)"
size="sm"
variant="danger"
>
<i class="fas fa-fw fa-minus"></i>
</b-button>
</b-list-group-item>
<b-list-group-item>
<b-input-group>
<b-form-input @keyup.enter="addChannel" v-model="models.addChannel"></b-form-input>
<b-input-group-append>
<b-button @click="addChannel" variant="success"><i class="fas fa-fw fa-plus mr-1"></i> Add</b-button>
</b-input-group-append>
</b-input-group>
</b-list-group-item>
</b-list-group>
</b-card>
<b-card no-body>
<b-card-header>
<i class="fas fa-fw fa-users mr-1"></i> Bot-Editors
</b-card-header>
<b-list-group flush>
<b-list-group-item
class="d-flex align-items-center align-middle"
:key="editor"
v-for="editor in sortedEditors"
>
<b-avatar class="mr-3" :src="userProfiles[editor]?.profile_image_url"></b-avatar>
<span class="mr-auto">{{ userProfiles[editor] ? userProfiles[editor].display_name : editor }}</span>
<b-button
@click="removeEditor(editor)"
size="sm"
variant="danger"
>
<i class="fas fa-fw fa-minus"></i>
</b-button>
</b-list-group-item>
<b-list-group-item>
<b-input-group>
<b-form-input @keyup.enter="addEditor" v-model="models.addEditor"></b-form-input>
<b-input-group-append>
<b-button @click="addEditor" variant="success"><i class="fas fa-fw fa-plus mr-1"></i> Add</b-button>
</b-input-group-append>
</b-input-group>
</b-list-group-item>
</b-list-group>
</b-card>
<b-card no-body>
<b-card-header
class="d-flex align-items-center align-middle"
>
<span class="mr-auto"><i class="fas fa-fw fa-ticket-alt mr-1"></i> Auth-Tokens</span>
<b-button-group size="sm">
<b-button @click="newAPIToken" variant="success"><i class="fas fa-fw fa-plus"></i></b-button>
</b-button-group>
</b-card-header>
<b-list-group flush>
<b-list-group-item
variant="success"
v-if="createdAPIToken"
>
Token was created, copy it within 30s as you will not see it again:<br>
<code>{{ createdAPIToken.token }}</code>
</b-list-group-item>
<b-list-group-item
class="d-flex align-items-center align-middle"
:key="uuid"
v-for="(token, uuid) in apiTokens"
>
<span class="mr-auto">
{{ token.name }}<br>
<b-badge
:key="module"
v-for="module in token.modules"
>{{ module === '*' ? 'ANY' : module }}</b-badge>
</span>
<b-button
@click="removeAPIToken(uuid)"
size="sm"
variant="danger"
>
<i class="fas fa-fw fa-minus"></i>
</b-button>
</b-list-group-item>
</b-list-group>
</b-card>
</b-card-group>
</b-col>
</b-row>
<b-row v-else-if="editMode === 'automessages'">
<b-col>
<b-table
:busy="!autoMessages"
:fields="autoMessageFields"
hover
:items="autoMessages"
key="autoMessagesTable"
striped
>
<template #cell(actions)="data">
<b-button-group size="sm">
<b-button @click="editAutoMessage(data.item)"><i class="fas fa-fw fa-pen"></i></b-button>
<b-button @click="deleteAutoMessage(data.item.uuid)" variant="danger"><i class="fas fa-fw fa-minus"></i></b-button>
</b-button-group>
</template>
<template #cell(channel)="data">
<i class="fas fa-fw fa-hashtag mr-1"></i>
{{ data.value }}
</template>
<template #cell(cron)="data">
<code>{{ data.value }}</code>
</template>
<template #head(actions)="data">
<b-button-group size="sm">
<b-button @click="newAutoMessage" variant="success"><i class="fas fa-fw fa-plus"></i></b-button>
</b-button-group>
</template>
</b-table>
</b-col>
</b-row>
<b-row v-else-if="editMode === 'rules'">
<b-col>
<b-table
:busy="!rules"
:fields="rulesFields"
hover
:items="rules"
key="rulesTable"
striped
>
<template #cell(_actions)="data">
<b-button-group size="sm">
<b-button @click="editRule(data.item)"><i class="fas fa-fw fa-pen"></i></b-button>
<b-button @click="deleteRule(data.item.uuid)" variant="danger"><i class="fas fa-fw fa-minus"></i></b-button>
</b-button-group>
</template>
<template #cell(_match)="data">
<b-badge
class="m-1 text-truncate text-left col-12"
style="max-width: 250px;"
v-for="badge in formatRuleMatch(data.item)"
>
<strong>{{ badge.key }}</strong> <code class="ml-2">{{ badge.value }}</code>
</b-badge>
</template>
<template #cell(_description)="data">
<template v-if="data.item.description">{{ data.item.description }}<br></template>
<b-badge class="mt-1 mr-1" variant="danger" v-if="data.item.disable">Disabled</b-badge>
<b-badge
class="mt-1 mr-1"
v-for="badge in formatRuleActions(data.item)"
>
{{ badge }}
</b-badge>
</template>
<template #head(_actions)="data">
<b-button-group size="sm">
<b-button @click="newRule" variant="success"><i class="fas fa-fw fa-plus"></i></b-button>
</b-button-group>
</template>
</b-table>
</b-col>
</b-row>
</template>
<!-- API-Token Editor -->
<b-modal
@hidden="showAPITokenEditModal=false"
hide-header-close
@ok="saveAPIToken"
:ok-disabled="!validateAPIToken"
ok-title="Save"
size="md"
:visible="showAPITokenEditModal"
title="New API-Token"
v-if="showAPITokenEditModal"
>
<b-form-group
label="Name"
label-for="formAPITokenName"
>
<b-form-input
id="formAPITokenName"
v-model="models.apiToken.name"
:state="Boolean(models.apiToken.name)"
type="text"
></b-form-input>
</b-form-group>
<b-form-group
label="Enabled for Modules"
>
<b-form-checkbox-group
class="mb-3"
:options="availableModules"
text-field="text"
value-field="value"
v-model="models.apiToken.modules"
></b-form-checkbox-group>
</b-form-group>
</b-modal>
<!-- Auto-Message Editor -->
<b-modal
@hidden="showAutoMessageEditModal=false"
hide-header-close
@ok="saveAutoMessage"
:ok-disabled="!validateAutoMessage"
ok-title="Save"
size="lg"
:visible="showAutoMessageEditModal"
title="Edit Auto-Message"
v-if="showAutoMessageEditModal"
>
<b-row>
<b-col cols="8">
<b-form-group
label="Channel"
label-for="formAutoMessageChannel"
>
<b-input-group
prepend="#"
>
<b-form-input
id="formAutoMessageChannel"
:state="validateAutoMessageChannel"
type="text"
required
v-model="models.autoMessage.channel"
></b-form-input>
</b-input-group>
</b-form-group>
<hr>
<b-form-group
:description="`${models.autoMessage.message?.length || 0} / ${validateAutoMessageMessageLength}`"
label="Message"
label-for="formAutoMessageMessage"
>
<b-form-textarea
id="formAutoMessageMessage"
max-rows="6"
required
rows="3"
:state="models.autoMessage.message?.length <= validateAutoMessageMessageLength"
v-model="models.autoMessage.message"
></b-form-textarea>
</b-form-group>
<b-form-group>
<b-form-checkbox
switch
v-model="models.autoMessage.use_action"
>
Send message as action (<code>/me</code>)
</b-form-checkbox>
</b-form-group>
<hr>
<b-form-group
label="Sending Mode"
label-for="formAutoMessageSendMode"
>
<b-form-select
id="formAutoMessageSendMode"
:options="autoMessageSendModes"
v-model="models.autoMessage.sendMode"
></b-form-select>
</b-form-group>
<b-form-group
label="Send at"
label-for="formAutoMessageCron"
v-if="models.autoMessage.sendMode === 'cron'"
>
<b-form-input
id="formAutoMessageCron"
v-model="models.autoMessage.cron"
:state="validateAutoMessageCron"
type="text"
></b-form-input>
<div slot="description">
<code>@every [time]</code> or Cron syntax
</div>
</b-form-group>
<b-form-group
label="Send every"
label-for="formAutoMessageNLines"
v-if="models.autoMessage.sendMode === 'lines'"
>
<b-input-group
append="Lines"
>
<b-form-input
id="formAutoMessageNLines"
v-model="models.autoMessage.message_interval"
type="number"
></b-form-input>
</b-input-group>
</b-form-group>
<hr>
<b-form-group>
<b-form-checkbox
switch
v-model="models.autoMessage.only_on_live"
>
Send only when channel is live
</b-form-checkbox>
</b-form-group>
<b-form-group
label="Disable on Template"
label-for="formAutoMessageDisableOnTemplate"
>
<div slot="description">
Template expression resulting in <code>true</code> to disable the rule or <code>false</code> to enable it
</div>
<b-form-textarea
id="formAutoMessageDisableOnTemplate"
max-rows="6"
required
rows="1"
v-model="models.autoMessage.disable_on_template"
></b-form-textarea>
</b-form-group>
</b-col>
<b-col cols="4">
<h6>Getting Help</h6>
<p>
For information about available template functions and variables to use in the <strong>Message</strong> see the <a href="https://github.com/Luzifer/twitch-bot/wiki#templating" target="_blank">Templating</a> section of the Wiki.
</p>
<p>
For information about the <strong>Cron</strong> syntax have a look at the <a href="https://cron.help/" target="_blank">cron.help</a> site. Aditionally you can use <code>@every [time]</code> syntax. The <code>[time]</code> part is in format <code>1h30m20s</code>. You can leave out every segment but need to specify the unit of every segment. So for example <code>@every 1h</code> or <code>@every 10m</code> would be a valid specification.
</p>
</b-col>
</b-row>
</b-modal>
<!-- Rule Editor -->
<b-modal
@hidden="showRuleEditModal=false"
hide-header-close
@ok="saveRule"
:ok-disabled="!validateRule"
ok-title="Save"
scrollable
size="xl"
:visible="showRuleEditModal"
title="Edit Rule"
v-if="showRuleEditModal"
>
<b-row>
<b-col cols="6">
<b-form-group
description="Human readable description for the rules list"
label="Description"
label-for="formRuleDescription"
>
<b-form-input
id="formRuleDescription"
type="text"
v-model="models.rule.description"
></b-form-input>
</b-form-group>
<hr>
<b-tabs content-class="mt-3">
<b-tab>
<div slot="title">
Matcher <b-badge>{{ countRuleMatchers }}</b-badge>
</div>
<b-form-group
description="Channel with leading hash: #mychannel - matches all channels if none are given"
label="Match Channels"
label-for="formRuleMatchChannels"
>
<b-form-tags
id="formRuleMatchChannels"
no-add-on-enter
placeholder="Enter channels separated by space or comma"
remove-on-delete
separator=" ,"
:tag-validator="(tag) => Boolean(tag.match(/^#[a-zA-Z0-9_]{4,25}$/))"
v-model="models.rule.match_channels"
></b-form-tags>
</b-form-group>
<b-form-group
description="Matches no events if not set"
label="Match Event"
label-for="formRuleMatchEvent"
>
<b-form-select
id="formRuleMatchEvent"
:options="availableEvents"
v-model="models.rule.match_event"
></b-form-select>
</b-form-group>
<b-form-group
description="Regular expression to match the message, matches all messages when not set"
label="Match Message"
label-for="formRuleMatchMessage"
>
<b-form-input
id="formRuleMatchMessage"
:state="models.rule.match_message__validation"
type="text"
v-model="models.rule.match_message"
></b-form-input>
</b-form-group>
<b-form-group
description="Matches all users if none are given"
label="Match Users"
label-for="formRuleMatchUsers"
>
<b-form-tags
id="formRuleMatchUsers"
no-add-on-enter
placeholder="Enter usernames separated by space or comma"
remove-on-delete
separator=" ,"
:tag-validator="(tag) => Boolean(tag.match(/^[a-z0-9_]{4,25}$/))"
v-model="models.rule.match_users"
></b-form-tags>
</b-form-group>
</b-tab>
<b-tab>
<div slot="title">
Cooldown <b-badge>{{ countRuleCooldowns }}</b-badge>
</div>
<b-row>
<b-col>
<b-form-group
label="Rule Cooldown"
label-for="formRuleRuleCooldown"
>
<b-form-input
id="formRuleRuleCooldown"
placeholder="No Cooldown"
:state="validateDuration(models.rule.cooldown, false)"
type="text"
v-model="models.rule.cooldown"
></b-form-input>
</b-form-group>
</b-col>
<b-col>
<b-form-group
label="Channel Cooldown"
label-for="formRuleChannelCooldown"
>
<b-form-input
id="formRuleChannelCooldown"
placeholder="No Cooldown"
:state="validateDuration(models.rule.channel_cooldown, false)"
type="text"
v-model="models.rule.channel_cooldown"
></b-form-input>
</b-form-group>
</b-col>
<b-col>
<b-form-group
label="User Cooldown"
label-for="formRuleUserCooldown"
>
<b-form-input
id="formRuleUserCooldown"
placeholder="No Cooldown"
:state="validateDuration(models.rule.user_cooldown, false)"
type="text"
v-model="models.rule.user_cooldown"
></b-form-input>
</b-form-group>
</b-row>
<b-form-group
:description="`Available badges: ${vars.IRCBadges?.join(', ')}`"
label="Skip Cooldown for"
label-for="formRuleSkipCooldown"
>
<b-form-tags
id="formRuleSkipCooldown"
no-add-on-enter
placeholder="Enter badges separated by space or comma"
remove-on-delete
separator=" ,"
:tag-validator="validateTwitchBadge"
v-model="models.rule.skip_cooldown_for"
></b-form-tags>
</b-form-group>
</b-tab>
<b-tab>
<div slot="title">
Conditions <b-badge>{{ countRuleConditions }}</b-badge>
</div>
<p>Disable rule&hellip;</p>
<b-row>
<b-col>
<b-form-group>
<b-form-checkbox
switch
v-model="models.rule.disable"
>
completely
</b-form-checkbox>
</b-form-group>
</b-col>
<b-col>
<b-form-group>
<b-form-checkbox
switch
v-model="models.rule.disable_on_offline"
>
when channel is offline
</b-form-checkbox>
</b-form-group>
</b-col>
<b-col>
<b-form-group>
<b-form-checkbox
switch
v-model="models.rule.disable_on_permit"
>
when user has permit
</b-form-checkbox>
</b-form-group>
</b-col>
</b-row>
<b-form-group
:description="`Available badges: ${vars.IRCBadges?.join(', ')}`"
label="Disable Rule for"
label-for="formRuleDisableOn"
>
<b-form-tags
id="formRuleDisableOn"
no-add-on-enter
placeholder="Enter badges separated by space or comma"
remove-on-delete
separator=" ,"
:tag-validator="validateTwitchBadge"
v-model="models.rule.disable_on"
></b-form-tags>
</b-form-group>
<b-form-group
:description="`Available badges: ${vars.IRCBadges?.join(', ')}`"
label="Enable Rule for"
label-for="formRuleEnableOn"
>
<b-form-tags
id="formRuleEnableOn"
no-add-on-enter
placeholder="Enter badges separated by space or comma"
remove-on-delete
separator=" ,"
:tag-validator="validateTwitchBadge"
v-model="models.rule.enable_on"
></b-form-tags>
</b-form-group>
<b-form-group
label="Disable on Template"
label-for="formRuleDisableOnTemplate"
>
<div slot="description">
Template expression resulting in <code>true</code> to disable the rule or <code>false</code> to enable it
</div>
<b-form-textarea
id="formRuleDisableOnTemplate"
max-rows="6"
required
rows="1"
v-model="models.rule.disable_on_template"
></b-form-textarea>
</b-form-group>
</b-tab>
</b-tabs>
</b-col>
<b-col cols="6">
<div class="accordion" role="tablist">
<b-card
:key="`${models.rule.uuid}-action-${idx}`"
no-body
class="mb-1"
v-for="(action, idx) in models.rule.actions"
>
<b-card-header header-tag="header" class="p-1 d-flex" role="tab">
<b-button-group class="flex-fill">
<b-button
block
v-b-toggle="`${models.rule.uuid}-action-${idx}`"
variant="primary"
>
{{ getActionDefinitionByType(action.type).name }}
<i class="fas fa-fw fa-exclamation-triangle text-danger" v-if="actionHasValidationError(idx)"></i>
</b-button>
<b-button
@click="moveAction(idx, -1)"
:disabled="idx === 0"
variant="secondary"
><i class="fas fa-fw fa-chevron-up"></i></b-button>
<b-button
@click="moveAction(idx, +1)"
:disabled="idx === models.rule.actions.length - 1"
variant="secondary"
><i class="fas fa-fw fa-chevron-down"></i></b-button>
<b-button
@click="removeAction(idx)"
variant="danger"
><i class="fas fa-fw fa-trash"></i></b-button>
</b-button-group>
</b-card-header>
<b-collapse
:id="`${models.rule.uuid}-action-${idx}`"
accordion="my-accordion"
role="tabpanel"
>
<b-card-body v-if="getActionDefinitionByType(action.type).fields?.length > 0">
<template
v-for="field in getActionDefinitionByType(action.type).fields"
>
<b-form-group
v-if="field.type === 'bool'"
>
<div slot="description">
<i
class="fas fa-fw fa-code mr-1 text-success"
title="Supports Templating"
v-if="field.support_template"
></i>
{{ field.description }}
</div>
<b-form-checkbox
switch
v-model="models.rule.actions[idx].attributes[field.key]"
>
{{ field.name }}
</b-form-checkbox>
</b-form-group>
<b-form-group
:label="field.name"
:label-for="`${models.rule.uuid}-action-${idx}-${field.key}`"
v-else-if="field.type === 'stringslice'"
>
<div slot="description">
<i
class="fas fa-fw fa-code mr-1 text-success"
title="Supports Templating"
v-if="field.support_template"
></i>
{{ field.description }}
</div>
<b-form-tags
:id="`${models.rule.uuid}-action-${idx}-${field.key}`"
:state="validateActionArgument(idx, field.key)"
placeholder="Enter elements and press enter to add the element"
remove-on-delete
v-model="models.rule.actions[idx].attributes[field.key]"
></b-form-tags>
</b-form-group>
<b-form-group
:label="field.name"
:label-for="`${models.rule.uuid}-action-${idx}-${field.key}`"
v-else-if="field.type === 'string' && field.long"
>
<div slot="description">
<i
class="fas fa-fw fa-code mr-1 text-success"
title="Supports Templating"
v-if="field.support_template"
></i>
{{ field.description }}
</div>
<b-form-textarea
:id="`${models.rule.uuid}-action-${idx}-${field.key}`"
max-rows="6"
:required="!field.optional"
rows="3"
:state="validateActionArgument(idx, field.key)"
v-model="models.rule.actions[idx].attributes[field.key]"
></b-form-textarea>
</b-form-group>
<b-form-group
:label="field.name"
:label-for="`${models.rule.uuid}-action-${idx}-${field.key}`"
v-else
>
<div slot="description">
<i
class="fas fa-fw fa-code mr-1 text-success"
title="Supports Templating"
v-if="field.support_template"
></i>
{{ field.description }}
</div>
<b-form-input
:id="`${models.rule.uuid}-action-${idx}-${field.key}`"
:placeholder="field.default"
:required="!field.optional"
:state="validateActionArgument(idx, field.key)"
type="text"
v-model="models.rule.actions[idx].attributes[field.key]"
></b-form-input>
</b-form-group>
</template>
</b-card-body>
<b-card-body v-else>
This action has no attributes.
</b-card-body>
</b-collapse>
</b-card>
<hr>
<b-form-group
:description="addActionDescription"
label="Add Action"
label-for="ruleAddAction"
>
<b-input-group>
<b-form-select
id="ruleAddAction"
:options="availableActionsForAdd"
v-model="models.addAction"
></b-form-select>
<b-input-group-append>
<b-button
@click="addAction"
:disabled="!models.addAction"
variant="success"
><i class="fas fa-fw fa-plus mr-1"></i> Add</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<div>
</b-col>
</b-row>
</b-modal>
</b-container>
</div>
<script src="editor/bundle.js"></script>
<script src="editor/app.js"></script>
</html>