~thrrgilag/woodstock

6023731dd35f43391db5067c936abdbb35a2dedb — Morgan McMillian 2 years ago aafe21a
added /files endpoint methods and tests
9 files changed, 403 insertions(+), 5 deletions(-)

M CHANGELOG.md
M LICENSE
M README.md
M api.go
M client.go
A files.go
A files_test.go
M objects.go
A test-pattern.jpg
M CHANGELOG.md => CHANGELOG.md +1 -0
@@ 6,6 6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Message object now includes is_deleted and reply_to
- /files endpoint methods

### Fixed
- Updated endpoint for user and post interactions

M LICENSE => LICENSE +1 -1
@@ 1,6 1,6 @@
MIT License

Copyright (c) 2019 Morgan McMillian
Copyright (c) 2020 Morgan McMillian
Copyright (c) 2017 yukkuri_sinai

Permission is hereby granted, free of charge, to any person obtaining a copy

M README.md => README.md +1 -1
@@ 7,5 7,5 @@ Based on peanuts, originally written by yukkuri_sinai, this project aims to crea
### Installation

```sh
go get gitlab.dreamfall.space/thrrgilag/woodstock
go get git.sr.ht/~thrrgilag/woodstock
```

M api.go => api.go +1 -0
@@ 26,6 26,7 @@ const (
	PresenceAPI           = APIBasesURL + "presence"
	ClientAPI             = APIBasesURL + "clients"
	MarkerAPI             = APIBasesURL + "markers"
	FileAPI               = APIBasesURL + "files"
)

// API definition

M client.go => client.go +53 -3
@@ 4,8 4,11 @@ import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"net/url"
	"strconv"
	"strings"


@@ 37,6 40,8 @@ type query struct {
	responseCh chan response
	json       string
	redirect   bool
	reader     io.Reader
	params     map[string]string
}

