mirror of
https://github.com/Fluffy-Bean/Lynxie.git
synced 2025-05-21 02:44:57 +00:00
Update pipeline
And commit half-baked code restructure
This commit is contained in:
parent
5d67b2e2b0
commit
2e5f4bce11
11 changed files with 32 additions and 59 deletions
77
pkg/commands/debug/debug.go
Normal file
77
pkg/commands/debug/debug.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/Fluffy-Bean/lynxie/app"
|
||||
"github.com/Fluffy-Bean/lynxie/internal/color"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func RegisterDebugCommands(a *app.App) {
|
||||
a.RegisterCommand("debug", registerDebug(a))
|
||||
}
|
||||
|
||||
func registerDebug(a *app.App) app.Callback {
|
||||
return func(h *app.Handler, args []string) app.Error {
|
||||
buildTags := "-"
|
||||
goVersion := strings.TrimPrefix(runtime.Version(), "go")
|
||||
gcCount := runtime.MemStats{}.NumGC
|
||||
buildHash, _ := a.Config.CommandExtras["debug_build-hash"]
|
||||
buildPipeline, _ := a.Config.CommandExtras["debug_build-pipeline"]
|
||||
latency := h.Session.HeartbeatLatency().Milliseconds()
|
||||
|
||||
info, _ := debug.ReadBuildInfo()
|
||||
for _, setting := range info.Settings {
|
||||
switch setting.Key {
|
||||
case "-tags":
|
||||
buildTags = strings.ReplaceAll(setting.Value, ",", " ")
|
||||
}
|
||||
}
|
||||
|
||||
h.Session.ChannelMessageSendComplex(h.Message.ChannelID, &discordgo.MessageSend{
|
||||
Embed: &discordgo.MessageEmbed{
|
||||
Title: "Lynxie",
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{
|
||||
Name: "Build Tags",
|
||||
Value: buildTags,
|
||||
Inline: false,
|
||||
},
|
||||
{
|
||||
Name: "Go version",
|
||||
Value: goVersion,
|
||||
Inline: false,
|
||||
},
|
||||
{
|
||||
Name: "OS/Arch",
|
||||
Value: runtime.GOOS + "/" + runtime.GOARCH,
|
||||
Inline: false,
|
||||
},
|
||||
{
|
||||
Name: "GC Count",
|
||||
Value: fmt.Sprint(gcCount),
|
||||
Inline: false,
|
||||
},
|
||||
{
|
||||
Name: "Build Hash",
|
||||
Value: fmt.Sprintf("[%s](%s)", buildHash, buildPipeline),
|
||||
Inline: false,
|
||||
},
|
||||
{
|
||||
Name: "Latency",
|
||||
Value: fmt.Sprintf("%dms", latency),
|
||||
Inline: false,
|
||||
},
|
||||
},
|
||||
Color: color.RGBToDiscord(255, 255, 255),
|
||||
},
|
||||
Reference: h.Reference,
|
||||
})
|
||||
|
||||
return app.Error{}
|
||||
}
|
||||
}
|
259
pkg/commands/img/img.go
Normal file
259
pkg/commands/img/img.go
Normal file
|
@ -0,0 +1,259 @@
|
|||
package img
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~sbinet/gg"
|
||||
"github.com/Fluffy-Bean/lynxie/app"
|
||||
"github.com/Fluffy-Bean/lynxie/internal/color"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
const maxFileSize = 1024 * 1024 * 10 // 10MB
|
||||
|
||||
var client = http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
//go:embed resources/Impact.ttf
|
||||
var resourceImpactFont []byte
|
||||
|
||||
func RegisterImgCommands(a *app.App) {
|
||||
a.RegisterCommand("saveable", registerSaveable(a))
|
||||
a.RegisterCommandAlias("gif", "saveable")
|
||||
|
||||
a.RegisterCommand("caption", registerCaption(a))
|
||||
a.RegisterCommandAlias("c", "caption")
|
||||
}
|
||||
|
||||
func registerSaveable(a *app.App) app.Callback {
|
||||
return func(h *app.Handler, args []string) app.Error {
|
||||
fileEndpoint, err := getClosestImage(h)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Could not get image",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fileEndpoint, nil)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if req.ContentLength > maxFileSize {
|
||||
return app.Error{
|
||||
Msg: "Could not get image",
|
||||
Err: errors.New("requested file is too big"),
|
||||
}
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
h.Session.ChannelMessageSendComplex(h.Message.ChannelID, &discordgo.MessageSend{
|
||||
Embed: &discordgo.MessageEmbed{
|
||||
Title: "Saveable",
|
||||
Description: "Image converted to GIF :3",
|
||||
Image: &discordgo.MessageEmbedImage{
|
||||
URL: "attachment://saveable.gif",
|
||||
},
|
||||
Color: color.RGBToDiscord(255, 255, 255),
|
||||
},
|
||||
Files: []*discordgo.File{
|
||||
{
|
||||
Name: "saveable.gif",
|
||||
ContentType: "image/gif",
|
||||
Reader: res.Body,
|
||||
},
|
||||
},
|
||||
Reference: h.Reference,
|
||||
})
|
||||
|
||||
return app.Error{}
|
||||
}
|
||||
}
|
||||
|
||||
func registerCaption(a *app.App) app.Callback {
|
||||
return func(h *app.Handler, args []string) app.Error {
|
||||
fileEndpoint, err := getClosestImage(h)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Could not get image",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fileEndpoint, nil)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if req.ContentLength > maxFileSize {
|
||||
return app.Error{
|
||||
Msg: "Could not get image",
|
||||
Err: errors.New("requested file is too big"),
|
||||
}
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
buff, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Failed to read image",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
var img image.Image
|
||||
switch http.DetectContentType(buff) {
|
||||
case "image/png":
|
||||
img, err = png.Decode(bytes.NewReader(buff))
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Failed to decode PNG",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
break
|
||||
case "image/jpeg":
|
||||
img, err = jpeg.Decode(bytes.NewReader(buff))
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Failed to decode JPEG",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
return app.Error{
|
||||
Msg: "Unknown or unsupported image format",
|
||||
Err: errors.New("Unknown or unsupported image format " + http.DetectContentType(buff)),
|
||||
}
|
||||
}
|
||||
|
||||
fontSize := float64(img.Bounds().Dx() / 25)
|
||||
if fontSize < 16 {
|
||||
fontSize = 16
|
||||
} else if fontSize > 50 {
|
||||
fontSize = 50
|
||||
}
|
||||
|
||||
canvas := gg.NewContext(img.Bounds().Dx(), img.Bounds().Dy()+200)
|
||||
err = canvas.LoadFontFaceFromBytes(resourceImpactFont, fontSize)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Failed to load font",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
canvas.SetRGBA(255, 255, 255, 255)
|
||||
canvas.Clear()
|
||||
|
||||
canvas.SetRGBA(0, 0, 0, 255)
|
||||
canvas.DrawStringWrapped(
|
||||
strings.Join(args, " "),
|
||||
float64(img.Bounds().Dx()/2),
|
||||
100,
|
||||
0.5,
|
||||
0.5,
|
||||
float64(img.Bounds().Dx()),
|
||||
1.5,
|
||||
gg.AlignCenter,
|
||||
)
|
||||
|
||||
canvas.DrawImage(img, 0, 200)
|
||||
|
||||
var export bytes.Buffer
|
||||
err = canvas.EncodeJPG(bufio.NewWriter(&export), &jpeg.Options{
|
||||
Quality: 100,
|
||||
})
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Failed to encode JPEG",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
h.Session.ChannelMessageSendComplex(h.Message.ChannelID, &discordgo.MessageSend{
|
||||
Embed: &discordgo.MessageEmbed{
|
||||
Title: "Caption",
|
||||
Image: &discordgo.MessageEmbedImage{
|
||||
URL: "attachment://caption.jpeg",
|
||||
},
|
||||
Color: color.RGBToDiscord(255, 255, 255),
|
||||
},
|
||||
Files: []*discordgo.File{
|
||||
{
|
||||
Name: "caption.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
Reader: bytes.NewReader(export.Bytes()),
|
||||
},
|
||||
},
|
||||
Reference: h.Reference,
|
||||
})
|
||||
|
||||
return app.Error{}
|
||||
}
|
||||
}
|
||||
|
||||
func getClosestImage(h *app.Handler) (string, error) {
|
||||
// Get message attachments
|
||||
if len(h.Message.Attachments) >= 1 {
|
||||
if h.Message.Attachments[0].Size > maxFileSize {
|
||||
return "", errors.New("file size is too big")
|
||||
}
|
||||
|
||||
return h.Message.Attachments[0].ProxyURL, nil
|
||||
}
|
||||
|
||||
// If no attachments exist... see if the message is replying to someone
|
||||
if h.Message.ReferencedMessage != nil {
|
||||
if len(h.Message.ReferencedMessage.Attachments) >= 1 {
|
||||
if h.Message.ReferencedMessage.Attachments[0].Size > maxFileSize {
|
||||
return "", errors.New("file size is too big")
|
||||
}
|
||||
|
||||
return h.Message.ReferencedMessage.Attachments[0].ProxyURL, nil
|
||||
}
|
||||
|
||||
// Maybe replying to an embed...?
|
||||
if len(h.Message.ReferencedMessage.Embeds) >= 1 {
|
||||
//... no file size is provided
|
||||
return h.Message.ReferencedMessage.Embeds[0].Image.ProxyURL, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("no files exists")
|
||||
}
|
BIN
pkg/commands/img/resources/Impact.ttf
Normal file
BIN
pkg/commands/img/resources/Impact.ttf
Normal file
Binary file not shown.
190
pkg/commands/porb/porb.go
Normal file
190
pkg/commands/porb/porb.go
Normal file
|
@ -0,0 +1,190 @@
|
|||
package porb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Fluffy-Bean/lynxie/app"
|
||||
"github.com/Fluffy-Bean/lynxie/internal/color"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var client = http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
type post struct {
|
||||
Id int `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
File struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Ext string `json:"ext"`
|
||||
Size int `json:"size"`
|
||||
Md5 string `json:"md5"`
|
||||
Url string `json:"url"`
|
||||
} `json:"file"`
|
||||
Score struct {
|
||||
Up int `json:"up"`
|
||||
Down int `json:"down"`
|
||||
Total int `json:"total"`
|
||||
} `json:"score"`
|
||||
Tags struct {
|
||||
General []string `json:"general"`
|
||||
Artist []string `json:"artist"`
|
||||
Contributor []interface{} `json:"contributor"`
|
||||
Copyright []string `json:"copyright"`
|
||||
Character []interface{} `json:"character"`
|
||||
Species []string `json:"species"`
|
||||
Invalid []interface{} `json:"invalid"`
|
||||
Meta []string `json:"meta"`
|
||||
Lore []interface{} `json:"lore"`
|
||||
} `json:"tags"`
|
||||
Rating string `json:"rating"`
|
||||
FavCount int `json:"fav_count"`
|
||||
Sources []string `json:"sources"`
|
||||
Description string `json:"description"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
}
|
||||
|
||||
func RegisterPorbCommands(a *app.App) {
|
||||
username, _ := a.Config.CommandExtras["e621_username"]
|
||||
password, _ := a.Config.CommandExtras["e621_password"]
|
||||
|
||||
if username == "" || password == "" {
|
||||
log.Println("Not registering e621 command...")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
a.RegisterCommand("e621", registerE621(a))
|
||||
|
||||
a.RegisterCommandAlias("porb", "e621")
|
||||
}
|
||||
|
||||
func registerE621(a *app.App) app.Callback {
|
||||
return func(h *app.Handler, args []string) app.Error {
|
||||
var options struct {
|
||||
Order string
|
||||
Rating string
|
||||
}
|
||||
|
||||
cmd := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
|
||||
cmd.StringVar(&options.Order, "order", "random", "Search order")
|
||||
cmd.StringVar(&options.Rating, "rating", "e", "Search rating")
|
||||
|
||||
cmd.Parse(args)
|
||||
|
||||
req, err := http.NewRequest(
|
||||
http.MethodGet,
|
||||
fmt.Sprintf(
|
||||
"https://e621.net/posts.json/?limit=1&tags=order:%s+rating:%s+%s",
|
||||
options.Order,
|
||||
options.Rating,
|
||||
strings.Join(cmd.Args(), "+"),
|
||||
),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Failed to make request",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
username, _ := a.Config.CommandExtras["e621_username"]
|
||||
password, _ := a.Config.CommandExtras["e621_password"]
|
||||
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("User-Agent", fmt.Sprintf("Lynxie/2.0 (by %s on e621)", username))
|
||||
req.SetBasicAuth(username, password)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Failed to do request",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var data struct {
|
||||
Posts []post `json:"posts"`
|
||||
}
|
||||
json.NewDecoder(res.Body).Decode(&data)
|
||||
|
||||
if len(data.Posts) == 0 {
|
||||
return app.Error{
|
||||
Msg: "No posts found",
|
||||
Err: fmt.Errorf("no posts found"),
|
||||
}
|
||||
}
|
||||
|
||||
var description string
|
||||
if len(data.Posts[0].Description) > 0 {
|
||||
description = data.Posts[0].Description
|
||||
} else {
|
||||
description = "No description provided."
|
||||
}
|
||||
|
||||
var generalTags string
|
||||
if len(data.Posts[0].Tags.General) > 0 {
|
||||
generalTags = strings.Join(data.Posts[0].Tags.General[:20], ", ")
|
||||
} else {
|
||||
generalTags = "No tags provided."
|
||||
}
|
||||
|
||||
h.Session.ChannelMessageSendComplex(h.Message.ChannelID, &discordgo.MessageSend{
|
||||
Embed: &discordgo.MessageEmbed{
|
||||
Title: "E621",
|
||||
Description: description,
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{
|
||||
Name: "Score",
|
||||
Value: fmt.Sprintf("⬆️ %d | ⬇️ %d", data.Posts[0].Score.Up, data.Posts[0].Score.Down),
|
||||
},
|
||||
{
|
||||
Name: "Favorites",
|
||||
Value: fmt.Sprintf("%d", data.Posts[0].FavCount),
|
||||
},
|
||||
{
|
||||
Name: "Comments",
|
||||
Value: fmt.Sprintf("%d", data.Posts[0].CommentCount),
|
||||
},
|
||||
{
|
||||
Name: "Source(s)",
|
||||
Value: strings.Join(data.Posts[0].Sources, ", "),
|
||||
Inline: false,
|
||||
},
|
||||
{
|
||||
Name: "Tag(s)",
|
||||
Value: generalTags,
|
||||
Inline: false,
|
||||
},
|
||||
},
|
||||
Image: &discordgo.MessageEmbedImage{
|
||||
URL: data.Posts[0].File.Url,
|
||||
},
|
||||
Footer: &discordgo.MessageEmbedFooter{
|
||||
Text: fmt.Sprintf(
|
||||
"ID: %d | Created: %s",
|
||||
data.Posts[0].Id,
|
||||
data.Posts[0].CreatedAt.Format(time.DateTime),
|
||||
),
|
||||
},
|
||||
Color: color.RGBToDiscord(255, 255, 255),
|
||||
},
|
||||
Reference: h.Reference,
|
||||
})
|
||||
|
||||
return app.Error{}
|
||||
}
|
||||
}
|
148
pkg/commands/tinyfox/tinyfox.go
Normal file
148
pkg/commands/tinyfox/tinyfox.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package tinyfox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Fluffy-Bean/lynxie/app"
|
||||
"github.com/Fluffy-Bean/lynxie/internal/color"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var client = http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
var animals = []string{
|
||||
"fox",
|
||||
"yeen",
|
||||
"dog",
|
||||
"guara",
|
||||
"serval",
|
||||
"ott",
|
||||
"jackal",
|
||||
"bleat",
|
||||
"woof",
|
||||
"chi",
|
||||
"puma",
|
||||
"skunk",
|
||||
"tig",
|
||||
"wah",
|
||||
"manul",
|
||||
"snep",
|
||||
"jaguar",
|
||||
"badger",
|
||||
"chee",
|
||||
"racc",
|
||||
"bear",
|
||||
"capy",
|
||||
"bun",
|
||||
"marten",
|
||||
"caracal",
|
||||
"snek",
|
||||
"shiba",
|
||||
"dook",
|
||||
"leo",
|
||||
"yote",
|
||||
"poss",
|
||||
"lynx",
|
||||
}
|
||||
|
||||
var animalAliases = map[string]string{
|
||||
"hyena": "yeen",
|
||||
"serv": "serval",
|
||||
"otter": "ott",
|
||||
"deer": "bleat",
|
||||
"wolf": "woof",
|
||||
"tiger": "tig",
|
||||
"red-panda": "wah",
|
||||
"panda": "wah",
|
||||
"manual": "manul",
|
||||
"palas": "manul",
|
||||
"palas-cat": "manul",
|
||||
"snow-leopard": "snep",
|
||||
"jag": "jaguar",
|
||||
"cheetah": "chee",
|
||||
"raccoon": "racc",
|
||||
"rac": "racc",
|
||||
"capybara": "capy",
|
||||
"bunny": "bun",
|
||||
"carac": "caracal",
|
||||
"snake": "snek",
|
||||
"ferret": "dook",
|
||||
"leopard": "leo",
|
||||
"coyote": "yote",
|
||||
"possum": "poss",
|
||||
"opossum": "poss",
|
||||
}
|
||||
|
||||
func RegisterTinyfoxCommands(a *app.App) {
|
||||
a.RegisterCommand("animal", registerAnimal(a))
|
||||
|
||||
a.RegisterCommandAlias("a", "animal")
|
||||
}
|
||||
|
||||
func registerAnimal(a *app.App) app.Callback {
|
||||
return func(h *app.Handler, args []string) app.Error {
|
||||
if len(args) < 1 {
|
||||
return app.Error{
|
||||
Msg: "Animal name is required!",
|
||||
Err: errors.New("animal name is required"),
|
||||
}
|
||||
}
|
||||
|
||||
animal := args[0]
|
||||
|
||||
if !slices.Contains(animals, animal) {
|
||||
alias, ok := animalAliases[animal]
|
||||
if !ok {
|
||||
return app.Error{
|
||||
Msg: fmt.Sprintf("Animal \"%s\" is invalid. The following animals are supported:\n%s", animal, strings.Join(animals, ", ")),
|
||||
Err: errors.New("entered invalid animal name"),
|
||||
}
|
||||
}
|
||||
animal = alias
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://api.tinyfox.dev/img?animal="+animal, nil)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Failed to make request",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return app.Error{
|
||||
Msg: "Failed to do request",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
h.Session.ChannelMessageSendComplex(h.Message.ChannelID, &discordgo.MessageSend{
|
||||
Embed: &discordgo.MessageEmbed{
|
||||
Title: "Animal",
|
||||
Image: &discordgo.MessageEmbedImage{
|
||||
URL: "attachment://animal.png",
|
||||
},
|
||||
Color: color.RGBToDiscord(255, 255, 255),
|
||||
},
|
||||
Files: []*discordgo.File{
|
||||
{
|
||||
Name: "animal.png",
|
||||
ContentType: "",
|
||||
Reader: res.Body,
|
||||
},
|
||||
},
|
||||
Reference: h.Reference,
|
||||
})
|
||||
|
||||
return app.Error{}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue