188 lines
3.8 KiB
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.
|
|
}
|