type response struct {


@@ 111,9 116,52 @@ func decodeResponse(res *http.Response, data interface{}) error {
	return nil
}

func (c *Client) execQuery(url string, form url.Values, data interface{}, method string, jsonStr string, redirect bool) (err error) {
func (c *Client) execQuery(url string, form url.Values, data interface{}, params map[string]string, reader io.Reader, method string, jsonStr string, redirect bool) (err error) {
	var req *http.Request
	if jsonStr == "" {
	if reader != nil {
		body := &bytes.Buffer{}
		writer := multipart.NewWriter(body)
		if params != nil {
			h := make(textproto.MIMEHeader)
			h.Set("Content-Disposition",
				fmt.Sprintf(`form-data; name="%s"; filename="%s";`,
					params["field"], params["name"]))
			h.Set("Content-Type", params["mimetype"])
			delete(params, "field")
			delete(params, "name")
			delete(params, "mimetype")
			fmt.Println(h)
			part, err := writer.CreatePart(h)
			if err != nil {
				return err
			}
			_, err = io.Copy(part, reader)
			for k, v := range params {
				_ = writer.WriteField(k, v)
			}
			err = writer.Close()
			req, err = http.NewRequest(
				method,
				url,
				body,
			)
			req.Header.Set("Content-Type", writer.FormDataContentType())
		} else {
			// TODO: this is for setting the file content but doesn't work
			part, err := writer.CreateFormField("content")
			if err != nil {
				return err
			}
			_, err = io.Copy(part, reader)
			err = writer.Close()
			req, err = http.NewRequest(
				method,
				url,
				body,
			)
			req.Header.Set("Content-Type", writer.FormDataContentType())
		}
	} else if jsonStr == "" {
		req, err = http.NewRequest(
			method,
			url,


@@ 165,13 213,15 @@ func (c *Client) throttledQuery() {
		url := q.url
		form := q.form
		data := q.data
		reader := q.reader
		params := q.params
		method := q.method
		jsonStr := q.json
		redirect := q.redirect

		responseCh := q.responseCh

		err := c.execQuery(url, form, data, method, jsonStr, redirect)
		err := c.execQuery(url, form, data, params, reader, method, jsonStr, redirect)

		responseCh <- response{data, err}
	}

A files.go => files.go +103 -0
@@ 0,0 1,103 @@
package woodstock

import (
	"encoding/json"
	"io"
	"net/url"
	"strings"
)

// FileResult response definition
type FileResult struct {
	*CommonResponse
	Data File `json:"data"`
}

// FilesResult response definition
type FilesResult struct {
	*CommonResponse
	Data []File `json:"data"`
}

// FileDetails object definition
type FileDetails struct {
	Name     string `json:"name"`
	IsPublic bool   `json:"is_public"`
}

// GetFile retrieve a file object
// https://pnut.io/docs/api/resources/files/lookup#get-files-id
func (c *Client) GetFile(id string) (result FileResult, err error) {
	responseCh := make(chan response)
	c.queryQueue <- query{url: FileAPI + "/" + id, data: &result, method: "GET", responseCh: responseCh}
	return result, (<-responseCh).err
}

// GetFiles retrieve multiple file objects
// https://pnut.io/docs/api/resources/files/lookup#get-files
func (c *Client) GetFiles(ids []string) (result FilesResult, err error) {
	v := url.Values{}
	v.Set("ids", strings.Join(ids, ","))
	responseCh := make(chan response)
	c.queryQueue <- query{url: FileAPI + "?" + v.Encode(), data: &result, method: "GET", responseCh: responseCh}
	return result, (<-responseCh).err
}

// GetUserFiles retrieve the authenticated users file objects
// https://pnut.io/docs/api/resources/files/lookup#get-users-me-files
func (c *Client) GetUserFiles() (result FilesResult, err error) {
	responseCh := make(chan response)
	c.queryQueue <- query{url: UserMeAPI + "/files", data: &result, method: "GET", responseCh: responseCh}
	return result, (<-responseCh).err
}

// CreateFile upload a complete file
// https://pnut.io/docs/api/resources/files/lifecycle#post-files
func (c *Client) CreateFile(params map[string]string, reader io.Reader) (result FileResult, err error) {
	// v.Set("field", "content")
	params["field"] = "content"
	responseCh := make(chan response)
	c.queryQueue <- query{url: FileAPI, params: params, reader: reader, data: &result, method: "POST", responseCh: responseCh}
	return result, (<-responseCh).err
}

// CreateFilePlaceholder create a file placeholder
// https://pnut.io/docs/api/resources/files/lifecycle#post-files
func (c *Client) CreateFilePlaceholder(params File) (result FileResult, err error) {
	json, err := json.Marshal(params)
	if err != nil {
		return
	}
	responseCh := make(chan response)
	c.queryQueue <- query{url: FileAPI, data: &result, method: "POST", json: string(json), responseCh: responseCh}
	return result, (<-responseCh).err
}

// TODO: is broken
// UpdateFileContent uploads a file for an existing file placeholder
// https://pnut.io/docs/api/resources/files/lifecycle#put-files-id-content
// func (c *Client) UpdateFileContent(id string, reader io.Reader) (result FileResult, err error) {
// 	responseCh := make(chan response)
// 	c.queryQueue <- query{url: FileAPI + "/" + id + "/content", reader: reader, data: &result, method: "PUT", responseCh: responseCh}
// 	return result, (<-responseCh).err
// }

// UpdateFileDetails updates a files details, only name, is_public, and raw
// https://pnut.io/docs/api/resources/files/lifecycle#put-files-id
func (c *Client) UpdateFileDetails(id string, details FileDetails) (result FileResult, err error) {
	json, err := json.Marshal(details)
	if err != nil {
		return
	}
	responseCh := make(chan response)
	c.queryQueue <- query{url: FileAPI + "/" + id, data: &result, method: "PUT", json: string(json), responseCh: responseCh}
	return result, (<-responseCh).err
}

// DeleteFile delete a file. This will not disassociate a file with any other objects (posts, messages...)
// https://pnut.io/docs/api/resources/files/lifecycle#delete-files-id
func (c *Client) DeleteFile(id string) (result FileResult, err error) {
	responseCh := make(chan response)
	c.queryQueue <- query{url: FileAPI + "/" + id, data: &result, method: "DELETE", responseCh: responseCh}
	return result, (<-responseCh).err
}

A files_test.go => files_test.go +196 -0
@@ 0,0 1,196 @@
package woodstock

import (
	"os"
	"testing"
	"time"
)

func TestGetFile(t *testing.T) {
	config, err := GetConfig()
	if err != nil {
		t.Error(err)
	}
	fileID := "15596"
	fileName := "hawkward.jpg"
	client := NewClient(config.ClientID, "")
	// client.SetAccessToken(config.AccessToken)
	file, err := client.GetFile(fileID)
	if err != nil {
		t.Error(err)
	}
	if file.Data.Name != fileName {
		t.Errorf("File appears incorrect, got: %s, want: %s", file.Data.Name, fileName)
	}
	t.Log(file.Meta)
	time.Sleep(Delay)
}

func TestGetFiles(t *testing.T) {
	config, err := GetConfig()
	if err != nil {
		t.Error(err)
	}
	ids := []string{"15589", "15596", "15597"}
	fileName1 := "test-pattern.jpg"
	fileName2 := "hawkward.jpg"
	fileName3 := "walken.jpg"
	client := NewClient(config.ClientID, "")
	// client.SetAccessToken(config.AccessToken)
	files, err := client.GetFiles(ids)
	if err != nil {
		t.Error(err)
	}
	if files.Data[0].Name != fileName1 {
		t.Errorf("File appears incorrect, got: %s, want: %s", files.Data[0].Name, fileName1)
	}
	if files.Data[1].Name != fileName2 {
		t.Errorf("File appears incorrect, got: %s, want: %s", files.Data[1].Name, fileName2)
	}
	if files.Data[2].Name != fileName3 {
		t.Errorf("File appears incorrect, got: %s, want: %s", files.Data[2].Name, fileName3)
	}
	time.Sleep(Delay)
}

func TestGetUserFiles(t *testing.T) {
	config, err := GetConfig()
	if err != nil {
		t.Error(err)
	}
	client := NewClient(config.ClientID, "")
	client.SetAccessToken(config.AccessToken)
	files, err := client.GetUserFiles()
	if err != nil {
		t.Error(err)
	}
	if len(files.Data) < 3 {
		t.Errorf("I recieved fewer files than expected, got: %d, want at least: %d", len(files.Data), 3)
	}
	time.Sleep(Delay)
}

func TestCreateFile(t *testing.T) {
	config, err := GetConfig()
	if err != nil {
		t.Error(err)
	}
	filename := "test-pattern.jpg"
	filereader, err := os.Open(filename)
	if err != nil {
		t.Error(err)
	}
	defer filereader.Close()
	params := map[string]string{
		"type":      "dev.thrrgilag.woodstock",
		"name":      filename,
		"kind":      "image",
		"mimetype":  "image/jpeg",
		"is_public": "true",
	}
	client := NewClient(config.ClientID, "")
	client.SetAccessToken(config.AccessToken)
	file, err := client.CreateFile(params, filereader)
	if err != nil {
		t.Error(err)
	}
	if file.Data.Name != filename {
		t.Errorf("File appears incorrect, got: %s, want: %s", file.Data.Name, filename)
	}
	time.Sleep(Delay)
}

func TestCreateFilePlaceholder(t *testing.T) {
	config, err := GetConfig()
	if err != nil {
		t.Error(err)
	}
	var params File
	params.Type = "dev.thrrgilag.woodstock"
	params.Name = "test-pattern.jpg"
	params.Kind = "image"
	params.MimeType = "image/jpeg"
	params.IsPublic = true
	client := NewClient(config.ClientID, "")
	client.SetAccessToken(config.AccessToken)
	file, err := client.CreateFilePlaceholder(params)
	if err != nil {
		t.Error(err)
	}
	t.Log(file.Meta)
	time.Sleep(Delay)
}

// TODO: is broken
// func TestUpdateFileContent(t *testing.T) {
// 	config, err := GetConfig()
// 	if err != nil {
// 		t.Error(err)
// 	}
// 	filename := "test-pattern.jpg"
// 	filereader, err := os.Open(filename)
// 	if err != nil {
// 		t.Error(err)
// 	}
// 	defer filereader.Close()
// 	client := NewClient(config.ClientID, "")
// 	client.SetAccessToken(config.AccessToken)
// 	file, err := client.UpdateFileContent("15588", filereader)
// 	if err != nil {
// 		t.Error(err)
// 	}
// 	t.Log(file.Meta)
// 	time.Sleep(Delay)
// }

func TestUpdateFileDetails(t *testing.T) {
	config, err := GetConfig()
	if err != nil {
		t.Error(err)
	}
	fileID := "14987"
	// name := "test image"
	var details FileDetails
	details.Name = "test_image"
	client := NewClient(config.ClientID, "")
	client.SetAccessToken(config.AccessToken)
	file, err := client.UpdateFileDetails(fileID, details)
	if err != nil {
		t.Error(err)
	}
	t.Log(file.Meta)
	time.Sleep(Delay)
	// TODO: change filename string to timestamp that can be checked
}

func TestDeleteFile(t *testing.T) {
	config, err := GetConfig()
	if err != nil {
		t.Error(err)
	}
	filename := "test-pattern.jpg"
	filereader, err := os.Open(filename)
	if err != nil {
		t.Error(err)
	}
	defer filereader.Close()
	params := map[string]string{
		"type":      "dev.thrrgilag.woodstock",
		"name":      filename,
		"kind":      "image",
		"mimetype":  "image/jpeg",
		"is_public": "true",
	}
	client := NewClient(config.ClientID, "")
	client.SetAccessToken(config.AccessToken)
	newfile, err := client.CreateFile(params, filereader)
	if err != nil {
		t.Error(err)
	}
	file, err := client.DeleteFile(newfile.Data.ID)
	if err != nil {
		t.Error(err)
	}
	t.Log(file.Meta)
	time.Sleep(Delay)
}

M objects.go => objects.go +47 -0
@@ 186,6 186,53 @@ type Message struct {
	PaginationID string           `json:"pagination_id"`
}

// AudioInfo object definition
type AudioInfo struct {
	Duration       int    `json:"duration"`
	DurationString string `json:"duration_string"`
	Bitrate        int    `json:"bitrate"`
}

// VideoInfo object definition
type VideoInfo struct {
	Duration       int    `json:"duration"`
	DurationString string `json:"duration_string"`
	Bitrate        int    `json:"bitrate"`
	Height         int    `json:"height"`
	Width          int    `json:"width"`
}

// UploadParameters object definition
type UploadParameters struct {
	Method string `json:"method"`
	Url    string `json:"url"`
}

// File object definition
type File struct {
	AudioInfo        AudioInfo        `json:"audio_info"`
	CreatedAt        string           `json:"created_at"`
	FileToken        string           `json:"file_token"`
	FileTokenRead    string           `json:"file_token_read"`
	ID               string           `json:"id"`
	ImageInfo        Image            `json:"image_info"`
	IsComplete       bool             `json:"is_complete"`
	IsPublic         bool             `json:"is_public"`
	Kind             string           `json:"kind"`
	Link             string           `json:"link"`
	LinkExpiresAt    string           `json:"link_expires_at"`
	LinkShort        string           `json:"link_short"`
	MimeType         string           `json:"mime_type"`
	Name             string           `json:"name"`
	Sha256           string           `json:"sha256"`
	Size             int              `json:"size"`
	Source           Source           `json:"source"`
	Type             string           `json:"type"`
	UploadParameters UploadParameters `json:"upload_parameters"`
	User             User             `json:"user"`
	VideoInfo        VideoInfo        `json:"video_info"`
}

// ContentOfClient object definition
type ContentOfClient struct {
	*ContentOfMessage

A test-pattern.jpg => test-pattern.jpg +0 -0