From dae9189052b8ee436c04a82f157997e193e2bc58 Mon Sep 17 00:00:00 2001
From: rxdn <29165304+rxdn@users.noreply.github.com>
Date: Tue, 2 Aug 2022 15:09:55 +0100
Subject: [PATCH] Tag overhaul
---
app/http/endpoints/api/tags/tagcreate.go | 97 +++++++++--
app/http/endpoints/api/tags/tagdelete.go | 15 +-
app/http/endpoints/api/tags/tagslist.go | 26 ++-
botcontext/botcontext.go | 9 +
.../src/components/ConfirmationModal.svelte | 5 +-
frontend/src/components/EmbedBuilder.svelte | 103 +----------
frontend/src/components/EmbedForm.svelte | 116 +++++++++++++
frontend/src/components/form/Checkbox.svelte | 1 +
frontend/src/components/form/Input.svelte | 11 +-
frontend/src/components/form/Slider.svelte | 70 --------
frontend/src/components/form/Toggle.svelte | 17 +-
.../src/components/manage/TagEditor.svelte | 98 +++++++++++
frontend/src/includes/NotifyModal.svelte | 4 +-
frontend/src/views/Tags.svelte | 163 +++++++++++-------
frontend/src/views/Transcripts.svelte | 1 +
go.mod | 4 +-
go.sum | 4 +
17 files changed, 469 insertions(+), 275 deletions(-)
create mode 100644 frontend/src/components/EmbedForm.svelte
delete mode 100644 frontend/src/components/form/Slider.svelte
create mode 100644 frontend/src/components/manage/TagEditor.svelte
diff --git a/app/http/endpoints/api/tags/tagcreate.go b/app/http/endpoints/api/tags/tagcreate.go
index 22e458c..24ce765 100644
--- a/app/http/endpoints/api/tags/tagcreate.go
+++ b/app/http/endpoints/api/tags/tagcreate.go
@@ -1,21 +1,35 @@
package api
import (
- "github.com/TicketsBot/GoPanel/database"
+ "fmt"
+ dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
+ "github.com/TicketsBot/GoPanel/utils/types"
+ "github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
+ "github.com/go-playground/validator/v10"
+ "regexp"
+ "strings"
)
type tag struct {
- Id string `json:"id"`
- Content string `json:"content"`
+ Id string `json:"id" validate:"required,min=1,max=16"`
+ UseGuildCommand bool `json:"use_guild_command"` // Not yet implemented
+ Content *string `json:"content" validate:"omitempty,min=1,max=4096"`
+ UseEmbed bool `json:"use_embed"`
+ Embed *types.CustomEmbed `json:"embed" validate:"omitempty,dive"`
}
+var (
+ validate = validator.New()
+ slashCommandRegex = regexp.MustCompile(`^[-_a-zA-Z0-9]{1,32}$`)
+)
+
func CreateTag(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
// Max of 200 tags
- count, err := database.Client.Tag.GetTagCount(guildId)
+ count, err := dbclient.Client.Tag.GetTagCount(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
@@ -32,27 +46,80 @@ func CreateTag(ctx *gin.Context) {
return
}
- if !data.verifyIdLength() {
- ctx.JSON(400, utils.ErrorStr("Tag ID must be 1 - 16 characters in length"))
+ if !data.UseEmbed {
+ data.Embed = nil
+ }
+
+ // TODO: Limit command amount
+ if err := validate.Struct(data); err != nil {
+ validationErrors, ok := err.(validator.ValidationErrors)
+ if !ok {
+ ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration"))
+ return
+ }
+
+ formatted := "Your input contained the following errors:"
+ for _, validationError := range validationErrors {
+ formatted += fmt.Sprintf("\n%s", validationError.Error())
+ }
+
+ formatted = strings.TrimSuffix(formatted, "\n")
+ ctx.JSON(400, utils.ErrorStr(formatted))
return
}
- if !data.verifyContentLength() {
- ctx.JSON(400, utils.ErrorStr("Tag content must be 1 - 2000 characters in length"))
+ if !data.verifyContent() {
+ ctx.JSON(400, utils.ErrorStr("You have not provided any content for the tag"))
return
}
- if err := database.Client.Tag.Set(guildId, data.Id, data.Content); err != nil {
+ var embed *database.CustomEmbedWithFields
+ if data.Embed != nil {
+ customEmbed, fields := data.Embed.IntoDatabaseStruct()
+ embed = &database.CustomEmbedWithFields{
+ CustomEmbed: customEmbed,
+ Fields: fields,
+ }
+ }
+
+ wrapped := database.Tag{
+ Id: data.Id,
+ GuildId: guildId,
+ UseGuildCommand: data.UseGuildCommand,
+ Content: data.Content,
+ Embed: embed,
+ }
+
+ if err := dbclient.Client.Tag.Set(wrapped); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
+ return
+ }
+
+ ctx.Status(204)
+}
+
+func (t *tag) verifyId() bool {
+ if len(t.Id) == 0 || len(t.Id) > 16 || strings.Contains(t.Id, " ") {
+ return false
+ }
+
+ if t.UseGuildCommand {
+ return slashCommandRegex.MatchString(t.Id)
} else {
- ctx.JSON(200, utils.SuccessResponse)
+ return true
}
}
-func (t *tag) verifyIdLength() bool {
- return len(t.Id) > 0 && len(t.Id) <= 16
-}
+func (t *tag) verifyContent() bool {
+ if t.Content != nil { // validator ensures that if this is not nil, > 0 length
+ return true
+ }
-func (t *tag) verifyContentLength() bool {
- return len(t.Content) > 0 && len(t.Content) <= 2000
+ if t.Embed != nil {
+ if t.Embed.Description != nil || len(t.Embed.Fields) > 0 || t.Embed.ImageUrl != nil || t.Embed.ThumbnailUrl != nil {
+ return true
+ }
+ }
+
+ return false
}
diff --git a/app/http/endpoints/api/tags/tagdelete.go b/app/http/endpoints/api/tags/tagdelete.go
index 5b0ee21..6a4a550 100644
--- a/app/http/endpoints/api/tags/tagdelete.go
+++ b/app/http/endpoints/api/tags/tagdelete.go
@@ -6,14 +6,14 @@ import (
"github.com/gin-gonic/gin"
)
+type deleteBody struct {
+ TagId string `json:"tag_id"`
+}
+
func DeleteTag(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
- type Body struct {
- TagId string `json:"tag_id"`
- }
-
- var body Body
+ var body deleteBody
if err := ctx.BindJSON(&body); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
@@ -26,7 +26,8 @@ func DeleteTag(ctx *gin.Context) {
if err := database.Client.Tag.Delete(guildId, body.TagId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
- } else {
- ctx.JSON(200, utils.SuccessResponse)
+ return
}
+
+ ctx.Status(204)
}
diff --git a/app/http/endpoints/api/tags/tagslist.go b/app/http/endpoints/api/tags/tagslist.go
index fd13e41..45554d5 100644
--- a/app/http/endpoints/api/tags/tagslist.go
+++ b/app/http/endpoints/api/tags/tagslist.go
@@ -2,21 +2,35 @@ package api
import (
"github.com/TicketsBot/GoPanel/database"
+ "github.com/TicketsBot/GoPanel/utils"
+ "github.com/TicketsBot/GoPanel/utils/types"
"github.com/gin-gonic/gin"
)
-// TODO: Make client take new structure
func TagsListHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
tags, err := database.Client.Tag.GetByGuild(guildId)
if err != nil {
- ctx.AbortWithStatusJSON(500, gin.H{
- "success": false,
- "error": err.Error(),
- })
+ ctx.JSON(500, utils.ErrorJson(err))
return
}
- ctx.JSON(200, tags)
+ wrapped := make(map[string]tag)
+ for id, data := range tags {
+ var embed *types.CustomEmbed
+ if data.Embed != nil {
+ embed = types.NewCustomEmbed(data.Embed.CustomEmbed, data.Embed.Fields)
+ }
+
+ wrapped[id] = tag{
+ Id: data.Id,
+ UseGuildCommand: data.UseGuildCommand,
+ Content: data.Content,
+ UseEmbed: data.Embed != nil,
+ Embed: embed,
+ }
+ }
+
+ ctx.JSON(200, wrapped)
}
diff --git a/botcontext/botcontext.go b/botcontext/botcontext.go
index 1e1b1ae..01fe32f 100644
--- a/botcontext/botcontext.go
+++ b/botcontext/botcontext.go
@@ -10,6 +10,7 @@ import (
"github.com/rxdn/gdl/objects/channel"
"github.com/rxdn/gdl/objects/guild"
"github.com/rxdn/gdl/objects/guild/emoji"
+ "github.com/rxdn/gdl/objects/interaction"
"github.com/rxdn/gdl/objects/member"
"github.com/rxdn/gdl/objects/user"
"github.com/rxdn/gdl/rest"
@@ -168,3 +169,11 @@ func (ctx BotContext) ListMembers(guildId uint64) (members []member.Member, err
return
}
+
+func (ctx BotContext) CreateGuildCommand(guildId uint64, data rest.CreateCommandData) (interaction.ApplicationCommand, error) {
+ return rest.CreateGuildCommand(ctx.Token, ctx.RateLimiter, ctx.BotId, guildId, data)
+}
+
+func (ctx BotContext) DeleteGuildCommand(guildId, commandId uint64) error {
+ return rest.DeleteGuildCommand(ctx.Token, ctx.RateLimiter, ctx.BotId, guildId, commandId)
+}
diff --git a/frontend/src/components/ConfirmationModal.svelte b/frontend/src/components/ConfirmationModal.svelte
index 7fcccda..8dab052 100644
--- a/frontend/src/components/ConfirmationModal.svelte
+++ b/frontend/src/components/ConfirmationModal.svelte
@@ -1,7 +1,9 @@
- Embed Builder
+
+ Embed Builder
+
@@ -51,6 +53,7 @@
display: flex;
width: 60%;
margin: 10% auto auto auto;
+ padding-bottom: 5%;
}
@media only screen and (max-width: 1280px) {
diff --git a/frontend/src/components/EmbedBuilder.svelte b/frontend/src/components/EmbedBuilder.svelte
index ca82279..5aab382 100644
--- a/frontend/src/components/EmbedBuilder.svelte
+++ b/frontend/src/components/EmbedBuilder.svelte
@@ -4,71 +4,7 @@
Embed Builder
@@ -97,32 +33,12 @@
import DateTimePicker from "./form/DateTimePicker.svelte";
import Collapsible from "./Collapsible.svelte";
import Checkbox from "./form/Checkbox.svelte";
+ import EmbedForm from "./EmbedForm.svelte";
export let guildId;
export let data;
- if (data === undefined || data === null) {
- if (!data) {
- data = {};
- }
-
- data.fields = [];
- data.colour = '#2ECC71';
- data.author = {};
- data.footer = {};
- }
-
- function addField() {
- data.fields.push({name: '', value: '', inline: false});
- data = data;
- }
-
- function deleteField(i) {
- data.fields.splice(i, 1);
- data = data;
- }
-
const dispatch = createEventDispatcher();
function dispatchClose() {
@@ -190,19 +106,4 @@
background-color: #000;
opacity: .5;
}
-
- .form-wrapper {
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 100%;
- }
-
- .row {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- width: 100%;
- height: 100%;
- }
\ No newline at end of file
diff --git a/frontend/src/components/EmbedForm.svelte b/frontend/src/components/EmbedForm.svelte
new file mode 100644
index 0000000..8002980
--- /dev/null
+++ b/frontend/src/components/EmbedForm.svelte
@@ -0,0 +1,116 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/form/Checkbox.svelte b/frontend/src/components/form/Checkbox.svelte
index 84a5d3c..b67eb98 100644
--- a/frontend/src/components/form/Checkbox.svelte
+++ b/frontend/src/components/form/Checkbox.svelte
@@ -18,5 +18,6 @@
.form-checkbox {
height: 40px;
width: 40px;
+ margin: 0 !important;
}
diff --git a/frontend/src/components/form/Input.svelte b/frontend/src/components/form/Input.svelte
index ebb20fa..5a2505c 100644
--- a/frontend/src/components/form/Input.svelte
+++ b/frontend/src/components/form/Input.svelte
@@ -1,12 +1,12 @@
{#if label !== undefined}
-
-
+
+
{#if badge !== undefined}
{badge}
{/if}
{#if tooltipText !== undefined}
-
+
{#if tooltipLink !== undefined}
@@ -53,6 +53,11 @@
flex-direction: row;
align-items: center;
gap: 5px;
+ margin-bottom: 5px;
+ }
+
+ .no-margin {
+ margin-bottom: 0 !important;
}
.tooltip-icon {
diff --git a/frontend/src/components/form/Slider.svelte b/frontend/src/components/form/Slider.svelte
deleted file mode 100644
index 8de13ec..0000000
--- a/frontend/src/components/form/Slider.svelte
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
- console.log('b')} />
-
-
-
-
-
-
diff --git a/frontend/src/components/form/Toggle.svelte b/frontend/src/components/form/Toggle.svelte
index f0a5960..2b2933d 100644
--- a/frontend/src/components/form/Toggle.svelte
+++ b/frontend/src/components/form/Toggle.svelte
@@ -1,4 +1,4 @@
-
+
{#if label !== undefined}
{/if}
@@ -16,4 +16,17 @@
export let toggledColour = "#66bb6a";
export let untoggledColour = "#ccc";
-
\ No newline at end of file
+ export let inline = false;
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/manage/TagEditor.svelte b/frontend/src/components/manage/TagEditor.svelte
new file mode 100644
index 0000000..e0acea5
--- /dev/null
+++ b/frontend/src/components/manage/TagEditor.svelte
@@ -0,0 +1,98 @@
+{#if data}
+
dispatch("confirm", data)} on:cancel={() => dispatch("cancel", {})}>
+ Tag Editor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if data.use_embed}
+
+ {/if}
+
+
+
+ Save
+
+{/if}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/includes/NotifyModal.svelte b/frontend/src/includes/NotifyModal.svelte
index d53d0c1..aa451ce 100644
--- a/frontend/src/includes/NotifyModal.svelte
+++ b/frontend/src/includes/NotifyModal.svelte
@@ -65,7 +65,7 @@
left: 0;
width: 100%;
height: 100%;
- z-index: 999;
+ z-index: 1001;
display: flex;
justify-content: center;
@@ -83,7 +83,7 @@
left: 0;
width: 100%;
height: 100%;
- z-index: 998;
+ z-index: 1000;
background-color: #000;
opacity: .5;
}
diff --git a/frontend/src/views/Tags.svelte b/frontend/src/views/Tags.svelte
index 3cb9b71..fd6c317 100644
--- a/frontend/src/views/Tags.svelte
+++ b/frontend/src/views/Tags.svelte
@@ -1,25 +1,28 @@
+{#if tagCreateModal}
+ tagCreateModal = false} on:confirm={createTag}/>
+{:else if tagEditModal}
+
+{/if}
+
-
+
Tags
Tag |
- Edit |
- Delete |
+ Actions |
- {#each Object.entries(tags) as [id, content]}
+ {#each Object.entries(tags) as [id, tag]}
{id} |
-
-
- |
-
+ |
+
|
@@ -27,27 +30,8 @@
-
-
-
-
- Create A Tag
-
-
+
+
@@ -61,30 +45,89 @@
import axios from "axios";
import {API_URL} from "../js/constants";
import {setDefaultHeaders} from '../includes/Auth.svelte'
- import Input from "../components/form/Input.svelte";
- import Textarea from "../components/form/Textarea.svelte";
+ import {fade} from "svelte/transition";
+ import TagEditor from "../components/manage/TagEditor.svelte";
export let currentRoute;
let guildId = currentRoute.namedParams.id;
- let createData = {};
let tags = {};
+ let editData;
+ let editId;
- function editTag(id) {
- createData.id = id;
- createData.content = tags[id];
+ let tagCreateModal = false;
+ let tagEditModal = false;
+
+ function openEditModal(id) {
+ editId = id;
+ editData = tags[id];
+ tagEditModal = true;
}
- async function createTag() {
- const res = await axios.put(`${API_URL}/api/${guildId}/tags`, createData);
- if (res.status !== 200) {
+ function cancelEdit() {
+ editId = undefined;
+ editData = undefined;
+ tagEditModal = false;
+ }
+
+ async function createTag(e) {
+ const data = e.detail;
+ if (!data.id || data.id.length === 0) {
+ notifyError("Tag ID is required");
+ return;
+ }
+
+ if (data.content !== null && data.content.length === 0) {
+ data.content = null;
+ }
+
+ const res = await axios.put(`${API_URL}/api/${guildId}/tags`, data);
+ if (res.status !== 204) {
notifyError(res.data.error);
return;
}
- notifySuccess(`Tag ${createData.id} has been created`);
- tags[createData.id] = createData.content;
- createData = {};
+ notifySuccess(`Tag ${data.id} has been created`);
+ tagCreateModal = false;
+ tags[data.id] = data;
+ }
+
+ async function editTag(e) {
+ const data = e.detail;
+
+ if (editId !== data.id) {
+ // Delete old tag
+ const res = await axios.delete(`${API_URL}/api/${guildId}/tags`, {data: {tag_id: editId}});
+ if (res.status !== 204) {
+ notifyError(res.data.error);
+ return;
+ }
+
+ delete tags[editId];
+ }
+
+ if (!data.id || data.id.length === 0) {
+ notifyError("Tag ID is required");
+ return;
+ }
+
+ if (data.content !== null && data.content.length === 0) {
+ data.content = null;
+ }
+
+ const res = await axios.put(`${API_URL}/api/${guildId}/tags`, data);
+ if (res.status !== 204) {
+ notifyError(res.data.error);
+ return;
+ }
+
+ tags[data.id] = data;
+
+ tagEditModal = false;
+ editData = undefined;
+ editId = undefined;
+
+ notifySuccess("Tag edited successfully");
}
async function deleteTag(id) {
@@ -93,14 +136,14 @@
};
const res = await axios.delete(`${API_URL}/api/${guildId}/tags`, {data: data});
- if (res.status !== 200) {
+ if (res.status !== 204) {
notifyError(res.data.error);
return;
}
notifySuccess(`Tag deleted successfully`);
delete tags[id];
- tags = tags; // svelte terrible
+ tags = tags;
}
async function loadTags() {
@@ -111,6 +154,9 @@
}
tags = res.data;
+ for (const id in tags) {
+ tags[id].use_embed = tags[id].embed !== null;
+ }
}
withLoadingScreen(async () => {
@@ -142,34 +188,23 @@
height: 100%;
}
- .right-col {
- display: flex;
- flex-direction: column;
- width: 34%;
- height: 100%;
- }
-
.body-wrapper {
display: flex;
flex-direction: column;
+ row-gap: 2vh;
width: 100%;
height: 100%;
}
- .row {
+ table {
+ width: 100%;
+ }
+
+ .actions {
display: flex;
flex-direction: row;
- width: 100%;
- height: 100%;
- margin-bottom: 2%;
- }
-
- .col {
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 100%;
- margin-bottom: 2%;
+ gap: 10px;
+ justify-content: flex-end;
}
@media only screen and (max-width: 950px) {
@@ -181,9 +216,5 @@
width: 100%;
margin-top: 4%;
}
-
- .right-col {
- width: 100%;
- }
}
diff --git a/frontend/src/views/Transcripts.svelte b/frontend/src/views/Transcripts.svelte
index 356f389..857590f 100644
--- a/frontend/src/views/Transcripts.svelte
+++ b/frontend/src/views/Transcripts.svelte
@@ -288,6 +288,7 @@
font-weight: normal;
border-bottom: 1px solid #dee2e6;
padding-left: 10px;
+ padding-right: 10px;
}
:global(table.nice > thead > tr, table.nice > tbody > tr) {
diff --git a/go.mod b/go.mod
index b027531..db6dfb8 100644
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,9 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42
- github.com/TicketsBot/database v0.0.0-20220731213519-9fc9b34ab06f
+ github.com/TicketsBot/database v0.0.0-20220802140804-643695cd8347
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
- github.com/TicketsBot/worker v0.0.0-20220726162721-eb8978799cd0
+ github.com/TicketsBot/worker v0.0.0-20220802140902-30ca73aea6b8
github.com/apex/log v1.1.2
github.com/getsentry/sentry-go v0.13.0
github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607
diff --git a/go.sum b/go.sum
index b9a758b..61d7fc2 100644
--- a/go.sum
+++ b/go.sum
@@ -41,12 +41,16 @@ github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqS
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
github.com/TicketsBot/database v0.0.0-20220731213519-9fc9b34ab06f h1:5tpytvC/I1eOgLXhhWvg4RA+vu40oXXzLFwfLde37gY=
github.com/TicketsBot/database v0.0.0-20220731213519-9fc9b34ab06f/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
+github.com/TicketsBot/database v0.0.0-20220802140804-643695cd8347 h1:JVdXXxs6vxxldfesM/mmBNVBEVfkrFOpjcMdwxJQ4fw=
+github.com/TicketsBot/database v0.0.0-20220802140804-643695cd8347/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261/go.mod h1:2zPxDAN2TAPpxUPjxszjs3QFKreKrQh5al/R3cMXmYk=
github.com/TicketsBot/worker v0.0.0-20220726162721-eb8978799cd0 h1:94TYfRYYoAoV3Vyg9hqjtyDFw/Nb5a5+X7ob5rEjjos=
github.com/TicketsBot/worker v0.0.0-20220726162721-eb8978799cd0/go.mod h1:gThk0bKZTfcKgwpxrFN5BvOgQ6sXoRkz7ojj/9yiU28=
+github.com/TicketsBot/worker v0.0.0-20220802140902-30ca73aea6b8 h1:/hxUoNMsAD1HeG66iPDn0G5zQ8RNltaiL2h50LNplaA=
+github.com/TicketsBot/worker v0.0.0-20220802140902-30ca73aea6b8/go.mod h1:E8y+9Xu8el7QzHALhR/IFvITcJJkDeAxfsBFIEEWJuo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=