Lynxie/pkg/commands/img/img.go
Fluffy-Bean 2e5f4bce11 Update pipeline
And commit half-baked code restructure
2025-04-28 22:32:30 +01:00

259 lines
5.4 KiB
Go

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