~handlerug/handlebot

ref: e2138e35984f582b50a481b58c332afae2841b1f handlebot/urlpreview/gemini.go -rw-r--r-- 2.0 KiB
e2138e35Umar Getagazov urlpreview: Parse Content-Disposition 5 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package urlpreview

import (
	"bufio"
	"context"
	"errors"
	"io"
	"mime"
	"net/url"

	"git.sr.ht/~adnano/go-gemini"
)

type gemTitleExtractor struct {
	Result string
	Level  int
}

var errTooManyRedirects = errors.New("Too many redirects")

func (p *Previewer) gemini(ctx context.Context, u *url.URL) (string, error) {
	req := &gemini.Request{URL: u, Host: "", Certificate: nil}
	resp, err := doGemini(ctx, &p.GeminiClient, req, nil)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.Status != gemini.StatusSuccess {
		return "", ErrBadResponse
	}

	mediatype, _, err := mime.ParseMediaType(resp.Meta)
	if err != nil {
		return "", err
	}
	if mediatype != "text/gemini" {
		return resp.Meta, nil
	}

	lr := io.LimitedReader{R: resp.Body, N: 100 * 1024}
	var body io.Reader = bufio.NewReader(&lr)

	var extractor gemTitleExtractor
	err = gemini.ParseLines(body, extractor.Handle)
	if err != nil {
		return "", err
	}

	if extractor.Result != "" {
		return extractor.Result, nil
	}
	return "", nil
}

func doGemini(ctx context.Context, client *gemini.Client, req *gemini.Request, via []*gemini.Request) (*gemini.Response, error) {
	resp, err := client.Do(ctx, req)
	if err != nil {
		return resp, err
	}

	if resp.Status.Class() == gemini.StatusRedirect {
		via = append(via, req)
		if len(via) > 5 {
			return resp, errTooManyRedirects
		}

		target, err := url.Parse(resp.Meta)
		if err != nil {
			return resp, err
		}
		target = req.URL.ResolveReference(target)
		redirect := *req
		redirect.URL = target
		return doGemini(ctx, client, &redirect, via)
	}

	return resp, err
}

func (e *gemTitleExtractor) Handle(line gemini.Line) {
	switch line := line.(type) {
	case gemini.LineHeading1:
		if e.Level < 4 {
			e.Result = string(line)
			e.Level = 4
		}
	case gemini.LineHeading2:
		if e.Level < 3 {
			e.Result = string(line)
			e.Level = 3
		}
	case gemini.LineHeading3:
		if e.Level < 2 {
			e.Result = string(line)
			e.Level = 2
		}
	case gemini.LineText:
		if e.Level < 1 {
			e.Result = string(line)
			e.Level = 1
		}
	}
}