-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathattachment.go
237 lines (208 loc) · 5.32 KB
/
attachment.go
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
package llm
import (
"crypto/md5"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
)
///////////////////////////////////////////////////////////////////////////////
// TYPES
// General attachment metadata
type AttachmentMeta struct {
Id string `json:"id,omitempty"`
Filename string `json:"filename,omitempty"`
ExpiresAt uint64 `json:"expires_at,omitempty"`
Caption string `json:"transcript,omitempty"`
Data []byte `json:"data"`
Type string `json:"type"`
}
// OpenAI image metadata
type ImageMeta struct {
Url string `json:"url,omitempty"`
Data []byte `json:"b64_json,omitempty"`
Prompt string `json:"revised_prompt,omitempty"`
}
// Attachment for messages
type Attachment struct {
meta *AttachmentMeta
image *ImageMeta
}
const (
defaultMimetype = "application/octet-stream"
)
////////////////////////////////////////////////////////////////////////////////
// LIFECYCLE
// NewAttachment creates a new, empty attachment
func NewAttachment() *Attachment {
return new(Attachment)
}
// NewAttachment with OpenAI image
func NewAttachmentWithImage(image *ImageMeta) *Attachment {
return &Attachment{image: image}
}
// ReadAttachment returns an attachment from a reader object.
// It is the responsibility of the caller to close the reader.
func ReadAttachment(r io.Reader, mimetype ...string) (*Attachment, error) {
var filename, typ string
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
if f, ok := r.(*os.File); ok {
filename = f.Name()
}
if len(mimetype) > 0 {
typ = mimetype[0]
}
return &Attachment{
meta: &AttachmentMeta{
Filename: filename,
Data: data,
Type: typ,
},
}, nil
}
////////////////////////////////////////////////////////////////////////////////
// STRINGIFY
// Convert JSON into an attachment
func (a *Attachment) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &a.meta)
}
// Convert an attachment into JSON
func (a *Attachment) MarshalJSON() ([]byte, error) {
// Create a JSON representation
var j struct {
Id string `json:"id,omitempty"`
Filename string `json:"filename,omitempty"`
Type string `json:"type"`
Bytes uint64 `json:"bytes"`
Hash string `json:"hash,omitempty"`
Caption string `json:"caption,omitempty"`
}
j.Type = a.Type()
j.Caption = a.Caption()
j.Hash = a.Hash()
j.Filename = a.Filename()
if a.meta != nil {
j.Id = a.meta.Id
j.Bytes = uint64(len(a.meta.Data))
} else if a.image != nil {
j.Bytes = uint64(len(a.image.Data))
}
return json.Marshal(j)
}
// Stringify an attachment
func (a *Attachment) String() string {
data, err := json.MarshalIndent(a, "", " ")
if err != nil {
return err.Error()
}
return string(data)
}
////////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS
// Compute and print the MD5 hash
func (a *Attachment) Hash() string {
hash := md5.New()
hash.Write(a.Data())
return fmt.Sprintf("%x", hash.Sum(nil))
}
// Write out attachment
func (a *Attachment) Write(w io.Writer) (int, error) {
if a.meta != nil {
return w.Write(a.meta.Data)
}
if a.image != nil {
return w.Write(a.image.Data)
}
return 0, io.EOF
}
// Return the filename of an attachment
func (a *Attachment) Filename() string {
if a.meta != nil && a.meta.Filename != "" {
return a.meta.Filename
}
// Obtain filename from MD5
if ext, err := mime.ExtensionsByType(a.Type()); err == nil && len(ext) > 0 {
return a.Hash() + ext[0]
}
return ""
}
// Return the raw attachment data
func (a *Attachment) Data() []byte {
if a.meta != nil {
return a.meta.Data
}
if a.image != nil {
return a.image.Data
}
return nil
}
// Return the caption for the attachment
func (a *Attachment) Caption() string {
if a.meta != nil {
return strings.TrimSpace(a.meta.Caption)
}
if a.image != nil {
return strings.TrimSpace(a.image.Prompt)
}
return ""
}
// Return the mime media type for the attachment, based
// on the data and/or filename extension. Returns an empty string if
// there is no data or filename
func (a *Attachment) Type() string {
// If there's a mimetype set, use this
if a.meta != nil && a.meta.Type != "" {
return a.meta.Type
}
// If there's no data or filename, return empty
if len(a.Data()) == 0 && a.Filename() == "" {
return ""
}
// Mimetype based on content
mimetype := defaultMimetype
if len(a.Data()) > 0 {
mimetype = http.DetectContentType(a.Data())
if mimetype != defaultMimetype {
return mimetype
}
}
// Mimetype based on filename
if a.meta != nil && a.meta.Filename != "" {
// Detect mimetype from extension
mimetype = mime.TypeByExtension(filepath.Ext(a.meta.Filename))
}
// Return the default mimetype
return mimetype
}
func (a *Attachment) Url() string {
return "data:" + a.Type() + ";base64," + base64.StdEncoding.EncodeToString(a.Data())
}
// Streaming includes the ability to append data
func (a *Attachment) Append(other *Attachment) {
if a.meta != nil {
if other.meta.Id != "" {
a.meta.Id = other.meta.Id
}
if other.meta.Filename != "" {
a.meta.Filename = other.meta.Filename
}
if other.meta.ExpiresAt != 0 {
a.meta.ExpiresAt = other.meta.ExpiresAt
}
if other.meta.Caption != "" {
a.meta.Caption += other.meta.Caption
}
if len(other.meta.Data) > 0 {
a.meta.Data = append(a.meta.Data, other.meta.Data...)
}
}
// TODO: Append for image
}