label_printer/epson/faxmachine/ntfy/ntfy.go

188 lines
3.8 KiB
Go

package ntfy
import (
"context"
"encoding/json"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"log"
"log/slog"
"net/http"
"strings"
"time"
"github.com/lestrrat-go/dither"
"github.com/nfnt/resize"
_ "golang.org/x/image/webp"
"nhooyr.io/websocket"
"git.sr.ht/~guacamolie/faxmachine/config"
"git.sr.ht/~guacamolie/faxmachine/escpos"
"git.sr.ht/~guacamolie/faxmachine/printjob"
)
type Instance struct {
logger *slog.Logger
host string
accessToken string
topics map[string]config.Topic
}
func NewInstance(conf config.Ntfy, logger *slog.Logger) *Instance {
return &Instance{
host: conf.Host,
accessToken: conf.AccessToken,
topics: conf.Topics,
logger: logger,
}
}
type message struct {
Event string `json:"event"`
Topic string `json:"topic"`
Message string `json:"message"`
Time int64 `json:"time"`
Attachment attachment `json:"attachment"`
}
type attachment struct {
Name string `json:"name"`
Type string `json:"type"`
Size int `json:"size"`
URL string `json:"url"`
}
func (i *Instance) Subscribe(ctx context.Context, queue chan<- printjob.Job) error {
topicNames := make([]string, 0, len(i.topics))
for k := range i.topics {
topicNames = append(topicNames, k)
}
uri := fmt.Sprintf("wss://%s/%s/ws", i.host, strings.Join(topicNames, ","))
header := make(http.Header)
if i.accessToken != "" {
header.Set("Authorization", fmt.Sprintf("Bearer %s", i.accessToken))
}
conn, _, err := websocket.Dial(ctx, uri, &websocket.DialOptions{
HTTPHeader: header,
})
if err != nil {
return fmt.Errorf("couldn't connect to instance: %v", err)
}
go i.drain(ctx, conn, queue)
return nil
}
func (i *Instance) drain(ctx context.Context, conn *websocket.Conn, queue chan<- printjob.Job) {
for {
_, data, err := conn.Read(ctx)
if err != nil {
log.Printf("failed to read: %v\n", err)
return
}
var msg message
if err := json.Unmarshal(data, &msg); err != nil {
log.Printf("failed to unmarshal: %v\n", err)
return
}
if msg.Event != "message" {
continue
}
sender := i.topics[msg.Topic].Name
i.logger.Info("received print job",
slog.Group("topic",
slog.String("topic", msg.Topic),
slog.String("name", sender),
),
slog.Group("message",
slog.String("message", msg.Message),
slog.String("attachment", msg.Attachment.URL),
),
)
select {
case queue <- &job{sender, msg}:
default:
}
}
}
type job struct {
sender string
msg message
}
func (j *job) Sender() string {
return j.msg.Topic
}
func (j *job) Time() time.Time {
return time.Unix(j.msg.Time, 0)
}
func (j *job) Description() string {
switch {
case j.msg.Message != "":
return j.msg.Message
case j.msg.Attachment.Name != "":
return j.msg.Attachment.Name
default:
return ""
}
}
func (j *job) Print(ctx context.Context, p *escpos.Printer) error {
text := j.msg.Message
if len(text) > 2000 {
text = text[:2000]
}
if strings.HasPrefix(text, "You received a file: ") {
text = ""
}
text, _ = strings.CutPrefix(text, "[guac::prefix]")
if j.msg.Attachment.URL != "" {
resp, err := http.Get(j.msg.Attachment.URL)
if err != nil {
return fmt.Errorf("failed to get attachment: %w", err)
}
img, _, err := image.Decode(resp.Body)
if err != nil {
return fmt.Errorf("failed to decode image: %w", err)
}
img = resize.Resize(512, 0, img, resize.Lanczos3)
img = dither.Monochrome(dither.FloydSteinberg, img, 1.18)
if err := p.PrintImage(img); err != nil {
return fmt.Errorf("failed to print image: %w", err)
}
}
if text != "" {
fmt.Fprintf(p, "\n%s\n", text)
}
return nil
}
func (j *job) OnPrinted(ctx context.Context) {
// Nothing to do since we can't reply to the job submitter.
}
func (j *job) OnShredded(ctx context.Context, err error) {
// Nothing to do since we can't reply to the job submitter.
}