Lynxie/pkg/commands/img/img.go

316 lines
6.9 KiB
Go

package img
import (
"bufio"
"bytes"
_ "embed"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"net/http"
"strings"
"time"
"git.sr.ht/~sbinet/gg"
"github.com/Fluffy-Bean/lynxie/_resources"
"github.com/Fluffy-Bean/lynxie/internal/color"
"github.com/Fluffy-Bean/lynxie/internal/errors"
"github.com/Fluffy-Bean/lynxie/internal/handler"
"github.com/bwmarrin/discordgo"
)
const maxFileSize = 1024 * 1024 * 10 // 10MB
var client = http.Client{
Timeout: 10 * time.Second,
}
func RegisterImgCommands(bot *handler.Bot) {
bot.RegisterCommand("saveable", registerSaveable(bot))
bot.RegisterCommandAlias("gif", "saveable")
bot.RegisterCommand("caption", registerCaption(bot))
bot.RegisterCommandAlias("c", "caption")
}
func registerSaveable(bot *handler.Bot) handler.Callback {
return func(h *handler.Handler, args []string) errors.Error {
fileEndpoint, err := findClosestImage(h)
if err != nil {
return errors.Error{
Msg: "Could not get image",
Err: err,
}
}
req, err := http.NewRequest(http.MethodGet, fileEndpoint, nil)
if err != nil {
return errors.Error{
Msg: "",
Err: err,
}
}
if req.ContentLength > maxFileSize {
return errors.Error{
Msg: "Could not get image",
Err: fmt.Errorf("requested file is too big"),
}
}
res, err := client.Do(req)
if err != nil {
return errors.Error{
Msg: "failed to fetch image",
Err: err,
}
}
defer res.Body.Close()
_, err = 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,
})
if err != nil {
return errors.Error{
Msg: "failed to send saveable message",
Err: err,
}
}
return errors.Error{}
}
}
func registerCaption(bot *handler.Bot) handler.Callback {
return func(h *handler.Handler, args []string) errors.Error {
fileEndpoint, err := findClosestImage(h)
if err != nil {
return errors.Error{
Msg: "Could not get image",
Err: err,
}
}
req, err := http.NewRequest(http.MethodGet, fileEndpoint, nil)
if err != nil {
return errors.Error{
Msg: "failed to fetch image",
Err: err,
}
}
if req.ContentLength > maxFileSize {
return errors.Error{
Msg: "Could not get image",
Err: fmt.Errorf("requested file is too big"),
}
}
res, err := client.Do(req)
if err != nil {
return errors.Error{
Msg: "failed to fetch image",
Err: err,
}
}
defer res.Body.Close()
buff, err := io.ReadAll(res.Body)
if err != nil {
return errors.Error{
Msg: "failed to read image",
Err: err,
}
}
img, err := loadImageFromBytes(buff)
if err != nil {
return errors.Error{
Msg: "failed to load image",
Err: fmt.Errorf("Failed to load image " + err.Error()),
}
}
imgWidth, imgHeight := img.Bounds().Dx(), img.Bounds().Dy()
captionSize := float64(imgWidth / 15)
if captionSize < 16 {
captionSize = 16
} else if captionSize > 50 {
captionSize = 50
}
// 8px padding all around
_, captionHeight := measureText(_resources.FontRoboto, strings.Join(args, " "), captionSize, imgWidth-16)
captionHeight += 16
if captionHeight < 128 {
captionHeight = 128
}
canvas := gg.NewContext(imgWidth, imgHeight+captionHeight)
err = canvas.LoadFontFaceFromBytes(_resources.FontRoboto, captionSize)
if err != nil {
return errors.Error{
Msg: "failed to load font",
Err: err,
}
}
canvas.SetRGBA(1, 1, 1, 1)
canvas.Clear()
canvas.SetRGBA(0, 0, 0, 1)
canvas.DrawStringWrapped(
strings.Join(args, " "),
float64(imgWidth/2), float64(captionHeight/2),
0.5, 0.5, float64(imgWidth),
1.5,
gg.AlignCenter,
)
canvas.DrawImage(img, 0, captionHeight)
var export bytes.Buffer
err = canvas.EncodeJPG(
bufio.NewWriter(&export),
&jpeg.Options{Quality: 100},
)
if err != nil {
return errors.Error{
Msg: "failed to encode JPEG",
Err: 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,
})
if err != nil {
return errors.Error{
Msg: "failed to send caption message",
Err: err,
}
}
return errors.Error{}
}
}
func loadImageFromBytes(buff []byte) (image.Image, error) {
var (
img image.Image
err error
)
contentType := http.DetectContentType(buff)
switch contentType {
case "image/png":
img, err = png.Decode(bytes.NewReader(buff))
if err != nil {
return nil, fmt.Errorf("failed to decode png: %s", err)
}
break
case "image/jpeg":
img, err = jpeg.Decode(bytes.NewReader(buff))
if err != nil {
return nil, fmt.Errorf("failed to decode jpeg: %s", err)
}
break
default:
return nil, fmt.Errorf("unknown or unsupported format: %s", contentType)
}
return img, nil
}
func findClosestImage(h *handler.Handler) (string, error) {
if len(h.Message.Attachments) >= 1 {
if h.Message.Attachments[0].Size > maxFileSize {
return "", fmt.Errorf("file size is too big")
}
return h.Message.Attachments[0].ProxyURL, nil
}
if h.Message.ReferencedMessage != nil {
message := h.Message.ReferencedMessage
if len(message.Attachments) >= 1 {
if message.Attachments[0].Size > maxFileSize {
return "", fmt.Errorf("file size is too big")
}
return message.Attachments[0].ProxyURL, nil
}
if len(message.Embeds) >= 1 && message.Embeds[0].Image != nil {
return message.Embeds[0].Image.ProxyURL, nil
}
}
history, err := h.Session.ChannelMessages(h.Message.ChannelID, 10, h.Message.ID, "", "")
if err != nil {
return "", err
}
for _, message := range history {
if len(message.Attachments) >= 1 {
if message.Attachments[0].Size > maxFileSize {
return "", fmt.Errorf("file size is too big")
}
return message.Attachments[0].ProxyURL, nil
}
if len(message.Embeds) >= 1 && message.Embeds[0].Image != nil {
return message.Embeds[0].Image.ProxyURL, nil
}
}
return "", fmt.Errorf("no files exists")
}
func measureText(font []byte, text string, size float64, width int) (int, int) {
canvas := gg.NewContext(width, width)
err := canvas.LoadFontFaceFromBytes(font, size)
if err != nil {
return 0, 0
}
wrappedText := strings.Join(canvas.WordWrap(text, float64(width)), "\n")
lineWidth, lineHeight := canvas.MeasureMultilineString(wrappedText, 1.5)
return int(lineWidth), int(lineHeight)
}