Update pipeline

And commit half-baked code restructure
This commit is contained in:
Michał Gdula 2025-04-28 22:32:30 +01:00
parent 5d67b2e2b0
commit 2e5f4bce11
11 changed files with 32 additions and 59 deletions

View 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
View 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")
}

Binary file not shown.

190
pkg/commands/porb/porb.go Normal file
View 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{}
}
}

View 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{}
}
}