Skip to content

Commit b81d2ba

Browse files
authored
SendFile (#23)
* Add SendFile and SendImage * Add DeleteFile\DeleteImage
1 parent 10a1ed1 commit b81d2ba

File tree

8 files changed

+740
-321
lines changed

8 files changed

+740
-321
lines changed

channel.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package stream_chat //nolint: golint
22

33
import (
44
"errors"
5+
"io"
56
"net/http"
67
"net/url"
78
"path"
@@ -325,6 +326,50 @@ func (c *Client) CreateChannel(chanType, chanID, userID string, data map[string]
325326
return ch, err
326327
}
327328

329+
type SendFileRequest struct {
330+
Reader io.Reader `json:"-"`
331+
// name of the file would be stored
332+
FileName string
333+
// User object; required
334+
User *User
335+
// file content type, required for SendImage
336+
ContentType string
337+
}
338+
339+
// SendFile sends file to the channel. Returns file url or error
340+
func (ch *Channel) SendFile(request SendFileRequest) (string, error) {
341+
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "file")
342+
343+
return ch.client.sendFile(p, request)
344+
}
345+
346+
// SendFile sends image to the channel. Returns file url or error
347+
func (ch *Channel) SendImage(request SendFileRequest) (string, error) {
348+
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "image")
349+
350+
return ch.client.sendFile(p, request)
351+
}
352+
353+
// DeleteFile removes uploaded file
354+
func (ch *Channel) DeleteFile(location string) error {
355+
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "file")
356+
357+
var params = url.Values{}
358+
params.Set("url", location)
359+
360+
return ch.client.makeRequest(http.MethodDelete, p, params, nil, nil)
361+
}
362+
363+
// DeleteImage removes uploaded image
364+
func (ch *Channel) DeleteImage(location string) error {
365+
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "image")
366+
367+
var params = url.Values{}
368+
params.Set("url", location)
369+
370+
return ch.client.makeRequest(http.MethodDelete, p, params, nil, nil)
371+
}
372+
328373
// todo: cleanup this
329374
func (ch *Channel) refresh() error {
330375
options := map[string]interface{}{

channel_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package stream_chat // nolint: golint
22

33
import (
4+
"os"
5+
"path"
46
"testing"
57

68
"github.com/stretchr/testify/assert"
@@ -255,3 +257,72 @@ func TestChannel_DemoteModerators(t *testing.T) {
255257
func TestChannel_UnBanUser(t *testing.T) {
256258

257259
}
260+
261+
func TestChannel_SendFile(t *testing.T) {
262+
c := initClient(t)
263+
ch := initChannel(t, c)
264+
265+
var url string
266+
267+
t.Run("Send file", func(t *testing.T) {
268+
file, err := os.Open(path.Join("testdata", "helloworld.txt"))
269+
if err != nil {
270+
t.Fatal(err)
271+
}
272+
273+
url, err = ch.SendFile(SendFileRequest{
274+
Reader: file,
275+
FileName: "HelloWorld.txt",
276+
User: randomUser(),
277+
})
278+
if err != nil {
279+
t.Fatalf("send file failed: %s", err)
280+
}
281+
if url == "" {
282+
t.Fatal("upload file returned empty url")
283+
}
284+
})
285+
286+
t.Run("Delete file", func(t *testing.T) {
287+
err := ch.DeleteFile(url)
288+
if err != nil {
289+
t.Fatalf("delete file failed: %s", err.Error())
290+
}
291+
})
292+
}
293+
294+
func TestChannel_SendImage(t *testing.T) {
295+
c := initClient(t)
296+
ch := initChannel(t, c)
297+
298+
var url string
299+
300+
t.Run("Send image", func(t *testing.T) {
301+
file, err := os.Open(path.Join("testdata", "helloworld.jpg"))
302+
if err != nil {
303+
t.Fatal(err)
304+
}
305+
306+
url, err = ch.SendImage(SendFileRequest{
307+
Reader: file,
308+
FileName: "HelloWorld.jpg",
309+
User: randomUser(),
310+
ContentType: "image/jpeg",
311+
})
312+
313+
if err != nil {
314+
t.Fatalf("Send image failed: %s", err.Error())
315+
}
316+
317+
if url == "" {
318+
t.Fatal("upload image returned empty url")
319+
}
320+
})
321+
322+
t.Run("Delete image", func(t *testing.T) {
323+
err := ch.DeleteImage(url)
324+
if err != nil {
325+
t.Fatalf("delete image failed: %s", err.Error())
326+
}
327+
})
328+
}

client.go

Lines changed: 148 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ import (
77
"encoding/json"
88
"errors"
99
"fmt"
10+
"io"
1011
"io/ioutil"
12+
"mime/multipart"
1113
"net/http"
14+
"net/textproto"
1215
"net/url"
16+
"os"
17+
"strings"
1318
"time"
1419

1520
"github.com/getstream/easyjson"
@@ -72,31 +77,52 @@ func (c *Client) requestURL(path string, values url.Values) (string, error) {
7277
return _url.String(), nil
7378
}
7479

75-
func (c *Client) makeRequest(method, path string,
76-
params url.Values, data interface{}, result easyjson.Unmarshaler) error {
80+
func (c *Client) newRequest(method, path string, params url.Values, data interface{}) (*http.Request, error) {
7781
_url, err := c.requestURL(path, params)
7882
if err != nil {
79-
return err
83+
return nil, err
8084
}
8185

82-
var body []byte
83-
if m, ok := data.(easyjson.Marshaler); ok {
84-
body, err = easyjson.Marshal(m)
85-
} else {
86-
body, err = json.Marshal(data)
86+
r, err := http.NewRequest(method, _url, nil)
87+
if err != nil {
88+
return nil, err
8789
}
8890

89-
if err != nil {
90-
return err
91+
c.setHeaders(r)
92+
93+
switch t := data.(type) {
94+
case easyjson.Marshaler:
95+
b, err := easyjson.Marshal(t)
96+
if err != nil {
97+
return nil, err
98+
}
99+
r.Body = ioutil.NopCloser(bytes.NewReader(b))
100+
101+
case io.ReadCloser:
102+
r.Body = t
103+
104+
case io.Reader:
105+
r.Body = ioutil.NopCloser(t)
106+
107+
default:
108+
b, err := json.Marshal(data)
109+
if err != nil {
110+
return nil, err
111+
}
112+
r.Body = ioutil.NopCloser(bytes.NewReader(b))
91113
}
92114

93-
r, err := http.NewRequest(method, _url, bytes.NewReader(body))
115+
return r, nil
116+
}
117+
118+
func (c *Client) makeRequest(method, path string, params url.Values,
119+
data interface{}, result easyjson.Unmarshaler) error {
120+
121+
r, err := c.newRequest(method, path, params, data)
94122
if err != nil {
95123
return err
96124
}
97125

98-
c.setHeaders(r)
99-
100126
resp, err := c.HTTP.Do(r)
101127
if err != nil {
102128
return err
@@ -138,6 +164,115 @@ func (c *Client) VerifyWebhook(body []byte, signature []byte) (valid bool) {
138164
return hmac.Equal(signature, expectedMAC)
139165
}
140166

167+
type sendFileResponse struct {
168+
File string `json:"file"`
169+
}
170+
171+
//nolint:gochecknoglobals
172+
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
173+
174+
func escapeQuotes(s string) string {
175+
return quoteEscaper.Replace(s)
176+
}
177+
178+
// this adds possible to set content type
179+
type multipartForm struct {
180+
*multipart.Writer
181+
}
182+
183+
// CreateFormFile is a convenience wrapper around CreatePart. It creates
184+
// a new form-data header with the provided field name, file name and content type
185+
func (form *multipartForm) CreateFormFile(fieldName, filename, contentType string) (io.Writer, error) {
186+
h := make(textproto.MIMEHeader)
187+
188+
h.Set("Content-Disposition",
189+
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
190+
escapeQuotes(fieldName), escapeQuotes(filename)))
191+
192+
if contentType == "" {
193+
contentType = "application/octet-stream"
194+
}
195+
196+
h.Set("Content-Type", contentType)
197+
198+
return form.Writer.CreatePart(h)
199+
}
200+
201+
func (form *multipartForm) setData(fieldName string, data easyjson.Marshaler) error {
202+
field, err := form.CreateFormField(fieldName)
203+
if err != nil {
204+
return err
205+
}
206+
_, err = easyjson.MarshalToWriter(data, field)
207+
return err
208+
}
209+
210+
func (form *multipartForm) setFile(fieldName string, r io.Reader, fileName, contentType string) error {
211+
file, err := form.CreateFormFile(fieldName, fileName, contentType)
212+
if err != nil {
213+
return err
214+
}
215+
_, err = io.Copy(file, r)
216+
217+
return err
218+
}
219+
220+
func (c *Client) sendFile(url string, opts SendFileRequest) (string, error) {
221+
if opts.User == nil {
222+
return "", errors.New("user is nil")
223+
}
224+
225+
tmpfile, err := ioutil.TempFile("", opts.FileName)
226+
if err != nil {
227+
return "", err
228+
}
229+
230+
defer func() {
231+
_ = tmpfile.Close()
232+
_ = os.Remove(tmpfile.Name())
233+
}()
234+
235+
form := multipartForm{multipart.NewWriter(tmpfile)}
236+
237+
if err = form.setData("user", opts.User); err != nil {
238+
return "", err
239+
}
240+
241+
err = form.setFile("file", opts.Reader, opts.FileName, opts.ContentType)
242+
if err != nil {
243+
return "", err
244+
}
245+
246+
err = form.Close()
247+
if err != nil {
248+
return "", err
249+
}
250+
251+
if _, err = tmpfile.Seek(0, 0); err != nil {
252+
return "", err
253+
}
254+
255+
r, err := c.newRequest(http.MethodPost, url, nil, tmpfile)
256+
if err != nil {
257+
return "", err
258+
}
259+
260+
r.Header.Set("Content-Type", form.FormDataContentType())
261+
262+
res, err := c.HTTP.Do(r)
263+
if err != nil {
264+
return "", err
265+
}
266+
267+
var resp sendFileResponse
268+
err = c.parseResponse(res, &resp)
269+
if err != nil {
270+
return "", err
271+
}
272+
273+
return resp.File, err
274+
}
275+
141276
// NewClient creates new stream chat api client
142277
func NewClient(apiKey string, apiSecret []byte) (*Client, error) {
143278
switch {

stream_chat.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ type StreamChannel interface {
8686
Query(data map[string]interface{}) error
8787
Show(userID string) error
8888
Hide(userID string) error
89+
SendFile(request SendFileRequest) (url string, err error)
90+
SendImage(request SendFileRequest) (url string, err error)
91+
DeleteFile(location string) error
92+
DeleteImage(location string) error
8993

9094
// event.go
9195
SendEvent(event *Event, userID string) error

0 commit comments

Comments
 (0)