1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

Update dependencies

This commit is contained in:
Chris Cummer 2018-10-21 12:48:22 -07:00
parent 8f3ae94b4e
commit 3a0bcd21e7
132 changed files with 18033 additions and 9138 deletions

77
Gopkg.lock generated
View File

@ -6,16 +6,16 @@
name = "cloud.google.com/go"
packages = ["compute/metadata"]
pruneopts = "UT"
revision = "64a2037ec6be8a4b0c1d1f706ed35b428b989239"
version = "v0.26.0"
revision = "dfffe386c33fb24c34ee501e5723df5b97b98514"
version = "v0.30.0"
[[projects]]
branch = "master"
digest = "1:1164f5eccc8905c03a2c688141c81f06522ed5fd2eb65cb43debeb730a018276"
digest = "1:636ac9f696c988f0038afd43592f7a0fff29038588ab1064ba8ba3476bb41091"
name = "github.com/adlio/trello"
packages = ["."]
pruneopts = "UT"
revision = "8a458717123e328d9103a3bf075e64bc1ec961f8"
revision = "e4cc07c871d0ee17c5579131fc1c786efbb5fe2a"
[[projects]]
digest = "1:9253f97cfbbe049b631877c80badecc69620711b3e335f6cf97a7809681da388"
@ -59,11 +59,11 @@
[[projects]]
branch = "master"
digest = "1:b5d1ba5dc01e8129b7df5cf95380a66b0e0f122850189b7ec255953b15522f16"
digest = "1:579829cc9bf515e2f9e39f91df03b9333c311402bac5a4935dc8ef81e3a57160"
name = "github.com/andygrunwald/go-gerrit"
packages = ["."]
pruneopts = "UT"
revision = "197fe0d2e796b3d985b9cd91be8afd4415692f39"
revision = "30ce279197661497040ec81c4b64539562eb2d4b"
[[projects]]
branch = "master"
@ -117,12 +117,12 @@
version = "v1.1.6"
[[projects]]
branch = "master"
digest = "1:6f9339c912bbdda81302633ad7e99a28dfa5a639c864061f1929510a9a64aa74"
name = "github.com/dustin/go-humanize"
packages = ["."]
pruneopts = "UT"
revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e"
version = "v1.0.0"
[[projects]]
branch = "master"
@ -134,14 +134,14 @@
[[projects]]
branch = "master"
digest = "1:00b9cce210566117aff926677c005aeaea6c85374e67bdcb72783af237c48f97"
digest = "1:490cf9d7deec1b0dcac6cc8b17f307c48c4821bc2140d46e48175a83acbfe40d"
name = "github.com/gdamore/tcell"
packages = [
".",
"terminfo",
]
pruneopts = "UT"
revision = "de7e78efa4a71b3f36c7154989c529dbdf9ae623"
revision = "493f3b46b3c20880afc8e04ceeb1c6d5aa3363d7"
[[projects]]
digest = "1:57fa4c058c21ce25d0b7272518dd746065117abf6cc706158b0d361202024520"
@ -152,28 +152,28 @@
version = "v4.1.0"
[[projects]]
digest = "1:15042ad3498153684d09f393bbaec6b216c8eec6d61f63dff711de7d64ed8861"
digest = "1:97df918963298c287643883209a2c3f642e6593379f97ab400c2a2e219ab647d"
name = "github.com/golang/protobuf"
packages = ["proto"]
pruneopts = "UT"
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:4be3c01ef56542da4f1268c52a8b88bae5309ec3dade7755e250bc933e13ac70"
digest = "1:8c90219e8a1f5f7cad019b77697aaae2b236db5a1a46ae688e5ede406e109182"
name = "github.com/google/go-github"
packages = ["github"]
pruneopts = "UT"
revision = "d7732128a00e8e95e8fe896017da18ee20b2180d"
revision = "68a79fc6a32bab9406083545e667a65ba67b0a3e"
[[projects]]
branch = "master"
digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690"
name = "github.com/google/go-querystring"
packages = ["query"]
pruneopts = "UT"
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
revision = "44c6ddd0a2342c386950e880b658017258da92fc"
version = "v1.0.0"
[[projects]]
digest = "1:a2cff208d4759f6ba1b1cd228587b0a1869f95f22542ec9cd17fff64430113c7"
@ -201,11 +201,11 @@
[[projects]]
branch = "master"
digest = "1:3a16300d913e1050d4a7ff296106a11b72da05c5dd3475f2ae1feb0830866461"
digest = "1:2277d06c9dcd34d70119b624115f3d74672647322349bb76a8adedbc623a3955"
name = "github.com/olebedev/config"
packages = ["."]
pruneopts = "UT"
revision = "ed90d2035b8114c30b9cb65e7d52e10a7148f8c6"
revision = "57f804269e64d41bfe46efb76232c47f49d40902"
[[projects]]
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
@ -241,19 +241,19 @@
[[projects]]
branch = "master"
digest = "1:9deb696e9739b45e91369ffb3876d4e0d2bc45e0d35062fb3c5aa44fa77eff6c"
digest = "1:00d9f4017b55d590139b77fbd851aef56f179479fc40a4afdedaf187e828f99d"
name = "github.com/rivo/tview"
packages = ["."]
pruneopts = "UT"
revision = "21f50f5bc400083b4eb23304887d9cd0fc00d075"
revision = "a7c1880d62d37422830f0a15823b51301ca9f354"
[[projects]]
branch = "master"
digest = "1:de0cf35afb9cad7b20ff8871ae3b64cecc7116bffb583bc7b3c1bb5c37473149"
digest = "1:93ff598337cb35c40079a1ba766729c20eb1756e37f5740eac4c8e943f59664d"
name = "github.com/sticreations/spotigopher"
packages = ["spotigopher"]
pruneopts = "UT"
revision = "995ee350dc1c54259597de65dde7a9e56a4b73af"
revision = "98632f6f94b087f2582be76980f76bc070d37176"
[[projects]]
digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83"
@ -265,11 +265,11 @@
[[projects]]
branch = "master"
digest = "1:ca067f3d378064d50a239b1f0d4dcd2dab6356f7643993bdf02c91d6e621c028"
digest = "1:afc7e1c726a88e6cd5689cca19fbc86128dffe86fd37e3e0841767f3a951182e"
name = "github.com/xanzy/go-gitlab"
packages = ["."]
pruneopts = "UT"
revision = "f3bc634ab936f7e4ee5e21334ccfdfeb5601d477"
revision = "1444249c1b2a8e4cdb5a76dc9c8d02d1133180be"
[[projects]]
branch = "master"
@ -281,11 +281,19 @@
[[projects]]
branch = "master"
digest = "1:f6f2814b88ad3dd5ca45e1e9a7a0caba1ddb1bfaf1c4173ebd991d854e1669eb"
digest = "1:1ff6915a45fb06d272e614e72219aa058aefc545f597a708eb0cc6cb778c344a"
name = "github.com/zmb3/spotify"
packages = ["."]
pruneopts = "UT"
revision = "a4bd83f60e06ab58f3516c61680d0532aabf470e"
[[projects]]
branch = "master"
digest = "1:02a2f8f0718f239866000d2d1bca016d14f0f6de37615c315f34da17dc630259"
name = "github.com/zorkian/go-datadog-api"
packages = ["."]
pruneopts = "UT"
revision = "d7b8b10db6a7eb1c1c2424b10a795a1662e80c9a"
revision = "dc324c09cf05eef3e3a82bde06ae0c4dd349a767"
[[projects]]
branch = "master"
@ -296,11 +304,11 @@
"context/ctxhttp",
]
pruneopts = "UT"
revision = "aaf60122140d3fcf75376d319f0554393160eb50"
revision = "04a2e542c03f1d053ab3e4d6e5abcd4b66e2be8e"
[[projects]]
branch = "master"
digest = "1:bea0314c10bd362ab623af4880d853b5bad3b63d0ab9945c47e461b8d04203ed"
digest = "1:faa25cb78cf9c8cec9345d4ed07322cdef6a8c968b3d0a6b6c3609067c7386eb"
name = "golang.org/x/oauth2"
packages = [
".",
@ -310,7 +318,7 @@
"jwt",
]
pruneopts = "UT"
revision = "3d292e4d0cdc3a0113e6d207bb137145ef1de42f"
revision = "9dcd33a902f40452422c2367fefcb95b54f9f8f8"
[[projects]]
digest = "1:37672ad5821719e2df8509c2edd4ba5ae192463237c73c3a2d24ef8b2bc9e36f"
@ -328,7 +336,7 @@
[[projects]]
branch = "master"
digest = "1:c8d95fbc783cab4e8986b1bf3dbe5312db7521e4342ac755280cfa78ce0ee792"
digest = "1:a1d58b7c9eeceeac201a6f6b10092cd2a5985791a45843c838151ee3f751d3c3"
name = "google.golang.org/api"
packages = [
"calendar/v3",
@ -338,10 +346,10 @@
"sheets/v4",
]
pruneopts = "UT"
revision = "e21acd801f91da814261b938941d193bb036441a"
revision = "a2651947f503a1793446d4058bb073a6fdf99e53"
[[projects]]
digest = "1:c8907869850adaa8bd7631887948d0684f3787d0912f1c01ab72581a6c34432e"
digest = "1:193950893ea275f89ed92e5da11ed8fa1436872f755a9ea5d4afa83dc9d9c3a8"
name = "google.golang.org/appengine"
packages = [
".",
@ -356,8 +364,8 @@
"urlfetch",
]
pruneopts = "UT"
revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
version = "v1.1.0"
revision = "ae0ab99deb4dc413a2b4bd6c8bdd0eb67f1e4d06"
version = "v1.2.0"
[[projects]]
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
@ -390,6 +398,7 @@
"github.com/stretchr/testify/assert",
"github.com/xanzy/go-gitlab",
"github.com/yfronto/newrelic",
"github.com/zmb3/spotify",
"github.com/zorkian/go-datadog-api",
"golang.org/x/oauth2",
"golang.org/x/oauth2/google",

View File

@ -23,10 +23,10 @@ type Action struct {
type ActionData struct {
Text string `json:"text,omitempty"`
List *List `json:"list,omitempty"`
Card *Card `json:"card,omitempty"`
CardSource *Card `json:"cardSource,omitempty"`
Card *ActionDataCard `json:"card,omitempty"`
CardSource *ActionDataCard `json:"cardSource,omitempty"`
Board *Board `json:"board,omitempty"`
Old *Card `json:"old,omitempty"`
Old *ActionDataCard `json:"old,omitempty"`
ListBefore *List `json:"listBefore,omitempty"`
ListAfter *List `json:"listAfter,omitempty"`
DateLastEdited time.Time `json:"dateLastEdited"`
@ -35,6 +35,15 @@ type ActionData struct {
Checklist *Checklist `json:"checklist"`
}
type ActionDataCard struct {
ID string `json:"id"`
Name string `json:"name"`
IDShort int `json:"idShort"`
ShortLink string `json:"shortLink"`
Pos float64 `json:"pos"`
Closed bool `json:"closed"`
}
func (b *Board) GetActions(args Arguments) (actions ActionCollection, err error) {
path := fmt.Sprintf("boards/%s/actions", b.ID)
err = b.client.Get(path, args, &actions)

View File

@ -54,6 +54,7 @@ type Board struct {
} `json:"labelNames"`
Lists []*List `json:"lists"`
Actions []*Action `json:"actions"`
Organization Organization `json:"organization"`
}
type BackgroundImage struct {

View File

@ -76,6 +76,11 @@ type Card struct {
// Labels
IDLabels []string `json:"idLabels,omitempty"`
Labels []*Label `json:"labels,omitempty"`
// Custom Fields
CustomFieldItems []*CustomFieldItem `json:"customFieldItems",omitempty`
customFieldMap *map[string]interface{}
}
func (c *Card) CreatedAt() time.Time {
@ -83,6 +88,52 @@ func (c *Card) CreatedAt() time.Time {
return t
}
func (c *Card) CustomFields(boardCustomFields []*CustomField) (map[string]interface{}) {
cfm := c.customFieldMap
if cfm == nil {
cfm = &(map[string]interface{} {})
// bcfOptionNames[CustomField ID] = Custom Field Name
bcfOptionNames := map[string]string{}
// bcfOptionsMap[CustomField ID][ID of the option] = Value of the option
bcfOptionsMap := map[string] map[string]interface{}{}
for _, bcf := range boardCustomFields {
bcfOptionNames[bcf.ID] = bcf.Name
for _, cf := range bcf.Options {
// create 2nd level map when not available yet
map2, ok := bcfOptionsMap[cf.IDCustomField]
if !ok {
map2 = map[string]interface{}{}
bcfOptionsMap[bcf.ID] = map2
}
bcfOptionsMap[bcf.ID][cf.ID] = cf.Value.Text
}
}
for _, cf := range c.CustomFieldItems {
name := bcfOptionNames[cf.IDCustomField]
// create 2nd level map when not available yet
map2, ok := bcfOptionsMap[cf.IDCustomField]
if !ok {
continue
}
value, ok := map2[cf.IDValue]
if ok {
(*cfm)[name] = value
}
}
c.customFieldMap = cfm
}
return *cfm
}
func (c *Card) MoveToList(listID string, args Arguments) error {
path := fmt.Sprintf("cards/%s", c.ID)
args["idList"] = listID
@ -105,6 +156,18 @@ func (c *Card) AddMemberID(memberID string) (member []*Member, err error) {
return member, err
}
func (c *Card) RemoveIDLabel(labelID string, label *Label) error {
path := fmt.Sprintf("cards/%s/idLabels/%s", c.ID, labelID)
return c.client.Delete(path, Defaults(), label)
}
func (c *Card) AddIDLabel(labelID string) error {
path := fmt.Sprintf("cards/%s/idLabels", c.ID)
err := c.client.Post(path, Arguments{"value": labelID}, &c.IDLabels)
return err
}
func (c *Card) MoveToTopOfList() error {
path := fmt.Sprintf("cards/%s", c.ID)
return c.client.Put(path, Arguments{"pos": "top"}, c)

View File

@ -234,6 +234,6 @@ func (c *Client) Delete(path string, args Arguments, target interface{}) error {
func (c *Client) log(format string, args ...interface{}) {
if c.Logger != nil {
c.Logger.Debugf(format, args)
c.Logger.Debugf(format, args...)
}
}

49
vendor/github.com/adlio/trello/custom-fields.go generated vendored Normal file
View File

@ -0,0 +1,49 @@
package trello
import "fmt"
type CustomFieldItem struct {
ID string `json:"id"`
IDValue string `json:"idValue"`
IDCustomField string `json:"idCustomField"`
IDModel string `json:"idModel"`
IDModelType string `json:"modelType,omitempty"`
}
type CustomField struct {
ID string `json:"id"`
IDModel string `json:"idModel"`
IDModelType string `json:"modelType,omitempty"`
FieldGroup string `json:"fieldGroup"`
Name string `json:"name"`
Pos int `json:"pos"`
Display struct {
CardFront bool `json:"cardfront"`
} `json:"display"`
Type string `json:"type"`
Options []*CustomFieldOption `json:"options"`
}
type CustomFieldOption struct {
ID string `json:"id"`
IDCustomField string `json:"idCustomField"`
Value struct {
Text string `json:"text"`
} `json:"value"`
Color string `json:"color,omitempty"`
Pos int `json:"pos"`
}
func (c *Client) GetCustomField(fieldID string, args Arguments) (customField *CustomField, err error) {
path := fmt.Sprintf("customFields/%s", fieldID)
err = c.Get(path, args, &customField)
return
}
func (b *Board) GetCustomFields(args Arguments) (customFields []*CustomField, err error) {
path := fmt.Sprintf("boards/%s/customFields", b.ID)
err = b.client.Get(path, args, &customFields)
return
}

View File

@ -3,6 +3,7 @@ language: go
sudo: false
go:
- "1.11.x"
- "1.10.x"
- "1.9.x"
- "1.8.x"

View File

@ -1,7 +1,7 @@
package gerrit
import (
"crypto/md5" // nolint: gas
"crypto/md5" // nolint: gosec
"crypto/rand"
"encoding/base64"
"errors"
@ -117,7 +117,7 @@ func (s *AuthenticationService) digestAuthHeader(response *http.Response) (strin
uriHeader := authenticate["uri"]
// A1
h := md5.New() // nolint: gas
h := md5.New() // nolint: gosec
A1 := fmt.Sprintf("%s:%s:%s", s.name, realmHeader, s.secret)
if _, err := io.WriteString(h, A1); err != nil {
return "", err
@ -125,7 +125,7 @@ func (s *AuthenticationService) digestAuthHeader(response *http.Response) (strin
HA1 := fmt.Sprintf("%x", h.Sum(nil))
// A2
h = md5.New() // nolint: gas
h = md5.New() // nolint: gosec
A2 := fmt.Sprintf("%s:%s", response.Request.Method, uriHeader)
if _, err := io.WriteString(h, A2); err != nil {
return "", err
@ -141,7 +141,7 @@ func (s *AuthenticationService) digestAuthHeader(response *http.Response) (strin
bytes += n
}
cnonce := base64.StdEncoding.EncodeToString(k)
digest := md5.New() // nolint: gas
digest := md5.New() // nolint: gosec
if _, err := digest.Write([]byte(strings.Join([]string{HA1, nonceHeader, "00000001", cnonce, qopHeader, HA2}, ":"))); err != nil {
return "", err
}

View File

@ -258,11 +258,11 @@ type RobotCommentInput struct {
CommentInput
// The ID of the robot that generated this comment.
RobotId string `json:"robot_id"`
RobotID string `json:"robot_id"`
// An ID of the run of the robot.
RobotRunId string `json:"robot_run_id"`
RobotRunID string `json:"robot_run_id"`
// URL to more information.
Url string `json:"url,omitempty"`
URL string `json:"url,omitempty"`
// Robot specific properties as map that maps arbitrary keys to values.
Properties *map[string]*string `json:"properties,omitempty"`
// Suggested fixes for this robot comment as a list of FixSuggestionInfo
@ -277,11 +277,11 @@ type RobotCommentInfo struct {
CommentInfo
// The ID of the robot that generated this comment.
RobotId string `json:"robot_id"`
RobotID string `json:"robot_id"`
// An ID of the run of the robot.
RobotRunId string `json:"robot_run_id"`
RobotRunID string `json:"robot_run_id"`
// URL to more information.
Url string `json:"url,omitempty"`
URL string `json:"url,omitempty"`
// Robot specific properties as map that maps arbitrary keys to values.
Properties map[string]string `json:"properties,omitempty"`
// Suggested fixes for this robot comment as a list of FixSuggestionInfo
@ -294,7 +294,7 @@ type RobotCommentInfo struct {
type FixSuggestionInfo struct {
// The UUID of the suggested fix. It will be generated automatically and
// hence will be ignored if its set for input objects.
FixId string `json:"fix_id"`
FixID string `json:"fix_id"`
// A description of the suggested fix.
Description string `json:"description"`
// A list of FixReplacementInfo entities indicating how the content of one or
@ -780,12 +780,18 @@ func (s *ChangesService) change(tail string, changeID string, input interface{})
v := new(ChangeInfo)
resp, err := s.client.Do(req, v)
if resp.StatusCode == http.StatusConflict {
body, _ := ioutil.ReadAll(resp.Body)
err = errors.New(string(body[:]))
if err != nil {
return nil, resp, err
}
if resp.StatusCode == http.StatusConflict {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return v, resp, err
}
return v, resp, errors.New(string(body[:]))
}
return v, resp, nil
}
// SubmitChange submits a change.
//

View File

@ -25,6 +25,7 @@ ways. It also adds substantial functionality beyond termbox.
* [tui-go](https://github.com/marcusolsson/tui-go) - UI library for terminal apps
* [gomandelbrot](https://github.com/rgm3/gomandelbrot) - Mandelbrot!
* [WTF](https://github.com/senorprogrammer/wtf)- Personal information dashboard for your terminal
* [browsh](https://github.com/browsh-org/browsh) - A fully-modern text-based browser, rendering to TTY and browsers ([video](https://www.youtube.com/watch?v=HZq86XfBoRo))
## Pure Go Terminfo Database

View File

@ -52,6 +52,10 @@ func (cb *CellBuffer) SetContent(x int, y int,
i := 0
for i < len(c.currComb) {
r := c.currComb[i]
if r == '\u200d' {
i += 2
continue
}
if runewidth.RuneWidth(r) != 0 {
// not a combining character, yank it
c.currComb = append(c.currComb[:i-1], c.currComb[i+1:]...)
@ -175,12 +179,13 @@ func (cb *CellBuffer) Resize(w, h int) {
// Fill fills the entire cell buffer array with the specified character
// and style. Normally choose ' ' to clear the screen. This API doesn't
// support combining characters.
// support combining characters, or characters with a width larger than one.
func (cb *CellBuffer) Fill(r rune, style Style) {
for i := range cb.cells {
c := &cb.cells[i]
c.currMain = r
c.currComb = nil
c.currStyle = style
c.width = 1
}
}

View File

@ -1,7 +1,4 @@
Go support for Protocol Buffers - Google's data interchange format
Copyright 2010 The Go Authors. All rights reserved.
https://github.com/golang/protobuf
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are

View File

@ -37,27 +37,9 @@ package proto
import (
"errors"
"fmt"
"reflect"
)
// RequiredNotSetError is the error returned if Marshal is called with
// a protocol buffer struct whose required fields have not
// all been initialized. It is also the error returned if Unmarshal is
// called with an encoded protocol buffer that does not include all the
// required fields.
//
// When printed, RequiredNotSetError reports the first unset required field in a
// message. If the field cannot be precisely determined, it is reported as
// "{Unknown}".
type RequiredNotSetError struct {
field string
}
func (e *RequiredNotSetError) Error() string {
return fmt.Sprintf("proto: required field %q not set", e.field)
}
var (
// errRepeatedHasNil is the error returned if Marshal is called with
// a struct with a repeated field containing a nil element.

View File

@ -265,7 +265,6 @@ package proto
import (
"encoding/json"
"errors"
"fmt"
"log"
"reflect"
@ -274,7 +273,66 @@ import (
"sync"
)
var errInvalidUTF8 = errors.New("proto: invalid UTF-8 string")
// RequiredNotSetError is an error type returned by either Marshal or Unmarshal.
// Marshal reports this when a required field is not initialized.
// Unmarshal reports this when a required field is missing from the wire data.
type RequiredNotSetError struct{ field string }
func (e *RequiredNotSetError) Error() string {
if e.field == "" {
return fmt.Sprintf("proto: required field not set")
}
return fmt.Sprintf("proto: required field %q not set", e.field)
}
func (e *RequiredNotSetError) RequiredNotSet() bool {
return true
}
type invalidUTF8Error struct{ field string }
func (e *invalidUTF8Error) Error() string {
if e.field == "" {
return "proto: invalid UTF-8 detected"
}
return fmt.Sprintf("proto: field %q contains invalid UTF-8", e.field)
}
func (e *invalidUTF8Error) InvalidUTF8() bool {
return true
}
// errInvalidUTF8 is a sentinel error to identify fields with invalid UTF-8.
// This error should not be exposed to the external API as such errors should
// be recreated with the field information.
var errInvalidUTF8 = &invalidUTF8Error{}
// isNonFatal reports whether the error is either a RequiredNotSet error
// or a InvalidUTF8 error.
func isNonFatal(err error) bool {
if re, ok := err.(interface{ RequiredNotSet() bool }); ok && re.RequiredNotSet() {
return true
}
if re, ok := err.(interface{ InvalidUTF8() bool }); ok && re.InvalidUTF8() {
return true
}
return false
}
type nonFatal struct{ E error }
// Merge merges err into nf and reports whether it was successful.
// Otherwise it returns false for any fatal non-nil errors.
func (nf *nonFatal) Merge(err error) (ok bool) {
if err == nil {
return true // not an error
}
if !isNonFatal(err) {
return false // fatal error
}
if nf.E == nil {
nf.E = err // store first instance of non-fatal error
}
return true
}
// Message is implemented by generated protocol buffer messages.
type Message interface {

View File

@ -139,7 +139,7 @@ type Properties struct {
Repeated bool
Packed bool // relevant for repeated primitives only
Enum string // set for enum types only
proto3 bool // whether this is known to be a proto3 field; set for []byte only
proto3 bool // whether this is known to be a proto3 field
oneof bool // whether this is a oneof field
Default string // default value
@ -149,8 +149,8 @@ type Properties struct {
sprop *StructProperties // set for struct types only
mtype reflect.Type // set for map types only
mkeyprop *Properties // set for map types only
mvalprop *Properties // set for map types only
MapKeyProp *Properties // set for map types only
MapValProp *Properties // set for map types only
}
// String formats the properties in the protobuf struct field tag style.
@ -275,16 +275,16 @@ func (p *Properties) setFieldProps(typ reflect.Type, f *reflect.StructField, loc
case reflect.Map:
p.mtype = t1
p.mkeyprop = &Properties{}
p.mkeyprop.init(reflect.PtrTo(p.mtype.Key()), "Key", f.Tag.Get("protobuf_key"), nil, lockGetProp)
p.mvalprop = &Properties{}
p.MapKeyProp = &Properties{}
p.MapKeyProp.init(reflect.PtrTo(p.mtype.Key()), "Key", f.Tag.Get("protobuf_key"), nil, lockGetProp)
p.MapValProp = &Properties{}
vtype := p.mtype.Elem()
if vtype.Kind() != reflect.Ptr && vtype.Kind() != reflect.Slice {
// The value type is not a message (*T) or bytes ([]byte),
// so we need encoders for the pointer to this type.
vtype = reflect.PtrTo(vtype)
}
p.mvalprop.init(vtype, "Value", f.Tag.Get("protobuf_val"), nil, lockGetProp)
p.MapValProp.init(vtype, "Value", f.Tag.Get("protobuf_val"), nil, lockGetProp)
}
if p.stype != nil {

View File

@ -231,7 +231,7 @@ func (u *marshalInfo) marshal(b []byte, ptr pointer, deterministic bool) ([]byte
return b, err
}
var err, errreq error
var err, errLater error
// The old marshaler encodes extensions at beginning.
if u.extensions.IsValid() {
e := ptr.offset(u.extensions).toExtensions()
@ -252,11 +252,13 @@ func (u *marshalInfo) marshal(b []byte, ptr pointer, deterministic bool) ([]byte
}
}
for _, f := range u.fields {
if f.required && errreq == nil {
if f.required {
if ptr.offset(f.field).getPointer().isNil() {
// Required field is not set.
// We record the error but keep going, to give a complete marshaling.
errreq = &RequiredNotSetError{f.name}
if errLater == nil {
errLater = &RequiredNotSetError{f.name}
}
continue
}
}
@ -269,14 +271,21 @@ func (u *marshalInfo) marshal(b []byte, ptr pointer, deterministic bool) ([]byte
if err1, ok := err.(*RequiredNotSetError); ok {
// Required field in submessage is not set.
// We record the error but keep going, to give a complete marshaling.
if errreq == nil {
errreq = &RequiredNotSetError{f.name + "." + err1.field}
if errLater == nil {
errLater = &RequiredNotSetError{f.name + "." + err1.field}
}
continue
}
if err == errRepeatedHasNil {
err = errors.New("proto: repeated field " + f.name + " has nil element")
}
if err == errInvalidUTF8 {
if errLater == nil {
fullName := revProtoTypes[reflect.PtrTo(u.typ)] + "." + f.name
errLater = &invalidUTF8Error{fullName}
}
continue
}
return b, err
}
}
@ -284,7 +293,7 @@ func (u *marshalInfo) marshal(b []byte, ptr pointer, deterministic bool) ([]byte
s := *ptr.offset(u.unrecognized).toBytes()
b = append(b, s...)
}
return b, errreq
return b, errLater
}
// computeMarshalInfo initializes the marshal info.
@ -530,6 +539,7 @@ func typeMarshaler(t reflect.Type, tags []string, nozero, oneof bool) (sizer, ma
packed := false
proto3 := false
validateUTF8 := true
for i := 2; i < len(tags); i++ {
if tags[i] == "packed" {
packed = true
@ -538,6 +548,7 @@ func typeMarshaler(t reflect.Type, tags []string, nozero, oneof bool) (sizer, ma
proto3 = true
}
}
validateUTF8 = validateUTF8 && proto3
switch t.Kind() {
case reflect.Bool:
@ -735,6 +746,18 @@ func typeMarshaler(t reflect.Type, tags []string, nozero, oneof bool) (sizer, ma
}
return sizeFloat64Value, appendFloat64Value
case reflect.String:
if validateUTF8 {
if pointer {
return sizeStringPtr, appendUTF8StringPtr
}
if slice {
return sizeStringSlice, appendUTF8StringSlice
}
if nozero {
return sizeStringValueNoZero, appendUTF8StringValueNoZero
}
return sizeStringValue, appendUTF8StringValue
}
if pointer {
return sizeStringPtr, appendStringPtr
}
@ -1984,9 +2007,6 @@ func appendBoolPackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byt
}
func appendStringValue(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) {
v := *ptr.toString()
if !utf8.ValidString(v) {
return nil, errInvalidUTF8
}
b = appendVarint(b, wiretag)
b = appendVarint(b, uint64(len(v)))
b = append(b, v...)
@ -1997,9 +2017,6 @@ func appendStringValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]b
if v == "" {
return b, nil
}
if !utf8.ValidString(v) {
return nil, errInvalidUTF8
}
b = appendVarint(b, wiretag)
b = appendVarint(b, uint64(len(v)))
b = append(b, v...)
@ -2011,9 +2028,6 @@ func appendStringPtr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, err
return b, nil
}
v := *p
if !utf8.ValidString(v) {
return nil, errInvalidUTF8
}
b = appendVarint(b, wiretag)
b = appendVarint(b, uint64(len(v)))
b = append(b, v...)
@ -2022,12 +2036,74 @@ func appendStringPtr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, err
func appendStringSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) {
s := *ptr.toStringSlice()
for _, v := range s {
b = appendVarint(b, wiretag)
b = appendVarint(b, uint64(len(v)))
b = append(b, v...)
}
return b, nil
}
func appendUTF8StringValue(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) {
var invalidUTF8 bool
v := *ptr.toString()
if !utf8.ValidString(v) {
return nil, errInvalidUTF8
invalidUTF8 = true
}
b = appendVarint(b, wiretag)
b = appendVarint(b, uint64(len(v)))
b = append(b, v...)
if invalidUTF8 {
return b, errInvalidUTF8
}
return b, nil
}
func appendUTF8StringValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) {
var invalidUTF8 bool
v := *ptr.toString()
if v == "" {
return b, nil
}
if !utf8.ValidString(v) {
invalidUTF8 = true
}
b = appendVarint(b, wiretag)
b = appendVarint(b, uint64(len(v)))
b = append(b, v...)
if invalidUTF8 {
return b, errInvalidUTF8
}
return b, nil
}
func appendUTF8StringPtr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) {
var invalidUTF8 bool
p := *ptr.toStringPtr()
if p == nil {
return b, nil
}
v := *p
if !utf8.ValidString(v) {
invalidUTF8 = true
}
b = appendVarint(b, wiretag)
b = appendVarint(b, uint64(len(v)))
b = append(b, v...)
if invalidUTF8 {
return b, errInvalidUTF8
}
return b, nil
}
func appendUTF8StringSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) {
var invalidUTF8 bool
s := *ptr.toStringSlice()
for _, v := range s {
if !utf8.ValidString(v) {
invalidUTF8 = true
}
b = appendVarint(b, wiretag)
b = appendVarint(b, uint64(len(v)))
b = append(b, v...)
}
if invalidUTF8 {
return b, errInvalidUTF8
}
return b, nil
}
@ -2107,7 +2183,8 @@ func makeGroupSliceMarshaler(u *marshalInfo) (sizer, marshaler) {
},
func(b []byte, ptr pointer, wiretag uint64, deterministic bool) ([]byte, error) {
s := ptr.getPointerSlice()
var err, errreq error
var err error
var nerr nonFatal
for _, v := range s {
if v.isNil() {
return b, errRepeatedHasNil
@ -2115,22 +2192,14 @@ func makeGroupSliceMarshaler(u *marshalInfo) (sizer, marshaler) {
b = appendVarint(b, wiretag) // start group
b, err = u.marshal(b, v, deterministic)
b = appendVarint(b, wiretag+(WireEndGroup-WireStartGroup)) // end group
if err != nil {
if _, ok := err.(*RequiredNotSetError); ok {
// Required field in submessage is not set.
// We record the error but keep going, to give a complete marshaling.
if errreq == nil {
errreq = err
}
continue
}
if !nerr.Merge(err) {
if err == ErrNil {
err = errRepeatedHasNil
}
return b, err
}
}
return b, errreq
return b, nerr.E
}
}
@ -2174,7 +2243,8 @@ func makeMessageSliceMarshaler(u *marshalInfo) (sizer, marshaler) {
},
func(b []byte, ptr pointer, wiretag uint64, deterministic bool) ([]byte, error) {
s := ptr.getPointerSlice()
var err, errreq error
var err error
var nerr nonFatal
for _, v := range s {
if v.isNil() {
return b, errRepeatedHasNil
@ -2184,22 +2254,14 @@ func makeMessageSliceMarshaler(u *marshalInfo) (sizer, marshaler) {
b = appendVarint(b, uint64(siz))
b, err = u.marshal(b, v, deterministic)
if err != nil {
if _, ok := err.(*RequiredNotSetError); ok {
// Required field in submessage is not set.
// We record the error but keep going, to give a complete marshaling.
if errreq == nil {
errreq = err
}
continue
}
if !nerr.Merge(err) {
if err == ErrNil {
err = errRepeatedHasNil
}
return b, err
}
}
return b, errreq
return b, nerr.E
}
}
@ -2223,6 +2285,25 @@ func makeMapMarshaler(f *reflect.StructField) (sizer, marshaler) {
// value.
// Key cannot be pointer-typed.
valIsPtr := valType.Kind() == reflect.Ptr
// If value is a message with nested maps, calling
// valSizer in marshal may be quadratic. We should use
// cached version in marshal (but not in size).
// If value is not message type, we don't have size cache,
// but it cannot be nested either. Just use valSizer.
valCachedSizer := valSizer
if valIsPtr && valType.Elem().Kind() == reflect.Struct {
u := getMarshalInfo(valType.Elem())
valCachedSizer = func(ptr pointer, tagsize int) int {
// Same as message sizer, but use cache.
p := ptr.getPointer()
if p.isNil() {
return 0
}
siz := u.cachedsize(p)
return siz + SizeVarint(uint64(siz)) + tagsize
}
}
return func(ptr pointer, tagsize int) int {
m := ptr.asPointerTo(t).Elem() // the map
n := 0
@ -2243,24 +2324,26 @@ func makeMapMarshaler(f *reflect.StructField) (sizer, marshaler) {
if len(keys) > 1 && deterministic {
sort.Sort(mapKeys(keys))
}
var nerr nonFatal
for _, k := range keys {
ki := k.Interface()
vi := m.MapIndex(k).Interface()
kaddr := toAddrPointer(&ki, false) // pointer to key
vaddr := toAddrPointer(&vi, valIsPtr) // pointer to value
b = appendVarint(b, tag)
siz := keySizer(kaddr, 1) + valSizer(vaddr, 1) // tag of key = 1 (size=1), tag of val = 2 (size=1)
siz := keySizer(kaddr, 1) + valCachedSizer(vaddr, 1) // tag of key = 1 (size=1), tag of val = 2 (size=1)
b = appendVarint(b, uint64(siz))
b, err = keyMarshaler(b, kaddr, keyWireTag, deterministic)
if err != nil {
if !nerr.Merge(err) {
return b, err
}
b, err = valMarshaler(b, vaddr, valWireTag, deterministic)
if err != nil && err != ErrNil { // allow nil value in map
if err != ErrNil && !nerr.Merge(err) { // allow nil value in map
return b, err
}
}
return b, nil
return b, nerr.E
}
}
@ -2333,6 +2416,7 @@ func (u *marshalInfo) appendExtensions(b []byte, ext *XXX_InternalExtensions, de
defer mu.Unlock()
var err error
var nerr nonFatal
// Fast-path for common cases: zero or one extensions.
// Don't bother sorting the keys.
@ -2352,11 +2436,11 @@ func (u *marshalInfo) appendExtensions(b []byte, ext *XXX_InternalExtensions, de
v := e.value
p := toAddrPointer(&v, ei.isptr)
b, err = ei.marshaler(b, p, ei.wiretag, deterministic)
if err != nil {
if !nerr.Merge(err) {
return b, err
}
}
return b, nil
return b, nerr.E
}
// Sort the keys to provide a deterministic encoding.
@ -2383,11 +2467,11 @@ func (u *marshalInfo) appendExtensions(b []byte, ext *XXX_InternalExtensions, de
v := e.value
p := toAddrPointer(&v, ei.isptr)
b, err = ei.marshaler(b, p, ei.wiretag, deterministic)
if err != nil {
if !nerr.Merge(err) {
return b, err
}
}
return b, nil
return b, nerr.E
}
// message set format is:
@ -2444,6 +2528,7 @@ func (u *marshalInfo) appendMessageSet(b []byte, ext *XXX_InternalExtensions, de
defer mu.Unlock()
var err error
var nerr nonFatal
// Fast-path for common cases: zero or one extensions.
// Don't bother sorting the keys.
@ -2470,12 +2555,12 @@ func (u *marshalInfo) appendMessageSet(b []byte, ext *XXX_InternalExtensions, de
v := e.value
p := toAddrPointer(&v, ei.isptr)
b, err = ei.marshaler(b, p, 3<<3|WireBytes, deterministic)
if err != nil {
if !nerr.Merge(err) {
return b, err
}
b = append(b, 1<<3|WireEndGroup)
}
return b, nil
return b, nerr.E
}
// Sort the keys to provide a deterministic encoding.
@ -2509,11 +2594,11 @@ func (u *marshalInfo) appendMessageSet(b []byte, ext *XXX_InternalExtensions, de
p := toAddrPointer(&v, ei.isptr)
b, err = ei.marshaler(b, p, 3<<3|WireBytes, deterministic)
b = append(b, 1<<3|WireEndGroup)
if err != nil {
if !nerr.Merge(err) {
return b, err
}
}
return b, nil
return b, nerr.E
}
// sizeV1Extensions computes the size of encoded data for a V1-API extension field.
@ -2556,6 +2641,7 @@ func (u *marshalInfo) appendV1Extensions(b []byte, m map[int32]Extension, determ
sort.Ints(keys)
var err error
var nerr nonFatal
for _, k := range keys {
e := m[int32(k)]
if e.value == nil || e.desc == nil {
@ -2572,11 +2658,11 @@ func (u *marshalInfo) appendV1Extensions(b []byte, m map[int32]Extension, determ
v := e.value
p := toAddrPointer(&v, ei.isptr)
b, err = ei.marshaler(b, p, ei.wiretag, deterministic)
if err != nil {
if !nerr.Merge(err) {
return b, err
}
}
return b, nil
return b, nerr.E
}
// newMarshaler is the interface representing objects that can marshal themselves.

View File

@ -97,6 +97,8 @@ type unmarshalFieldInfo struct {
// if a required field, contains a single set bit at this field's index in the required field list.
reqMask uint64
name string // name of the field, for error reporting
}
var (
@ -137,7 +139,7 @@ func (u *unmarshalInfo) unmarshal(m pointer, b []byte) error {
return UnmarshalMessageSet(b, m.offset(u.extensions).toExtensions())
}
var reqMask uint64 // bitmask of required fields we've seen.
var rnse *RequiredNotSetError // an instance of a RequiredNotSetError returned by a submessage.
var errLater error
for len(b) > 0 {
// Read tag and wire type.
// Special case 1 and 2 byte varints.
@ -176,11 +178,20 @@ func (u *unmarshalInfo) unmarshal(m pointer, b []byte) error {
if r, ok := err.(*RequiredNotSetError); ok {
// Remember this error, but keep parsing. We need to produce
// a full parse even if a required field is missing.
rnse = r
if errLater == nil {
errLater = r
}
reqMask |= f.reqMask
continue
}
if err != errInternalBadWireType {
if err == errInvalidUTF8 {
if errLater == nil {
fullName := revProtoTypes[reflect.PtrTo(u.typ)] + "." + f.name
errLater = &invalidUTF8Error{fullName}
}
continue
}
return err
}
// Fragments with bad wire type are treated as unknown fields.
@ -239,20 +250,16 @@ func (u *unmarshalInfo) unmarshal(m pointer, b []byte) error {
emap[int32(tag)] = e
}
}
if rnse != nil {
// A required field of a submessage/group is missing. Return that error.
return rnse
}
if reqMask != u.reqMask {
if reqMask != u.reqMask && errLater == nil {
// A required field of this message is missing.
for _, n := range u.reqFields {
if reqMask&1 == 0 {
return &RequiredNotSetError{n}
errLater = &RequiredNotSetError{n}
}
reqMask >>= 1
}
}
return nil
return errLater
}
// computeUnmarshalInfo fills in u with information for use
@ -351,7 +358,7 @@ func (u *unmarshalInfo) computeUnmarshalInfo() {
}
// Store the info in the correct slot in the message.
u.setTag(tag, toField(&f), unmarshal, reqMask)
u.setTag(tag, toField(&f), unmarshal, reqMask, name)
}
// Find any types associated with oneof fields.
@ -366,10 +373,17 @@ func (u *unmarshalInfo) computeUnmarshalInfo() {
f := typ.Field(0) // oneof implementers have one field
baseUnmarshal := fieldUnmarshaler(&f)
tagstr := strings.Split(f.Tag.Get("protobuf"), ",")[1]
tag, err := strconv.Atoi(tagstr)
tags := strings.Split(f.Tag.Get("protobuf"), ",")
fieldNum, err := strconv.Atoi(tags[1])
if err != nil {
panic("protobuf tag field not an integer: " + tagstr)
panic("protobuf tag field not an integer: " + tags[1])
}
var name string
for _, tag := range tags {
if strings.HasPrefix(tag, "name=") {
name = strings.TrimPrefix(tag, "name=")
break
}
}
// Find the oneof field that this struct implements.
@ -380,7 +394,7 @@ func (u *unmarshalInfo) computeUnmarshalInfo() {
// That lets us know where this struct should be stored
// when we encounter it during unmarshaling.
unmarshal := makeUnmarshalOneof(typ, of.ityp, baseUnmarshal)
u.setTag(tag, of.field, unmarshal, 0)
u.setTag(fieldNum, of.field, unmarshal, 0, name)
}
}
}
@ -401,7 +415,7 @@ func (u *unmarshalInfo) computeUnmarshalInfo() {
// [0 0] is [tag=0/wiretype=varint varint-encoded-0].
u.setTag(0, zeroField, func(b []byte, f pointer, w int) ([]byte, error) {
return nil, fmt.Errorf("proto: %s: illegal tag 0 (wire type %d)", t, w)
}, 0)
}, 0, "")
// Set mask for required field check.
u.reqMask = uint64(1)<<uint(len(u.reqFields)) - 1
@ -413,8 +427,9 @@ func (u *unmarshalInfo) computeUnmarshalInfo() {
// tag = tag # for field
// field/unmarshal = unmarshal info for that field.
// reqMask = if required, bitmask for field position in required field list. 0 otherwise.
func (u *unmarshalInfo) setTag(tag int, field field, unmarshal unmarshaler, reqMask uint64) {
i := unmarshalFieldInfo{field: field, unmarshal: unmarshal, reqMask: reqMask}
// name = short name of the field.
func (u *unmarshalInfo) setTag(tag int, field field, unmarshal unmarshaler, reqMask uint64, name string) {
i := unmarshalFieldInfo{field: field, unmarshal: unmarshal, reqMask: reqMask, name: name}
n := u.typ.NumField()
if tag >= 0 && (tag < 16 || tag < 2*n) { // TODO: what are the right numbers here?
for len(u.dense) <= tag {
@ -442,11 +457,17 @@ func typeUnmarshaler(t reflect.Type, tags string) unmarshaler {
tagArray := strings.Split(tags, ",")
encoding := tagArray[0]
name := "unknown"
proto3 := false
validateUTF8 := true
for _, tag := range tagArray[3:] {
if strings.HasPrefix(tag, "name=") {
name = tag[5:]
}
if tag == "proto3" {
proto3 = true
}
}
validateUTF8 = validateUTF8 && proto3
// Figure out packaging (pointer, slice, or both)
slice := false
@ -594,6 +615,15 @@ func typeUnmarshaler(t reflect.Type, tags string) unmarshaler {
}
return unmarshalBytesValue
case reflect.String:
if validateUTF8 {
if pointer {
return unmarshalUTF8StringPtr
}
if slice {
return unmarshalUTF8StringSlice
}
return unmarshalUTF8StringValue
}
if pointer {
return unmarshalStringPtr
}
@ -1448,9 +1478,6 @@ func unmarshalStringValue(b []byte, f pointer, w int) ([]byte, error) {
return nil, io.ErrUnexpectedEOF
}
v := string(b[:x])
if !utf8.ValidString(v) {
return nil, errInvalidUTF8
}
*f.toString() = v
return b[x:], nil
}
@ -1468,9 +1495,6 @@ func unmarshalStringPtr(b []byte, f pointer, w int) ([]byte, error) {
return nil, io.ErrUnexpectedEOF
}
v := string(b[:x])
if !utf8.ValidString(v) {
return nil, errInvalidUTF8
}
*f.toStringPtr() = &v
return b[x:], nil
}
@ -1488,14 +1512,72 @@ func unmarshalStringSlice(b []byte, f pointer, w int) ([]byte, error) {
return nil, io.ErrUnexpectedEOF
}
v := string(b[:x])
if !utf8.ValidString(v) {
return nil, errInvalidUTF8
}
s := f.toStringSlice()
*s = append(*s, v)
return b[x:], nil
}
func unmarshalUTF8StringValue(b []byte, f pointer, w int) ([]byte, error) {
if w != WireBytes {
return b, errInternalBadWireType
}
x, n := decodeVarint(b)
if n == 0 {
return nil, io.ErrUnexpectedEOF
}
b = b[n:]
if x > uint64(len(b)) {
return nil, io.ErrUnexpectedEOF
}
v := string(b[:x])
*f.toString() = v
if !utf8.ValidString(v) {
return b[x:], errInvalidUTF8
}
return b[x:], nil
}
func unmarshalUTF8StringPtr(b []byte, f pointer, w int) ([]byte, error) {
if w != WireBytes {
return b, errInternalBadWireType
}
x, n := decodeVarint(b)
if n == 0 {
return nil, io.ErrUnexpectedEOF
}
b = b[n:]
if x > uint64(len(b)) {
return nil, io.ErrUnexpectedEOF
}
v := string(b[:x])
*f.toStringPtr() = &v
if !utf8.ValidString(v) {
return b[x:], errInvalidUTF8
}
return b[x:], nil
}
func unmarshalUTF8StringSlice(b []byte, f pointer, w int) ([]byte, error) {
if w != WireBytes {
return b, errInternalBadWireType
}
x, n := decodeVarint(b)
if n == 0 {
return nil, io.ErrUnexpectedEOF
}
b = b[n:]
if x > uint64(len(b)) {
return nil, io.ErrUnexpectedEOF
}
v := string(b[:x])
s := f.toStringSlice()
*s = append(*s, v)
if !utf8.ValidString(v) {
return b[x:], errInvalidUTF8
}
return b[x:], nil
}
var emptyBuf [0]byte
func unmarshalBytesValue(b []byte, f pointer, w int) ([]byte, error) {
@ -1674,6 +1756,7 @@ func makeUnmarshalMap(f *reflect.StructField) unmarshaler {
// Maps will be somewhat slow. Oh well.
// Read key and value from data.
var nerr nonFatal
k := reflect.New(kt)
v := reflect.New(vt)
for len(b) > 0 {
@ -1694,7 +1777,7 @@ func makeUnmarshalMap(f *reflect.StructField) unmarshaler {
err = errInternalBadWireType // skip unknown tag
}
if err == nil {
if nerr.Merge(err) {
continue
}
if err != errInternalBadWireType {
@ -1717,7 +1800,7 @@ func makeUnmarshalMap(f *reflect.StructField) unmarshaler {
// Insert into map.
m.SetMapIndex(k.Elem(), v.Elem())
return r, nil
return r, nerr.E
}
}
@ -1743,15 +1826,16 @@ func makeUnmarshalOneof(typ, ityp reflect.Type, unmarshal unmarshaler) unmarshal
// Unmarshal data into holder.
// We unmarshal into the first field of the holder object.
var err error
var nerr nonFatal
b, err = unmarshal(b, valToPointer(v).offset(field0), w)
if err != nil {
if !nerr.Merge(err) {
return nil, err
}
// Write pointer to holder into target field.
f.asPointerTo(ityp).Elem().Set(v)
return b, nil
return b, nerr.E
}
}

View File

@ -353,7 +353,7 @@ func (tm *TextMarshaler) writeStruct(w *textWriter, sv reflect.Value) error {
return err
}
}
if err := tm.writeAny(w, key, props.mkeyprop); err != nil {
if err := tm.writeAny(w, key, props.MapKeyProp); err != nil {
return err
}
if err := w.WriteByte('\n'); err != nil {
@ -370,7 +370,7 @@ func (tm *TextMarshaler) writeStruct(w *textWriter, sv reflect.Value) error {
return err
}
}
if err := tm.writeAny(w, val, props.mvalprop); err != nil {
if err := tm.writeAny(w, val, props.MapValProp); err != nil {
return err
}
if err := w.WriteByte('\n'); err != nil {

View File

@ -630,17 +630,17 @@ func (p *textParser) readStruct(sv reflect.Value, terminator string) error {
if err := p.consumeToken(":"); err != nil {
return err
}
if err := p.readAny(key, props.mkeyprop); err != nil {
if err := p.readAny(key, props.MapKeyProp); err != nil {
return err
}
if err := p.consumeOptionalSeparator(); err != nil {
return err
}
case "value":
if err := p.checkForColon(props.mvalprop, dst.Type().Elem()); err != nil {
if err := p.checkForColon(props.MapValProp, dst.Type().Elem()); err != nil {
return err
}
if err := p.readAny(val, props.mvalprop); err != nil {
if err := p.readAny(val, props.MapValProp); err != nil {
return err
}
if err := p.consumeOptionalSeparator(); err != nil {

View File

@ -13,6 +13,7 @@ Abhinav Gupta <mail@abhinavg.net>
Ahmed Hagy <a.akram93@gmail.com>
Ainsley Chong <ainsley.chong@gmail.com>
Akeda Bagus <akeda@x-team.com>
Akhil Mohan <akhilerm@gmail.com>
Alec Thomas <alec@swapoff.org>
Aleks Clark <aleks.clark@gmail.com>
Alex Bramley <a.bramley@gmail.com>
@ -38,6 +39,7 @@ Björn Häuser <b.haeuser@rebuy.de>
Brad Harris <bmharris@gmail.com>
Brad Moylan <moylan.brad@gmail.com>
Bradley Falzon <brad@teambrad.net>
Brandon Cook <phylake@gmail.com>
Brian Egizi <brian@mojotech.com>
Bryan Boreham <bryan@weave.works>
Cami Diez <diezcami@gmail.com>
@ -70,6 +72,7 @@ Drew Fradette <drew.fradette@gmail.com>
Eli Uriegas <seemethere101@gmail.com>
Elliott Beach <elliott2.71828@gmail.com>
Emerson Wood <emersonwood94@gmail.com>
eperm <staffordworrell@gmail.com>
erwinvaneyk <erwinvaneyk@gmail.com>
Fabrice <fabrice.vaillant@student.ecp.fr>
Filippo Valsorda <hi@filippo.io>
@ -78,6 +81,7 @@ Francesc Gil <xescugil@gmail.com>
Francis <hello@francismakes.com>
Fredrik Jönsson <fredrik.jonsson@izettle.com>
Garrett Squire <garrettsquire@gmail.com>
George Kontridze <george.kontridze@gmail.com>
Georgy Buranov <gburanov@gmail.com>
Gnahz <p@oath.pl>
Google Inc.
@ -95,15 +99,19 @@ Isao Jonas <isao.jonas@gmail.com>
isqua <isqua@isqua.ru>
Jameel Haffejee <RC1140@republiccommandos.co.za>
Jan Kosecki <jan.kosecki91@gmail.com>
Javier Campanini <jcampanini@palantir.com>
Jeremy Morris <jeremylevanmorris@gmail.com>
Jesse Newland <jesse@jnewland.com>
Jihoon Chung <j.c@navercorp.com>
Jimmi Dyson <jimmidyson@gmail.com>
Joan Saum <joan.saum@epitech.eu>
Joe Tsai <joetsai@digital-static.net>
John Barton <jrbarton@gmail.com>
John Engelman <john.r.engelman@gmail.com>
JP Phillips <jonphill9@gmail.com>
jpbelanger-mtl <jp.belanger@gmail.com>
Juan Basso <jrbasso@gmail.com>
Julien Garcia Gonzalez <garciagonzalez.julien@gmail.com>
Julien Rostand <jrostand@users.noreply.github.com>
Justin Abrahms <justin@abrah.ms>
Jusung Lee <e.jusunglee@gmail.com>
@ -123,9 +131,11 @@ Luke Roberts <email@luke-roberts.co.uk>
Luke Young <luke@hydrantlabs.org>
Maksim Zhylinski <uzzable@gmail.com>
Martin-Louis Bright <mlbright@gmail.com>
Marwan Sulaiman <marwan.sameer@gmail.com>
Mat Geist <matgeist@gmail.com>
Matt <alpmatthew@gmail.com>
Matt Brender <mjbrender@gmail.com>
Matt Gaunt <matt@gauntface.co.uk>
Matt Landis <landis.matt@gmail.com>
Maxime Bury <maxime.bury@gmail.com>
Michael Spiegel <michael.m.spiegel@gmail.com>
@ -145,17 +155,20 @@ Parham Alvani <parham.alvani@gmail.com>
Parker Moore <parkrmoore@gmail.com>
parkhyukjun89 <park.hyukjun89@gmail.com>
Pavel Shtanko <pavel.shtanko@gmail.com>
Pete Wagner <thepwagner@github.com>
Petr Shevtsov <petr.shevtsov@gmail.com>
Pierre Carrier <pierre@meteor.com>
Piotr Zurek <p.zurek@gmail.com>
Quinn Slack <qslack@qslack.com>
Rackspace US, Inc.
Radek Simko <radek.simko@gmail.com>
Radliński Ignacy <radlinsk@student.agh.edu.pl>
Rajendra arora <rajendraarora16@yahoo.com>
RaviTeja Pothana <ravi-teja@live.com>
rc1140 <jameel@republiccommandos.co.za>
Red Hat, Inc.
Rob Figueiredo <robfig@yext.com>
Rohit Upadhyay <urohit011@gmail.com>
Ronak Jain <ronakjain@outlook.in>
Ruben Vereecken <rubenvereecken@gmail.com>
Ryan Lower <rpjlower@gmail.com>
@ -176,6 +189,7 @@ Shawn Smith <shawnpsmith@gmail.com>
sona-tar <sona.zip@gmail.com>
SoundCloud, Ltd.
Stian Eikeland <stian@eikeland.se>
Tasya Aditya Rukmana <tadityar@gmail.com>
Thomas Bruyelle <thomas.bruyelle@gmail.com>
Timothée Peignier <timothee.peignier@tryphon.org>
Trey Tacon <ttacon@gmail.com>
@ -186,6 +200,7 @@ Victor Vrantchan <vrancean+github@gmail.com>
Vlad Ungureanu <vladu@palantir.com>
Will Maier <wcmaier@gmail.com>
William Bailey <mail@williambailey.org.uk>
xibz <impactbchang@gmail.com>
Yann Malet <yann.malet@gmail.com>
Yannick Utard <yannickutard@gmail.com>
Yicheng Qin <qycqycqycqycqyc@gmail.com>

View File

@ -96,6 +96,8 @@ func (e *Event) ParsePayload() (payload interface{}, err error) {
payload = &ReleaseEvent{}
case "RepositoryEvent":
payload = &RepositoryEvent{}
case "RepositoryVulnerabilityAlertEvent":
payload = &RepositoryVulnerabilityAlertEvent{}
case "StatusEvent":
payload = &StatusEvent{}
case "TeamEvent":

View File

@ -8,6 +8,7 @@ package github
import (
"context"
"fmt"
"strings"
)
// StarredRepository is returned by ListStarred.
@ -84,8 +85,9 @@ func (s *ActivityService) ListStarred(ctx context.Context, user string, opt *Act
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeStarringPreview)
// TODO: remove custom Accept header when APIs fully launch
acceptHeaders := []string{mediaTypeStarringPreview, mediaTypeTopicsPreview}
req.Header.Set("Accept", strings.Join(acceptHeaders, ", "))
var repos []*StarredRepository
resp, err := s.client.Do(ctx, req, &repos)

View File

@ -164,7 +164,7 @@ func (s *AppsService) ListUserInstallations(ctx context.Context, opt *ListOption
//
// GitHub API docs: https://developer.github.com/v3/apps/#create-a-new-installation-token
func (s *AppsService) CreateInstallationToken(ctx context.Context, id int64) (*InstallationToken, *Response, error) {
u := fmt.Sprintf("installations/%v/access_tokens", id)
u := fmt.Sprintf("app/installations/%v/access_tokens", id)
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {

View File

@ -19,6 +19,7 @@ type ChecksService service
// CheckRun represents a GitHub check run on a repository associated with a GitHub app.
type CheckRun struct {
ID *int64 `json:"id,omitempty"`
NodeID *string `json:"node_id,omitempty"`
HeadSHA *string `json:"head_sha,omitempty"`
ExternalID *string `json:"external_id,omitempty"`
URL *string `json:"url,omitempty"`
@ -47,11 +48,11 @@ type CheckRunOutput struct {
// CheckRunAnnotation represents an annotation object for a CheckRun output.
type CheckRunAnnotation struct {
FileName *string `json:"filename,omitempty"`
Path *string `json:"path,omitempty"`
BlobHRef *string `json:"blob_href,omitempty"`
StartLine *int `json:"start_line,omitempty"`
EndLine *int `json:"end_line,omitempty"`
WarningLevel *string `json:"warning_level,omitempty"`
AnnotationLevel *string `json:"annotation_level,omitempty"`
Message *string `json:"message,omitempty"`
Title *string `json:"title,omitempty"`
RawDetails *string `json:"raw_details,omitempty"`
@ -67,6 +68,7 @@ type CheckRunImage struct {
// CheckSuite represents a suite of check runs.
type CheckSuite struct {
ID *int64 `json:"id,omitempty"`
NodeID *string `json:"node_id,omitempty"`
HeadBranch *string `json:"head_branch,omitempty"`
HeadSHA *string `json:"head_sha,omitempty"`
URL *string `json:"url,omitempty"`
@ -401,20 +403,11 @@ func (s *ChecksService) CreateCheckSuite(ctx context.Context, owner, repo string
return checkSuite, resp, nil
}
// RequestCheckSuiteOptions sets up the parameters for a request check suite endpoint.
type RequestCheckSuiteOptions struct {
HeadSHA string `json:"head_sha"` // The sha of the head commit. (Required.)
}
// RequestCheckSuite triggers GitHub to create a new check suite, without pushing new code to a repository.
// ReRequestCheckSuite triggers GitHub to rerequest an existing check suite, without pushing new code to a repository.
//
// GitHub API docs: https://developer.github.com/v3/checks/suites/#request-check-suites
func (s *ChecksService) RequestCheckSuite(ctx context.Context, owner, repo string, opt RequestCheckSuiteOptions) (*Response, error) {
u := fmt.Sprintf("repos/%v/%v/check-suite-requests", owner, repo)
u, err := addOptions(u, opt)
if err != nil {
return nil, err
}
// GitHub API docs: https://developer.github.com/v3/checks/suites/#rerequest-check-suite
func (s *ChecksService) ReRequestCheckSuite(ctx context.Context, owner, repo string, checkSuiteID int64) (*Response, error) {
u := fmt.Sprintf("repos/%v/%v/check-suites/%v/rerequest", owner, repo, checkSuiteID)
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {

View File

@ -519,6 +519,7 @@ type PullRequestEvent struct {
// the pull request was closed with unmerged commits. If the action is "closed"
// and the merged key is true, the pull request was merged.
Action *string `json:"action,omitempty"`
Assignee *User `json:"assignee,omitempty"`
Number *int `json:"number,omitempty"`
PullRequest *PullRequest `json:"pull_request,omitempty"`
@ -532,6 +533,10 @@ type PullRequestEvent struct {
Sender *User `json:"sender,omitempty"`
Installation *Installation `json:"installation,omitempty"`
Label *Label `json:"label,omitempty"` // Populated in "labeled" event deliveries.
// The following field is only present when the webhook is triggered on
// a repository belonging to an organization.
Organization *Organization `json:"organization,omitempty"`
}
// PullRequestReviewEvent is triggered when a review is submitted on a pull
@ -705,6 +710,27 @@ type RepositoryEvent struct {
Installation *Installation `json:"installation,omitempty"`
}
// RepositoryVulnerabilityAlertEvent is triggered when a security alert is created, dismissed, or resolved.
//
// GitHub API docs: https://developer.github.com/v3/activity/events/types/#repositoryvulnerabilityalertevent
type RepositoryVulnerabilityAlertEvent struct {
// Action is the action that was performed. This can be: "create", "dismiss", "resolve".
Action *string `json:"action,omitempty"`
//The security alert of the vulnerable dependency.
Alert *struct {
ID *int64 `json:"id,omitempty"`
AffectedRange *string `json:"affected_range,omitempty"`
AffectedPackageName *string `json:"affected_package_name,omitempty"`
ExternalReference *string `json:"external_reference,omitempty"`
ExternalIdentifier *string `json:"external_identifier,omitempty"`
FixedIn *string `json:"fixed_in,omitempty"`
Dismisser *User `json:"dismisser,omitempty"`
DismissReason *string `json:"dismiss_reason,omitempty"`
DismissedAt *Timestamp `json:"dismissed_at,omitempty"`
} `json:"alert,omitempty"`
}
// StatusEvent is triggered when the status of a Git commit changes.
// The Webhook event name is "status".
//

View File

@ -58,7 +58,7 @@ func (c CommitAuthor) String() string {
return Stringify(c)
}
// GetCommit fetchs the Commit object for a given SHA.
// GetCommit fetches the Commit object for a given SHA.
//
// GitHub API docs: https://developer.github.com/v3/git/commits/#get-a-commit
func (s *GitService) GetCommit(ctx context.Context, owner string, repo string, sha string) (*Commit, *Response, error) {

View File

@ -33,7 +33,7 @@ type createTagRequest struct {
Tagger *CommitAuthor `json:"tagger,omitempty"`
}
// GetTag fetchs a tag from a repo given a SHA.
// GetTag fetches a tag from a repo given a SHA.
//
// GitHub API docs: https://developer.github.com/v3/git/tags/#get-a-tag
func (s *GitService) GetTag(ctx context.Context, owner string, repo string, sha string) (*Tag, *Response, error) {

View File

@ -532,6 +532,14 @@ func (c *CheckRun) GetName() string {
return *c.Name
}
// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise.
func (c *CheckRun) GetNodeID() string {
if c == nil || c.NodeID == nil {
return ""
}
return *c.NodeID
}
// GetOutput returns the Output field.
func (c *CheckRun) GetOutput() *CheckRunOutput {
if c == nil {
@ -564,6 +572,14 @@ func (c *CheckRun) GetURL() string {
return *c.URL
}
// GetAnnotationLevel returns the AnnotationLevel field if it's non-nil, zero value otherwise.
func (c *CheckRunAnnotation) GetAnnotationLevel() string {
if c == nil || c.AnnotationLevel == nil {
return ""
}
return *c.AnnotationLevel
}
// GetBlobHRef returns the BlobHRef field if it's non-nil, zero value otherwise.
func (c *CheckRunAnnotation) GetBlobHRef() string {
if c == nil || c.BlobHRef == nil {
@ -580,14 +596,6 @@ func (c *CheckRunAnnotation) GetEndLine() int {
return *c.EndLine
}
// GetFileName returns the FileName field if it's non-nil, zero value otherwise.
func (c *CheckRunAnnotation) GetFileName() string {
if c == nil || c.FileName == nil {
return ""
}
return *c.FileName
}
// GetMessage returns the Message field if it's non-nil, zero value otherwise.
func (c *CheckRunAnnotation) GetMessage() string {
if c == nil || c.Message == nil {
@ -596,6 +604,14 @@ func (c *CheckRunAnnotation) GetMessage() string {
return *c.Message
}
// GetPath returns the Path field if it's non-nil, zero value otherwise.
func (c *CheckRunAnnotation) GetPath() string {
if c == nil || c.Path == nil {
return ""
}
return *c.Path
}
// GetRawDetails returns the RawDetails field if it's non-nil, zero value otherwise.
func (c *CheckRunAnnotation) GetRawDetails() string {
if c == nil || c.RawDetails == nil {
@ -620,14 +636,6 @@ func (c *CheckRunAnnotation) GetTitle() string {
return *c.Title
}
// GetWarningLevel returns the WarningLevel field if it's non-nil, zero value otherwise.
func (c *CheckRunAnnotation) GetWarningLevel() string {
if c == nil || c.WarningLevel == nil {
return ""
}
return *c.WarningLevel
}
// GetAction returns the Action field if it's non-nil, zero value otherwise.
func (c *CheckRunEvent) GetAction() string {
if c == nil || c.Action == nil {
@ -796,6 +804,14 @@ func (c *CheckSuite) GetID() int64 {
return *c.ID
}
// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise.
func (c *CheckSuite) GetNodeID() string {
if c == nil || c.NodeID == nil {
return ""
}
return *c.NodeID
}
// GetRepository returns the Repository field.
func (c *CheckSuite) GetRepository() *Repository {
if c == nil {
@ -2380,6 +2396,14 @@ func (d *DiscussionComment) GetNumber() int {
return *d.Number
}
// GetReactions returns the Reactions field.
func (d *DiscussionComment) GetReactions() *Reactions {
if d == nil {
return nil
}
return d.Reactions
}
// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise.
func (d *DiscussionComment) GetUpdatedAt() Timestamp {
if d == nil || d.UpdatedAt == nil {
@ -3116,14 +3140,6 @@ func (h *Hook) GetID() int64 {
return *h.ID
}
// GetName returns the Name field if it's non-nil, zero value otherwise.
func (h *Hook) GetName() string {
if h == nil || h.Name == nil {
return ""
}
return *h.Name
}
// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise.
func (h *Hook) GetUpdatedAt() time.Time {
if h == nil || h.UpdatedAt == nil {
@ -3860,6 +3876,14 @@ func (i *IssueComment) GetIssueURL() string {
return *i.IssueURL
}
// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise.
func (i *IssueComment) GetNodeID() string {
if i == nil || i.NodeID == nil {
return ""
}
return *i.NodeID
}
// GetReactions returns the Reactions field.
func (i *IssueComment) GetReactions() *Reactions {
if i == nil {
@ -5524,6 +5548,22 @@ func (o *Organization) GetCreatedAt() time.Time {
return *o.CreatedAt
}
// GetDefaultRepoPermission returns the DefaultRepoPermission field if it's non-nil, zero value otherwise.
func (o *Organization) GetDefaultRepoPermission() string {
if o == nil || o.DefaultRepoPermission == nil {
return ""
}
return *o.DefaultRepoPermission
}
// GetDefaultRepoSettings returns the DefaultRepoSettings field if it's non-nil, zero value otherwise.
func (o *Organization) GetDefaultRepoSettings() string {
if o == nil || o.DefaultRepoSettings == nil {
return ""
}
return *o.DefaultRepoSettings
}
// GetDescription returns the Description field if it's non-nil, zero value otherwise.
func (o *Organization) GetDescription() string {
if o == nil || o.Description == nil {
@ -5620,6 +5660,14 @@ func (o *Organization) GetLogin() string {
return *o.Login
}
// GetMembersCanCreateRepos returns the MembersCanCreateRepos field if it's non-nil, zero value otherwise.
func (o *Organization) GetMembersCanCreateRepos() bool {
if o == nil || o.MembersCanCreateRepos == nil {
return false
}
return *o.MembersCanCreateRepos
}
// GetMembersURL returns the MembersURL field if it's non-nil, zero value otherwise.
func (o *Organization) GetMembersURL() string {
if o == nil || o.MembersURL == nil {
@ -5708,6 +5756,14 @@ func (o *Organization) GetTotalPrivateRepos() int {
return *o.TotalPrivateRepos
}
// GetTwoFactorRequirementEnabled returns the TwoFactorRequirementEnabled field if it's non-nil, zero value otherwise.
func (o *Organization) GetTwoFactorRequirementEnabled() bool {
if o == nil || o.TwoFactorRequirementEnabled == nil {
return false
}
return *o.TwoFactorRequirementEnabled
}
// GetType returns the Type field if it's non-nil, zero value otherwise.
func (o *Organization) GetType() string {
if o == nil || o.Type == nil {
@ -6156,6 +6212,78 @@ func (p *PreReceiveHook) GetName() string {
return *p.Name
}
// GetHRef returns the HRef field if it's non-nil, zero value otherwise.
func (p *PRLink) GetHRef() string {
if p == nil || p.HRef == nil {
return ""
}
return *p.HRef
}
// GetComments returns the Comments field.
func (p *PRLinks) GetComments() *PRLink {
if p == nil {
return nil
}
return p.Comments
}
// GetCommits returns the Commits field.
func (p *PRLinks) GetCommits() *PRLink {
if p == nil {
return nil
}
return p.Commits
}
// GetHTML returns the HTML field.
func (p *PRLinks) GetHTML() *PRLink {
if p == nil {
return nil
}
return p.HTML
}
// GetIssue returns the Issue field.
func (p *PRLinks) GetIssue() *PRLink {
if p == nil {
return nil
}
return p.Issue
}
// GetReviewComment returns the ReviewComment field.
func (p *PRLinks) GetReviewComment() *PRLink {
if p == nil {
return nil
}
return p.ReviewComment
}
// GetReviewComments returns the ReviewComments field.
func (p *PRLinks) GetReviewComments() *PRLink {
if p == nil {
return nil
}
return p.ReviewComments
}
// GetSelf returns the Self field.
func (p *PRLinks) GetSelf() *PRLink {
if p == nil {
return nil
}
return p.Self
}
// GetStatuses returns the Statuses field.
func (p *PRLinks) GetStatuses() *PRLink {
if p == nil {
return nil
}
return p.Statuses
}
// GetBody returns the Body field if it's non-nil, zero value otherwise.
func (p *Project) GetBody() string {
if p == nil || p.Body == nil {
@ -6884,6 +7012,14 @@ func (p *PullRequest) GetIssueURL() string {
return *p.IssueURL
}
// GetLinks returns the Links field.
func (p *PullRequest) GetLinks() *PRLinks {
if p == nil {
return nil
}
return p.Links
}
// GetMaintainerCanModify returns the MaintainerCanModify field if it's non-nil, zero value otherwise.
func (p *PullRequest) GetMaintainerCanModify() bool {
if p == nil || p.MaintainerCanModify == nil {
@ -7228,6 +7364,14 @@ func (p *PullRequestEvent) GetAction() string {
return *p.Action
}
// GetAssignee returns the Assignee field.
func (p *PullRequestEvent) GetAssignee() *User {
if p == nil {
return nil
}
return p.Assignee
}
// GetChanges returns the Changes field.
func (p *PullRequestEvent) GetChanges() *EditChange {
if p == nil {
@ -7260,6 +7404,14 @@ func (p *PullRequestEvent) GetNumber() int {
return *p.Number
}
// GetOrganization returns the Organization field.
func (p *PullRequestEvent) GetOrganization() *Organization {
if p == nil {
return nil
}
return p.Organization
}
// GetPullRequest returns the PullRequest field.
func (p *PullRequestEvent) GetPullRequest() *PullRequest {
if p == nil {
@ -9804,6 +9956,14 @@ func (r *RepositoryTag) GetZipballURL() string {
return *r.ZipballURL
}
// GetAction returns the Action field if it's non-nil, zero value otherwise.
func (r *RepositoryVulnerabilityAlertEvent) GetAction() string {
if r == nil || r.Action == nil {
return ""
}
return *r.Action
}
// GetForkRepos returns the ForkRepos field if it's non-nil, zero value otherwise.
func (r *RepoStats) GetForkRepos() int {
if r == nil || r.ForkRepos == nil {
@ -10564,6 +10724,14 @@ func (t *TeamDiscussion) GetPrivate() bool {
return *t.Private
}
// GetReactions returns the Reactions field.
func (t *TeamDiscussion) GetReactions() *Reactions {
if t == nil {
return nil
}
return t.Reactions
}
// GetTeamURL returns the TeamURL field if it's non-nil, zero value otherwise.
func (t *TeamDiscussion) GetTeamURL() string {
if t == nil || t.TeamURL == nil {

View File

@ -375,7 +375,9 @@ type Response struct {
FirstPage int
LastPage int
Rate
// Explicitly specify the Rate type so Rate's String() receiver doesn't
// propagate to Response.
Rate Rate
}
// newResponse creates a new Response for the provided http.Response.
@ -500,13 +502,23 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Res
err = CheckResponse(resp)
if err != nil {
// Even though there was an error, we still return the response
// in case the caller wants to inspect it further.
// However, if the error is AcceptedError, decode it below before
// returning from this function and closing the response body.
if _, ok := err.(*AcceptedError); !ok {
return response, err
// Special case for AcceptedErrors. If an AcceptedError
// has been encountered, the response's payload will be
// added to the AcceptedError and returned.
//
// Issue #1022
aerr, ok := err.(*AcceptedError)
if ok {
b, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
return response, readErr
}
aerr.Raw = b
return response, aerr
}
return response, err
}
if v != nil {
@ -608,7 +620,10 @@ func (r *RateLimitError) Error() string {
// Technically, 202 Accepted is not a real error, it's just used to
// indicate that results are not ready yet, but should be available soon.
// The request can be repeated after some time.
type AcceptedError struct{}
type AcceptedError struct {
// Raw contains the response body.
Raw []byte
}
func (*AcceptedError) Error() string {
return "job scheduled on GitHub side; try again later"

View File

@ -228,7 +228,7 @@ func (s *IssuesService) ListByRepo(ctx context.Context, owner string, repo strin
}
// TODO: remove custom Accept headers when APIs fully launch.
acceptHeaders := []string{mediaTypeReactionsPreview, mediaTypeLabelDescriptionSearchPreview, mediaTypeLockReasonPreview}
acceptHeaders := []string{mediaTypeReactionsPreview, mediaTypeLabelDescriptionSearchPreview, mediaTypeIntegrationPreview}
req.Header.Set("Accept", strings.Join(acceptHeaders, ", "))
var issues []*Issue

View File

@ -14,6 +14,7 @@ import (
// IssueComment represents a comment left on an issue.
type IssueComment struct {
ID *int64 `json:"id,omitempty"`
NodeID *string `json:"node_id,omitempty"`
Body *string `json:"body,omitempty"`
User *User `json:"user,omitempty"`
Reactions *Reactions `json:"reactions,omitempty"`

View File

@ -72,6 +72,7 @@ var (
"pull_request": "PullRequestEvent",
"push": "PushEvent",
"repository": "RepositoryEvent",
"repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent",
"release": "ReleaseEvent",
"status": "StatusEvent",
"team": "TeamEvent",
@ -175,19 +176,19 @@ func ValidatePayload(r *http.Request, secretKey []byte) (payload []byte, err err
}
sig := r.Header.Get(signatureHeader)
if err := validateSignature(sig, body, secretKey); err != nil {
if err := ValidateSignature(sig, body, secretKey); err != nil {
return nil, err
}
return payload, nil
}
// validateSignature validates the signature for the given payload.
// ValidateSignature validates the signature for the given payload.
// signature is the GitHub hash signature delivered in the X-Hub-Signature header.
// payload is the JSON payload sent by GitHub Webhooks.
// secretKey is the GitHub Webhook secret message.
//
// GitHub API docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github
func validateSignature(signature string, payload, secretKey []byte) error {
func ValidateSignature(signature string, payload, secretKey []byte) error {
messageMAC, hashFunc, err := messageMAC(signature)
if err != nil {
return err

View File

@ -44,6 +44,17 @@ type Organization struct {
BillingEmail *string `json:"billing_email,omitempty"`
Type *string `json:"type,omitempty"`
Plan *Plan `json:"plan,omitempty"`
TwoFactorRequirementEnabled *bool `json:"two_factor_requirement_enabled,omitempty"`
// DefaultRepoPermission can be one of: "read", "write", "admin", or "none". (Default: "read").
// It is only used in OrganizationsService.Edit.
DefaultRepoPermission *string `json:"default_repository_permission,omitempty"`
// DefaultRepoSettings can be one of: "read", "write", "admin", or "none". (Default: "read").
// It is only used in OrganizationsService.Get.
DefaultRepoSettings *string `json:"default_repository_settings,omitempty"`
// MembersCanCreateRepos default value is true and is only used in Organizations.Edit.
MembersCanCreateRepos *bool `json:"members_can_create_repositories,omitempty"`
// API URLs
URL *string `json:"url,omitempty"`

View File

@ -49,12 +49,22 @@ func (s *OrganizationsService) GetHook(ctx context.Context, org string, id int64
}
// CreateHook creates a Hook for the specified org.
// Name and Config are required fields.
// Config is a required field.
//
// Note that only a subset of the hook fields are used and hook must
// not be nil.
//
// GitHub API docs: https://developer.github.com/v3/orgs/hooks/#create-a-hook
func (s *OrganizationsService) CreateHook(ctx context.Context, org string, hook *Hook) (*Hook, *Response, error) {
u := fmt.Sprintf("orgs/%v/hooks", org)
req, err := s.client.NewRequest("POST", u, hook)
hookReq := &createHookRequest{
Events: hook.Events,
Active: hook.Active,
Config: hook.Config,
}
req, err := s.client.NewRequest("POST", u, hookReq)
if err != nil {
return nil, nil, err
}

View File

@ -60,6 +60,11 @@ type PullRequest struct {
NodeID *string `json:"node_id,omitempty"`
RequestedReviewers []*User `json:"requested_reviewers,omitempty"`
// RequestedTeams is populated as part of the PullRequestEvent.
// See, https://developer.github.com/v3/activity/events/types/#pullrequestevent for an example.
RequestedTeams []*Team `json:"requested_teams,omitempty"`
Links *PRLinks `json:"_links,omitempty"`
Head *PullRequestBranch `json:"head,omitempty"`
Base *PullRequestBranch `json:"base,omitempty"`
@ -72,6 +77,23 @@ func (p PullRequest) String() string {
return Stringify(p)
}
// PRLink represents a single link object from Github pull request _links.
type PRLink struct {
HRef *string `json:"href,omitempty"`
}
// PRLinks represents the "_links" object in a Github pull request.
type PRLinks struct {
Self *PRLink `json:"self,omitempty"`
HTML *PRLink `json:"html,omitempty"`
Issue *PRLink `json:"issue,omitempty"`
Comments *PRLink `json:"comments,omitempty"`
ReviewComments *PRLink `json:"review_comments,omitempty"`
ReviewComment *PRLink `json:"review_comment,omitempty"`
Commits *PRLink `json:"commits,omitempty"`
Statuses *PRLink `json:"statuses,omitempty"`
}
// PullRequestBranch represents a base or head branch in a GitHub pull request.
type PullRequestBranch struct {
Label *string `json:"label,omitempty"`

View File

@ -74,6 +74,7 @@ func (s *ReactionsService) ListCommentReactions(ctx context.Context, owner, repo
// CreateCommentReaction creates a reaction for a commit comment.
// Note that if you have already created a reaction of type content, the
// previously created reaction will be returned with Status: 200 OK.
// The content should have one of the following values: "+1", "-1", "laugh", "confused", "heart", "hooray".
//
// GitHub API docs: https://developer.github.com/v3/reactions/#create-reaction-for-a-commit-comment
func (s ReactionsService) CreateCommentReaction(ctx context.Context, owner, repo string, id int64, content string) (*Reaction, *Response, error) {
@ -127,6 +128,7 @@ func (s *ReactionsService) ListIssueReactions(ctx context.Context, owner, repo s
// CreateIssueReaction creates a reaction for an issue.
// Note that if you have already created a reaction of type content, the
// previously created reaction will be returned with Status: 200 OK.
// The content should have one of the following values: "+1", "-1", "laugh", "confused", "heart", "hooray".
//
// GitHub API docs: https://developer.github.com/v3/reactions/#create-reaction-for-an-issue
func (s ReactionsService) CreateIssueReaction(ctx context.Context, owner, repo string, number int, content string) (*Reaction, *Response, error) {
@ -180,6 +182,7 @@ func (s *ReactionsService) ListIssueCommentReactions(ctx context.Context, owner,
// CreateIssueCommentReaction creates a reaction for an issue comment.
// Note that if you have already created a reaction of type content, the
// previously created reaction will be returned with Status: 200 OK.
// The content should have one of the following values: "+1", "-1", "laugh", "confused", "heart", "hooray".
//
// GitHub API docs: https://developer.github.com/v3/reactions/#create-reaction-for-an-issue-comment
func (s ReactionsService) CreateIssueCommentReaction(ctx context.Context, owner, repo string, id int64, content string) (*Reaction, *Response, error) {
@ -233,6 +236,7 @@ func (s *ReactionsService) ListPullRequestCommentReactions(ctx context.Context,
// CreatePullRequestCommentReaction creates a reaction for a pull request review comment.
// Note that if you have already created a reaction of type content, the
// previously created reaction will be returned with Status: 200 OK.
// The content should have one of the following values: "+1", "-1", "laugh", "confused", "heart", "hooray".
//
// GitHub API docs: https://developer.github.com/v3/reactions/#create-reaction-for-an-issue-comment
func (s ReactionsService) CreatePullRequestCommentReaction(ctx context.Context, owner, repo string, id int64, content string) (*Reaction, *Response, error) {
@ -256,6 +260,103 @@ func (s ReactionsService) CreatePullRequestCommentReaction(ctx context.Context,
return m, resp, nil
}
// ListTeamDiscussionReactions lists the reactions for a team discussion.
//
// GitHub API docs: https://developer.github.com/v3/reactions/#list-reactions-for-a-team-discussion
func (s *ReactionsService) ListTeamDiscussionReactions(ctx context.Context, teamID int64, discussionNumber int, opt *ListOptions) ([]*Reaction, *Response, error) {
u := fmt.Sprintf("teams/%v/discussions/%v/reactions", teamID, discussionNumber)
u, err := addOptions(u, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}
req.Header.Set("Accept", mediaTypeReactionsPreview)
var m []*Reaction
resp, err := s.client.Do(ctx, req, &m)
if err != nil {
return nil, resp, err
}
return m, resp, nil
}
// CreateTeamDiscussionReaction creates a reaction for a team discussion.
// The content should have one of the following values: "+1", "-1", "laugh", "confused", "heart", "hooray".
//
// GitHub API docs: https://developer.github.com/v3/reactions/#create-reaction-for-a-team-discussion
func (s *ReactionsService) CreateTeamDiscussionReaction(ctx context.Context, teamID int64, discussionNumber int, content string) (*Reaction, *Response, error) {
u := fmt.Sprintf("teams/%v/discussions/%v/reactions", teamID, discussionNumber)
body := &Reaction{Content: String(content)}
req, err := s.client.NewRequest("POST", u, body)
if err != nil {
return nil, nil, err
}
req.Header.Set("Accept", mediaTypeReactionsPreview)
m := &Reaction{}
resp, err := s.client.Do(ctx, req, m)
if err != nil {
return nil, resp, err
}
return m, resp, nil
}
// ListTeamDiscussionCommentReactions lists the reactions for a team discussion comment.
//
// GitHub API docs: https://developer.github.com/v3/reactions/#list-reactions-for-a-team-discussion-comment
func (s *ReactionsService) ListTeamDiscussionCommentReactions(ctx context.Context, teamID int64, discussionNumber, commentNumber int, opt *ListOptions) ([]*Reaction, *Response, error) {
u := fmt.Sprintf("teams/%v/discussions/%v/comments/%v/reactions", teamID, discussionNumber, commentNumber)
u, err := addOptions(u, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}
req.Header.Set("Accept", mediaTypeReactionsPreview)
var m []*Reaction
resp, err := s.client.Do(ctx, req, &m)
return m, resp, nil
}
// CreateTeamDiscussionCommentReaction creates a reaction for a team discussion comment.
// The content should have one of the following values: "+1", "-1", "laugh", "confused", "heart", "hooray".
//
// GitHub API docs: https://developer.github.com/v3/reactions/#create-reaction-for-a-team-discussion-comment
func (s *ReactionsService) CreateTeamDiscussionCommentReaction(ctx context.Context, teamID int64, discussionNumber, commentNumber int, content string) (*Reaction, *Response, error) {
u := fmt.Sprintf("teams/%v/discussions/%v/comments/%v/reactions", teamID, discussionNumber, commentNumber)
body := &Reaction{Content: String(content)}
req, err := s.client.NewRequest("POST", u, body)
if err != nil {
return nil, nil, err
}
req.Header.Set("Accept", mediaTypeReactionsPreview)
m := &Reaction{}
resp, err := s.client.Do(ctx, req, m)
if err != nil {
return nil, resp, err
}
return m, resp, nil
}
// DeleteReaction deletes a reaction.
//
// GitHub API docs: https://developer.github.com/v3/reaction/reactions/#delete-a-reaction-archive

View File

@ -56,6 +56,7 @@ type Repository struct {
AllowSquashMerge *bool `json:"allow_squash_merge,omitempty"`
AllowMergeCommit *bool `json:"allow_merge_commit,omitempty"`
Topics []string `json:"topics,omitempty"`
Archived *bool `json:"archived,omitempty"`
// Only provided when using RepositoriesService.Get while in preview
License *License `json:"license,omitempty"`
@ -69,7 +70,6 @@ type Repository struct {
HasDownloads *bool `json:"has_downloads,omitempty"`
LicenseTemplate *string `json:"license_template,omitempty"`
GitignoreTemplate *string `json:"gitignore_template,omitempty"`
Archived *bool `json:"archived,omitempty"`
// Creating an organization repository. Required for non-owners.
TeamID *int64 `json:"team_id,omitempty"`
@ -259,10 +259,40 @@ func (s *RepositoriesService) ListAll(ctx context.Context, opt *RepositoryListAl
return repos, resp, nil
}
// createRepoRequest is a subset of Repository and is used internally
// by Create to pass only the known fields for the endpoint.
//
// See https://github.com/google/go-github/issues/1014 for more
// information.
type createRepoRequest struct {
// Name is required when creating a repo.
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Homepage *string `json:"homepage,omitempty"`
Private *bool `json:"private,omitempty"`
HasIssues *bool `json:"has_issues,omitempty"`
HasProjects *bool `json:"has_projects,omitempty"`
HasWiki *bool `json:"has_wiki,omitempty"`
// Creating an organization repository. Required for non-owners.
TeamID *int64 `json:"team_id,omitempty"`
AutoInit *bool `json:"auto_init,omitempty"`
GitignoreTemplate *string `json:"gitignore_template,omitempty"`
LicenseTemplate *string `json:"license_template,omitempty"`
AllowSquashMerge *bool `json:"allow_squash_merge,omitempty"`
AllowMergeCommit *bool `json:"allow_merge_commit,omitempty"`
AllowRebaseMerge *bool `json:"allow_rebase_merge,omitempty"`
}
// Create a new repository. If an organization is specified, the new
// repository will be created under that org. If the empty string is
// specified, it will be created for the authenticated user.
//
// Note that only a subset of the repo fields are used and repo must
// not be nil.
//
// GitHub API docs: https://developer.github.com/v3/repos/#create
func (s *RepositoriesService) Create(ctx context.Context, org string, repo *Repository) (*Repository, *Response, error) {
var u string
@ -272,7 +302,24 @@ func (s *RepositoriesService) Create(ctx context.Context, org string, repo *Repo
u = "user/repos"
}
req, err := s.client.NewRequest("POST", u, repo)
repoReq := &createRepoRequest{
Name: repo.Name,
Description: repo.Description,
Homepage: repo.Homepage,
Private: repo.Private,
HasIssues: repo.HasIssues,
HasProjects: repo.HasProjects,
HasWiki: repo.HasWiki,
TeamID: repo.TeamID,
AutoInit: repo.AutoInit,
GitignoreTemplate: repo.GitignoreTemplate,
LicenseTemplate: repo.LicenseTemplate,
AllowSquashMerge: repo.AllowSquashMerge,
AllowMergeCommit: repo.AllowMergeCommit,
AllowRebaseMerge: repo.AllowRebaseMerge,
}
req, err := s.client.NewRequest("POST", u, repoReq)
if err != nil {
return nil, nil, err
}

View File

@ -8,6 +8,8 @@ package github
import (
"context"
"fmt"
"encoding/json"
)
// RepositoryListForksOptions specifies the optional parameters to the
@ -78,10 +80,15 @@ func (s *RepositoriesService) CreateFork(ctx context.Context, owner, repo string
fork := new(Repository)
resp, err := s.client.Do(ctx, req, fork)
if _, ok := err.(*AcceptedError); ok {
if err != nil {
// Persist AcceptedError's metadata to the Repository object.
if aerr, ok := err.(*AcceptedError); ok {
if err := json.Unmarshal(aerr.Raw, fork); err != nil {
return fork, resp, err
}
return fork, resp, err
}
if err != nil {
return nil, resp, err
}

View File

@ -71,25 +71,49 @@ func (w WebHookAuthor) String() string {
type Hook struct {
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Name *string `json:"name,omitempty"`
URL *string `json:"url,omitempty"`
ID *int64 `json:"id,omitempty"`
// Only the following fields are used when creating a hook.
// Config is required.
Config map[string]interface{} `json:"config,omitempty"`
Events []string `json:"events,omitempty"`
Active *bool `json:"active,omitempty"`
Config map[string]interface{} `json:"config,omitempty"`
ID *int64 `json:"id,omitempty"`
}
func (h Hook) String() string {
return Stringify(h)
}
// createHookRequest is a subset of Hook and is used internally
// by CreateHook to pass only the known fields for the endpoint.
//
// See https://github.com/google/go-github/issues/1015 for more
// information.
type createHookRequest struct {
// Config is required.
Config map[string]interface{} `json:"config,omitempty"`
Events []string `json:"events,omitempty"`
Active *bool `json:"active,omitempty"`
}
// CreateHook creates a Hook for the specified repository.
// Name and Config are required fields.
// Config is a required field.
//
// Note that only a subset of the hook fields are used and hook must
// not be nil.
//
// GitHub API docs: https://developer.github.com/v3/repos/hooks/#create-a-hook
func (s *RepositoriesService) CreateHook(ctx context.Context, owner, repo string, hook *Hook) (*Hook, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/hooks", owner, repo)
req, err := s.client.NewRequest("POST", u, hook)
hookReq := &createHookRequest{
Events: hook.Events,
Active: hook.Active,
Config: hook.Config,
}
req, err := s.client.NewRequest("POST", u, hookReq)
if err != nil {
return nil, nil, err
}

View File

@ -19,13 +19,15 @@ import (
// RepositoryRelease represents a GitHub release in a repository.
type RepositoryRelease struct {
ID *int64 `json:"id,omitempty"`
TagName *string `json:"tag_name,omitempty"`
TargetCommitish *string `json:"target_commitish,omitempty"`
Name *string `json:"name,omitempty"`
Body *string `json:"body,omitempty"`
Draft *bool `json:"draft,omitempty"`
Prerelease *bool `json:"prerelease,omitempty"`
// The following fields are not used in CreateRelease or EditRelease:
ID *int64 `json:"id,omitempty"`
CreatedAt *Timestamp `json:"created_at,omitempty"`
PublishedAt *Timestamp `json:"published_at,omitempty"`
URL *string `json:"url,omitempty"`
@ -125,13 +127,40 @@ func (s *RepositoriesService) getSingleRelease(ctx context.Context, url string)
return release, resp, nil
}
// repositoryReleaseRequest is a subset of RepositoryRelease and
// is used internally by CreateRelease and EditRelease to pass
// only the known fields for these endpoints.
//
// See https://github.com/google/go-github/issues/992 for more
// information.
type repositoryReleaseRequest struct {
TagName *string `json:"tag_name,omitempty"`
TargetCommitish *string `json:"target_commitish,omitempty"`
Name *string `json:"name,omitempty"`
Body *string `json:"body,omitempty"`
Draft *bool `json:"draft,omitempty"`
Prerelease *bool `json:"prerelease,omitempty"`
}
// CreateRelease adds a new release for a repository.
//
// Note that only a subset of the release fields are used.
// See RepositoryRelease for more information.
//
// GitHub API docs: https://developer.github.com/v3/repos/releases/#create-a-release
func (s *RepositoriesService) CreateRelease(ctx context.Context, owner, repo string, release *RepositoryRelease) (*RepositoryRelease, *Response, error) {
u := fmt.Sprintf("repos/%s/%s/releases", owner, repo)
req, err := s.client.NewRequest("POST", u, release)
releaseReq := &repositoryReleaseRequest{
TagName: release.TagName,
TargetCommitish: release.TargetCommitish,
Name: release.Name,
Body: release.Body,
Draft: release.Draft,
Prerelease: release.Prerelease,
}
req, err := s.client.NewRequest("POST", u, releaseReq)
if err != nil {
return nil, nil, err
}
@ -146,11 +175,23 @@ func (s *RepositoriesService) CreateRelease(ctx context.Context, owner, repo str
// EditRelease edits a repository release.
//
// Note that only a subset of the release fields are used.
// See RepositoryRelease for more information.
//
// GitHub API docs: https://developer.github.com/v3/repos/releases/#edit-a-release
func (s *RepositoriesService) EditRelease(ctx context.Context, owner, repo string, id int64, release *RepositoryRelease) (*RepositoryRelease, *Response, error) {
u := fmt.Sprintf("repos/%s/%s/releases/%d", owner, repo, id)
req, err := s.client.NewRequest("PATCH", u, release)
releaseReq := &repositoryReleaseRequest{
TagName: release.TagName,
TargetCommitish: release.TargetCommitish,
Name: release.Name,
Body: release.Body,
Draft: release.Draft,
Prerelease: release.Prerelease,
}
req, err := s.client.NewRequest("PATCH", u, releaseReq)
if err != nil {
return nil, nil, err
}

View File

@ -24,6 +24,7 @@ type DiscussionComment struct {
Number *int `json:"number,omitempty"`
UpdatedAt *Timestamp `json:"updated_at,omitempty"`
URL *string `json:"url,omitempty"`
Reactions *Reactions `json:"reactions,omitempty"`
}
func (c DiscussionComment) String() string {

View File

@ -29,6 +29,7 @@ type TeamDiscussion struct {
Title *string `json:"title,omitempty"`
UpdatedAt *Timestamp `json:"updated_at,omitempty"`
URL *string `json:"url,omitempty"`
Reactions *Reactions `json:"reactions,omitempty"`
}
func (d TeamDiscussion) String() string {

View File

@ -76,6 +76,7 @@ func (u User) String() string {
// user.
//
// GitHub API docs: https://developer.github.com/v3/users/#get-a-single-user
// and: https://developer.github.com/v3/users/#get-the-authenticated-user
func (s *UsersService) Get(ctx context.Context, user string) (*User, *Response, error) {
var u string
if user != "" {

View File

@ -498,7 +498,7 @@ func Set(cfg interface{}, path string, value interface{}) error {
default:
return fmt.Errorf(
"Invalid type at %q: expected []interface{} or map[string]interface{}; got %T",
strings.Join(parts[:pos+1], "."), cfg)
strings.Join(parts[:pos+1], "."), c)
}
}

View File

@ -65,6 +65,8 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio
(There are no corresponding tags in the project. I only keep such a history in this README.)
- v0.18 (2018-10-18)
- `InputField` elements can now be navigated freely.
- v0.17 (2018-06-20)
- Added `TreeView`.
- v0.15 (2018-05-02)

View File

@ -19,6 +19,9 @@ type Application struct {
// The application's screen.
screen tcell.Screen
// Indicates whether the application's screen is currently active.
running bool
// The primitive which currently has the keyboard focus.
focus Primitive
@ -70,22 +73,53 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event
return a.inputCapture
}
// SetScreen allows you to provide your own tcell.Screen object. For most
// applications, this is not needed and you should be familiar with
// tcell.Screen when using this function. Run() will call Init() and Fini() on
// the provided screen object.
//
// This function is typically called before calling Run(). Calling it while an
// application is running will switch the application to the new screen. Fini()
// will be called on the old screen and Init() on the new screen (errors
// returned by Init() will lead to a panic).
//
// Note that calling Suspend() will invoke Fini() on your screen object and it
// will not be restored when suspended mode ends. Instead, a new default screen
// object will be created.
func (a *Application) SetScreen(screen tcell.Screen) *Application {
a.Lock()
defer a.Unlock()
if a.running {
a.screen.Fini()
}
a.screen = screen
if a.running {
if err := a.screen.Init(); err != nil {
panic(err)
}
}
return a
}
// Run starts the application and thus the event loop. This function returns
// when Stop() was called.
func (a *Application) Run() error {
var err error
a.Lock()
// Make a screen.
// Make a screen if there is none yet.
if a.screen == nil {
a.screen, err = tcell.NewScreen()
if err != nil {
a.Unlock()
return err
}
}
if err = a.screen.Init(); err != nil {
a.Unlock()
return err
}
a.running = true
// We catch panics to clean up because they mess up the terminal.
defer func() {
@ -93,6 +127,7 @@ func (a *Application) Run() error {
if a.screen != nil {
a.screen.Fini()
}
a.running = false
panic(p)
}
}()
@ -170,6 +205,7 @@ func (a *Application) Stop() {
}
a.screen.Fini()
a.screen = nil
a.running = false
}
// Suspend temporarily suspends the application by exiting terminal UI mode and
@ -217,6 +253,7 @@ func (a *Application) Suspend(f func()) bool {
a.Unlock()
panic(err)
}
a.running = true
a.Unlock()
a.Draw()

12
vendor/github.com/rivo/tview/box.go generated vendored
View File

@ -51,10 +51,6 @@ type Box struct {
// Whether or not this box has focus.
hasFocus bool
// If set to true, the inner rect of this box will be within the screen at the
// last time the box was drawn.
clampToScreen bool
// An optional capture function which receives a key event and returns the
// event to be forwarded to the primitive's default input handler (nil if
// nothing should be forwarded).
@ -74,7 +70,6 @@ func NewBox() *Box {
borderColor: Styles.BorderColor,
titleColor: Styles.TitleColor,
titleAlign: AlignCenter,
clampToScreen: true,
}
b.focus = b
return b
@ -117,6 +112,7 @@ func (b *Box) SetRect(x, y, width, height int) {
b.y = y
b.width = width
b.height = height
b.innerX = -1 // Mark inner rect as uninitialized.
}
// SetDrawFunc sets a callback function which is invoked after the box primitive
@ -277,8 +273,8 @@ func (b *Box) Draw(screen tcell.Screen) {
// Draw title.
if b.title != "" && b.width >= 4 {
_, printed := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
if StringWidth(b.title)-printed > 0 && printed > 0 {
printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
if len(b.title)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(b.x+b.width-2, b.y)
fg, _, _ := style.Decompose()
Print(screen, string(SemigraphicsHorizontalEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg)
@ -296,7 +292,6 @@ func (b *Box) Draw(screen tcell.Screen) {
}
// Clamp inner rect to screen.
if b.clampToScreen {
width, height := screen.Size()
if b.innerX < 0 {
b.innerWidth += b.innerX
@ -313,7 +308,6 @@ func (b *Box) Draw(screen tcell.Screen) {
b.innerY = 0
}
}
}
// Focus is called when this primitive receives focus.
func (b *Box) Focus(delegate func(p Primitive)) {

View File

@ -124,9 +124,9 @@ func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
return c
}
// SetDoneFunc sets a handler which is called when the user is done entering
// text. The callback function is provided with the key that was pressed, which
// is one of the following:
// SetDoneFunc sets a handler which is called when the user is done using the
// checkbox. The callback function is provided with the key that was pressed,
// which is one of the following:
//
// - KeyEscape: Abort text input.
// - KeyTab: Move to the next field.

12
vendor/github.com/rivo/tview/form.go generated vendored
View File

@ -218,6 +218,13 @@ func (f *Form) AddButton(label string, selected func()) *Form {
return f
}
// GetButton returns the button at the specified 0-based index. Note that
// buttons have been specially prepared for this form and modifying some of
// their attributes may have unintended side effects.
func (f *Form) GetButton(index int) *Button {
return f.buttons[index]
}
// RemoveButton removes the button at the specified position, starting with 0
// for the button that was added first.
func (f *Form) RemoveButton(index int) *Form {
@ -225,6 +232,11 @@ func (f *Form) RemoveButton(index int) *Form {
return f
}
// GetButtonCount returns the number of buttons in this form.
func (f *Form) GetButtonCount() int {
return len(f.buttons)
}
// GetButtonIndex returns the index of the button with the given label, starting
// with 0 for the button that was added first. If no such label was found, -1
// is returned.

View File

@ -11,10 +11,23 @@ import (
)
// InputField is a one-line box (three lines if there is a title) where the
// user can enter text.
// user can enter text. Use SetAcceptanceFunc() to accept or reject input,
// SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input
// from onlookers (e.g. for password input).
//
// Use SetMaskCharacter() to hide input from onlookers (e.g. for password
// input).
// The following keys can be used for navigation and editing:
//
// - Left arrow: Move left by one character.
// - Right arrow: Move right by one character.
// - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
// - End, Ctrl-E, Alt-e: Move to the end of the line.
// - Alt-left, Alt-b: Move left by one word.
// - Alt-right, Alt-f: Move right by one word.
// - Backspace: Delete the character before the cursor.
// - Delete: Delete the character after the cursor.
// - Ctrl-K: Delete from the cursor to the end of the line.
// - Ctrl-W: Delete the last word before the cursor.
// - Ctrl-U: Delete the entire line.
//
// See https://github.com/rivo/tview/wiki/InputField for an example.
type InputField struct {
@ -53,6 +66,12 @@ type InputField struct {
// disables masking.
maskCharacter rune
// The cursor position as a byte index into the text string.
cursorPos int
// The number of bytes of the text string skipped ahead while drawing.
offset int
// An optional function which may reject the last character that was entered.
accept func(text string, ch rune) bool
@ -174,7 +193,7 @@ func (i *InputField) SetMaskCharacter(mask rune) *InputField {
// SetAcceptanceFunc sets a handler which may reject the last character that was
// entered (by returning false).
//
// This package defines a number of variables Prefixed with InputField which may
// This package defines a number of variables prefixed with InputField which may
// be used for common input (e.g. numbers, maximum text length).
func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField {
i.accept = handler
@ -244,56 +263,69 @@ func (i *InputField) Draw(screen tcell.Screen) {
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
}
// Draw placeholder text.
// Text.
var cursorScreenPos int
text := i.text
if text == "" && i.placeholder != "" {
Print(screen, i.placeholder, x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
// Draw placeholder text.
Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
i.offset = 0
} else {
// Draw entered text.
if i.maskCharacter > 0 {
text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
} else {
text = Escape(text)
}
fieldWidth-- // We need one cell for the cursor.
if fieldWidth < runewidth.StringWidth(text) {
Print(screen, text, x, y, fieldWidth, AlignRight, i.fieldTextColor)
stringWidth := runewidth.StringWidth(text)
if fieldWidth >= stringWidth {
// We have enough space for the full text.
Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
i.offset = 0
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
if textPos >= i.cursorPos {
return true
}
cursorScreenPos += screenWidth
return false
})
} else {
Print(screen, text, x, y, fieldWidth, AlignLeft, i.fieldTextColor)
// The text doesn't fit. Where is the cursor?
if i.cursorPos < 0 {
i.cursorPos = 0
} else if i.cursorPos > len(text) {
i.cursorPos = len(text)
}
// Shift the text so the cursor is inside the field.
var shiftLeft int
if i.offset > i.cursorPos {
i.offset = i.cursorPos
} else if subWidth := runewidth.StringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 {
shiftLeft = subWidth - fieldWidth + 1
}
currentOffset := i.offset
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
if textPos >= currentOffset {
if shiftLeft > 0 {
i.offset = textPos + textWidth
shiftLeft -= screenWidth
} else {
if textPos+textWidth > i.cursorPos {
return true
}
cursorScreenPos += screenWidth
}
}
return false
})
Print(screen, Escape(text[i.offset:]), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
}
}
// Set cursor.
if i.focus.HasFocus() {
i.setCursor(screen)
screen.ShowCursor(x+cursorScreenPos, y)
}
}
// setCursor sets the cursor position.
func (i *InputField) setCursor(screen tcell.Screen) {
x := i.x
y := i.y
rightLimit := x + i.width
if i.border {
x++
y++
rightLimit -= 2
}
fieldWidth := runewidth.StringWidth(i.text)
if i.fieldWidth > 0 && fieldWidth > i.fieldWidth-1 {
fieldWidth = i.fieldWidth - 1
}
if i.labelWidth > 0 {
x += i.labelWidth + fieldWidth
} else {
x += StringWidth(i.label) + fieldWidth
}
if x >= rightLimit {
x = rightLimit - 1
}
screen.ShowCursor(x, y)
}
// InputHandler returns the handler for this primitive.
func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
@ -305,27 +337,95 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
}
}()
// Movement functions.
home := func() { i.cursorPos = 0 }
end := func() { i.cursorPos = len(i.text) }
moveLeft := func() {
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.cursorPos -= textWidth
return true
})
}
moveRight := func() {
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.cursorPos += textWidth
return true
})
}
moveWordLeft := func() {
i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], ""))
}
moveWordRight := func() {
i.cursorPos = len(i.text) - len(regexp.MustCompile(`^\s*\S+\s*`).ReplaceAllString(i.text[i.cursorPos:], ""))
}
// Process key event.
switch key := event.Key(); key {
case tcell.KeyRune: // Regular character.
newText := i.text + string(event.Rune())
modifiers := event.Modifiers()
if modifiers == tcell.ModNone {
ch := string(event.Rune())
newText := i.text[:i.cursorPos] + ch + i.text[i.cursorPos:]
if i.accept != nil {
if !i.accept(newText, event.Rune()) {
break
}
}
i.text = newText
i.cursorPos += len(ch)
} else if modifiers&tcell.ModAlt > 0 {
// We accept some Alt- key combinations.
switch event.Rune() {
case 'a': // Home.
home()
case 'e': // End.
end()
case 'b': // Move word left.
moveWordLeft()
case 'f': // Move word right.
moveWordRight()
}
}
case tcell.KeyCtrlU: // Delete all.
i.text = ""
i.cursorPos = 0
case tcell.KeyCtrlK: // Delete until the end of the line.
i.text = i.text[:i.cursorPos]
case tcell.KeyCtrlW: // Delete last word.
lastWord := regexp.MustCompile(`\s*\S+\s*$`)
i.text = lastWord.ReplaceAllString(i.text, "")
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character.
if len(i.text) == 0 {
break
lastWord := regexp.MustCompile(`\S+\s*$`)
newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:]
i.cursorPos -= len(i.text) - len(newText)
i.text = newText
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor.
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.text = i.text[:textPos] + i.text[textPos+textWidth:]
i.cursorPos -= textWidth
return true
})
if i.offset >= i.cursorPos {
i.offset = 0
}
runes := []rune(i.text)
i.text = string(runes[:len(runes)-1])
case tcell.KeyDelete: // Delete character after the cursor.
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
return true
})
case tcell.KeyLeft:
if event.Modifiers()&tcell.ModAlt > 0 {
moveWordLeft()
} else {
moveLeft()
}
case tcell.KeyRight:
if event.Modifiers()&tcell.ModAlt > 0 {
moveWordRight()
} else {
moveRight()
}
case tcell.KeyHome, tcell.KeyCtrlA:
home()
case tcell.KeyEnd, tcell.KeyCtrlE:
end()
case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done.
if i.done != nil {
i.done(key)

15
vendor/github.com/rivo/tview/list.go generated vendored
View File

@ -85,6 +85,19 @@ func (l *List) GetCurrentItem() int {
return l.currentItem
}
// RemoveItem removes the item with the given index (starting at 0) from the
// list. Does nothing if the index is out of range.
func (l *List) RemoveItem(index int) *List {
if index < 0 || index >= len(l.items) {
return l
}
l.items = append(l.items[:index], l.items[index+1:]...)
if l.currentItem >= len(l.items) {
l.currentItem = len(l.items) - 1
}
return l
}
// SetMainTextColor sets the color of the items' main text.
func (l *List) SetMainTextColor(color tcell.Color) *List {
l.mainTextColor = color
@ -127,7 +140,7 @@ func (l *List) ShowSecondaryText(show bool) *List {
//
// This function is also called when the first item is added or when
// SetCurrentItem() is called.
func (l *List) SetChangedFunc(handler func(int, string, string, rune)) *List {
func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List {
l.changed = handler
return l
}

View File

@ -86,6 +86,16 @@ func (m *Modal) AddButtons(labels []string) *Modal {
m.done(i, l)
}
})
button := m.form.GetButton(m.form.GetButtonCount() - 1)
button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyDown, tcell.KeyRight:
return tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone)
case tcell.KeyUp, tcell.KeyLeft:
return tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone)
}
return event
})
}(index, label)
}
return m

View File

@ -231,6 +231,10 @@ type Table struct {
// The number of visible rows the last time the table was drawn.
visibleRows int
// The style of the selected rows. If this value is 0, selected rows are
// simply inverted.
selectedStyle tcell.Style
// An optional function which gets called when the user presses Enter on a
// selected cell. If entire rows selected, the column value is undefined.
// Likewise for entire columns.
@ -276,6 +280,18 @@ func (t *Table) SetBordersColor(color tcell.Color) *Table {
return t
}
// SetSelectedStyle sets a specific style for selected cells. If no such style
// is set, per default, selected cells are inverted (i.e. their foreground and
// background colors are swapped).
//
// To reset a previous setting to its default, make the following call:
//
// table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, 0)
func (t *Table) SetSelectedStyle(foregroundColor, backgroundColor tcell.Color, attributes tcell.AttrMask) *Table {
t.selectedStyle = tcell.StyleDefault.Foreground(foregroundColor).Background(backgroundColor) | tcell.Style(attributes)
return t
}
// SetSeparator sets the character used to fill the space between two
// neighboring cells. This is a space character ' ' per default but you may
// want to set it to Borders.Vertical (or any other rune) if the column
@ -743,12 +759,17 @@ ColumnLoop:
}
// Helper function which colors the background of a box.
colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, selected bool) {
// backgroundColor == tcell.ColorDefault => Don't color the background.
// textColor == tcell.ColorDefault => Don't change the text color.
// attr == 0 => Don't change attributes.
// invert == true => Ignore attr, set text to backgroundColor or t.backgroundColor;
// set background to textColor.
colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, attr tcell.AttrMask, invert bool) {
for by := 0; by < h && fromY+by < y+height; by++ {
for bx := 0; bx < w && fromX+bx < x+width; bx++ {
m, c, style, _ := screen.GetContent(fromX+bx, fromY+by)
if selected {
fg, _, _ := style.Decompose()
fg, bg, a := style.Decompose()
if invert {
if fg == textColor || fg == t.bordersColor {
fg = backgroundColor
}
@ -757,10 +778,16 @@ ColumnLoop:
}
style = style.Background(textColor).Foreground(fg)
} else {
if backgroundColor == tcell.ColorDefault {
continue
if backgroundColor != tcell.ColorDefault {
bg = backgroundColor
}
style = style.Background(backgroundColor)
if textColor != tcell.ColorDefault {
fg = textColor
}
if attr != 0 {
a = attr
}
style = style.Background(bg).Foreground(fg) | tcell.Style(a)
}
screen.SetContent(fromX+bx, fromY+by, m, c, style)
}
@ -769,11 +796,12 @@ ColumnLoop:
// Color the cell backgrounds. To avoid undesirable artefacts, we combine
// the drawing of a cell by background color, selected cells last.
cellsByBackgroundColor := make(map[tcell.Color][]*struct {
type cellInfo struct {
x, y, w, h int
text tcell.Color
selected bool
})
}
cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo)
var backgroundColors []tcell.Color
for rowY, row := range rows {
columnX := 0
@ -793,11 +821,7 @@ ColumnLoop:
columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn
cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow)
entries, ok := cellsByBackgroundColor[cell.BackgroundColor]
cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &struct {
x, y, w, h int
text tcell.Color
selected bool
}{
cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &cellInfo{
x: bx,
y: by,
w: bw,
@ -821,13 +845,18 @@ ColumnLoop:
_, _, lj := c.Hcl()
return li < lj
})
selFg, selBg, selAttr := t.selectedStyle.Decompose()
for _, bgColor := range backgroundColors {
entries := cellsByBackgroundColor[bgColor]
for _, cell := range entries {
if cell.selected {
defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, true)
if t.selectedStyle != 0 {
defer colorBackground(cell.x, cell.y, cell.w, cell.h, selBg, selFg, selAttr, false)
} else {
colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, false)
defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, 0, true)
}
} else {
colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, tcell.ColorDefault, 0, false)
}
}
}

View File

@ -5,7 +5,6 @@ import (
"fmt"
"regexp"
"sync"
"unicode"
"unicode/utf8"
"github.com/gdamore/tcell"
@ -549,12 +548,6 @@ func (t *TextView) reindexBuffer(width int) {
strippedStr = regionPattern.ReplaceAllString(strippedStr, "")
}
// Find all escape tags in this line. Escape them.
if t.dynamicColors || t.regions {
escapeIndices = escapePattern.FindAllStringIndex(str, -1)
strippedStr = escapePattern.ReplaceAllString(strippedStr, "[$1$2]")
}
// We don't need the original string anymore for now.
str = strippedStr
@ -810,8 +803,9 @@ func (t *TextView) Draw(screen tcell.Screen) {
colorTags [][]string
escapeIndices [][]int
)
strippedText := text
if t.dynamicColors {
colorTagIndices, colorTags, escapeIndices, _, _ = decomposeString(text)
colorTagIndices, colorTags, escapeIndices, strippedText, _ = decomposeString(text)
}
// Get regions.
@ -822,8 +816,10 @@ func (t *TextView) Draw(screen tcell.Screen) {
if t.regions {
regionIndices = regionPattern.FindAllStringIndex(text, -1)
regions = regionPattern.FindAllStringSubmatch(text, -1)
strippedText = regionPattern.ReplaceAllString(strippedText, "")
if !t.dynamicColors {
escapeIndices = escapePattern.FindAllStringIndex(text, -1)
strippedText = string(escapePattern.ReplaceAllString(strippedText, "[$1$2]"))
}
}
@ -842,11 +838,26 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
// Print the line.
var currentTag, currentRegion, currentEscapeTag, skipped, runeSeqWidth int
runeSequence := make([]rune, 0, 10)
flush := func() {
if len(runeSequence) == 0 {
return
var colorPos, regionPos, escapePos, tagOffset, skipped int
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
// Get the color.
if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
colorPos++
}
// Get the region.
if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
regionID = regions[regionPos][1]
tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
regionPos++
}
// Skip the second-to-last character of an escape tag.
if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
}
// Mix the existing style with the new style.
@ -876,87 +887,30 @@ func (t *TextView) Draw(screen tcell.Screen) {
style = style.Background(fg).Foreground(bg)
}
// Draw the character.
var comb []rune
if len(runeSequence) > 1 && !unicode.IsControl(runeSequence[1]) {
// Allocate space for the combining characters only when necessary.
comb = make([]rune, len(runeSequence)-1)
copy(comb, runeSequence[1:])
}
for offset := 0; offset < runeSeqWidth; offset++ {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, runeSequence[0], comb, style)
}
// Advance.
posX += runeSeqWidth
runeSequence = runeSequence[:0]
runeSeqWidth = 0
}
for pos, ch := range text {
// Get the color.
if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
flush()
if pos == colorTagIndices[currentTag][1]-1 {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[currentTag])
currentTag++
}
continue
}
// Get the region.
if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
flush()
if pos == regionIndices[currentRegion][1]-1 {
regionID = regions[currentRegion][1]
currentRegion++
}
continue
}
// Skip the second-to-last character of an escape tag.
if currentEscapeTag < len(escapeIndices) && pos >= escapeIndices[currentEscapeTag][0] && pos < escapeIndices[currentEscapeTag][1] {
flush()
if pos == escapeIndices[currentEscapeTag][1]-1 {
currentEscapeTag++
} else if pos == escapeIndices[currentEscapeTag][1]-2 {
continue
}
}
// Determine the width of this rune.
chWidth := runewidth.RuneWidth(ch)
if chWidth == 0 {
// If this is not a modifier, we treat it as a space character.
if len(runeSequence) == 0 {
ch = ' '
chWidth = 1
} else {
runeSequence = append(runeSequence, ch)
continue
}
}
// Skip to the right.
if !t.wrap && skipped < skip {
skipped += chWidth
continue
skipped += screenWidth
return false
}
// Stop at the right border.
if posX+runeSeqWidth+chWidth > width {
break
if posX+screenWidth >= width {
return true
}
// Flush the rune sequence.
flush()
// Draw the character.
for offset := screenWidth - 1; offset >= 0; offset-- {
if offset == 0 {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style)
} else {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style)
}
}
// Queue this rune.
runeSequence = append(runeSequence, ch)
runeSeqWidth += chWidth
}
if posX+runeSeqWidth <= width {
flush()
}
// Advance.
posX += screenWidth
return false
})
}
// If this view is not scrollable, we'll purge the buffer of lines that have

528
vendor/github.com/rivo/tview/util.go generated vendored
View File

@ -1,11 +1,9 @@
package tview
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
"unicode"
"github.com/gdamore/tcell"
@ -25,7 +23,7 @@ var (
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
boundaryPattern = regexp.MustCompile(`(([[:punct:]]|\n)[ \t\f\r]*|(\s+))`)
spacePattern = regexp.MustCompile(`\s+`)
)
@ -203,8 +201,8 @@ func decomposeString(text string) (colorIndices [][]int, colors [][]string, esca
// You can change the colors and text styles mid-text by inserting a color tag.
// See the package description for details.
//
// Returns the number of actual runes printed (not including color tags) and the
// actual width used for the printed runes.
// Returns the number of actual bytes of the text printed (including color tags)
// and the actual width used for the printed runes.
func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) {
return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color))
}
@ -217,115 +215,136 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
}
// Decompose the text.
colorIndices, colors, escapeIndices, strippedText, _ := decomposeString(text)
colorIndices, colors, escapeIndices, strippedText, strippedWidth := decomposeString(text)
// We deal with runes, not with bytes.
runes := []rune(strippedText)
// This helper function takes positions for a substring of "runes" and returns
// a new string corresponding to this substring, making sure printing that
// substring will observe color tags.
substring := func(from, to int) string {
// We want to reduce all alignments to AlignLeft.
if align == AlignRight {
if strippedWidth <= maxWidth {
// There's enough space for the entire text.
return printWithStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style)
}
// Trim characters off the beginning.
var (
colorPos, escapePos, runePos, startPos int
bytes, width, colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
if from >= len(runes) {
return ""
}
for pos := range text {
// Handle color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
if pos == colorIndices[colorPos][1]-1 {
if runePos <= from {
_, originalBackground, _ := style.Decompose()
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
// Update color/escape tag offset and style.
if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
}
style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
colorPos++
}
continue
}
// Handle escape tags.
if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
if pos == escapeIndices[escapePos][1]-1 {
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
tagOffset++
escapePos++
} else if pos == escapeIndices[escapePos][1]-2 {
continue
}
if strippedWidth-screenPos < maxWidth {
// We chopped off enough.
if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] {
// Unescape open escape sequences.
escapeCharPos := escapeIndices[escapePos-1][1] - 2
text = text[:escapeCharPos] + text[escapeCharPos+1:]
}
// Check boundaries.
if runePos == from {
startPos = pos
} else if runePos >= to {
return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:pos])
// Print and return.
bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style)
return true
}
runePos++
}
return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:])
}
// We want to reduce everything to AlignLeft.
if align == AlignRight {
width := 0
start := len(runes)
for index := start - 1; index >= 0; index-- {
w := runewidth.RuneWidth(runes[index])
if width+w > maxWidth {
break
}
width += w
start = index
}
for start < len(runes) && runewidth.RuneWidth(runes[start]) == 0 {
start++
}
return printWithStyle(screen, substring(start, len(runes)), x+maxWidth-width, y, width, AlignLeft, style)
return false
})
return bytes, width
} else if align == AlignCenter {
width := runewidth.StringWidth(strippedText)
if width == maxWidth {
if strippedWidth == maxWidth {
// Use the exact space.
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style)
} else if width < maxWidth {
} else if strippedWidth < maxWidth {
// We have more space than we need.
half := (maxWidth - width) / 2
half := (maxWidth - strippedWidth) / 2
return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style)
} else {
// Chop off runes until we have a perfect fit.
var choppedLeft, choppedRight, leftIndex, rightIndex int
rightIndex = len(runes) - 1
for rightIndex > leftIndex && width-choppedLeft-choppedRight > maxWidth {
rightIndex = len(strippedText)
for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth {
if choppedLeft < choppedRight {
leftWidth := runewidth.RuneWidth(runes[leftIndex])
choppedLeft += leftWidth
leftIndex++
for leftIndex < len(runes) && leftIndex < rightIndex && runewidth.RuneWidth(runes[leftIndex]) == 0 {
leftIndex++
}
// Iterate on the left by one character.
iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
choppedLeft += screenWidth
leftIndex += textWidth
return true
})
} else {
rightWidth := runewidth.RuneWidth(runes[rightIndex])
choppedRight += rightWidth
rightIndex--
// Iterate on the right by one character.
iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
choppedRight += screenWidth
rightIndex -= textWidth
return true
})
}
}
return printWithStyle(screen, substring(leftIndex, rightIndex), x, y, maxWidth, AlignLeft, style)
// Add tag offsets and determine start style.
var (
colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
_, originalBackground, _ := style.Decompose()
for index := range strippedText {
// We only need the offset of the left index.
if index > leftIndex {
// We're done.
if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] {
// Unescape open escape sequences.
escapeCharPos := escapeIndices[escapePos-1][1] - 2
text = text[:escapeCharPos] + text[escapeCharPos+1:]
}
break
}
// Update color/escape tag offset.
if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] {
if index <= leftIndex {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
}
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
colorPos++
}
if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] {
tagOffset++
escapePos++
}
}
return printWithStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style)
}
}
// Draw text.
drawn := 0
drawnWidth := 0
var (
colorPos, escapePos int
drawn, drawnWidth, colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
runeSequence := make([]rune, 0, 10)
runeSeqWidth := 0
flush := func() {
if len(runeSequence) == 0 {
return // Nothing to flush.
iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool {
// Only continue if there is still space.
if drawnWidth+screenWidth > maxWidth {
return true
}
// Handle color tags.
if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
colorPos++
}
// Handle scape tags.
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
}
}
// Print the rune sequence.
@ -333,69 +352,23 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
_, _, finalStyle, _ := screen.GetContent(finalX, y)
_, background, _ := finalStyle.Decompose()
finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes)
var comb []rune
if len(runeSequence) > 1 && !unicode.IsControl(runeSequence[1]) {
// Allocate space for the combining characters only when necessary.
comb = make([]rune, len(runeSequence)-1)
copy(comb, runeSequence[1:])
}
for offset := 0; offset < runeSeqWidth; offset++ {
// To avoid undesired effects, we place the same character in all cells.
screen.SetContent(finalX+offset, y, runeSequence[0], comb, finalStyle)
}
// Advance and reset.
drawn += len(runeSequence)
drawnWidth += runeSeqWidth
runeSequence = runeSequence[:0]
runeSeqWidth = 0
}
for pos, ch := range text {
// Handle color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
flush()
if pos == colorIndices[colorPos][1]-1 {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
colorPos++
}
continue
}
// Handle escape tags.
if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
flush()
if pos == escapeIndices[escapePos][1]-1 {
escapePos++
} else if pos == escapeIndices[escapePos][1]-2 {
continue
}
}
// Check if we have enough space for this rune.
chWidth := runewidth.RuneWidth(ch)
if drawnWidth+chWidth > maxWidth {
break // No. We're done then.
}
// Put this rune in the queue.
if chWidth == 0 {
// If this is not a modifier, we treat it as a space character.
if len(runeSequence) == 0 {
ch = ' '
chWidth = 1
}
for offset := screenWidth - 1; offset >= 0; offset-- {
// To avoid undesired effects, we populate all cells.
if offset == 0 {
screen.SetContent(finalX+offset, y, main, comb, finalStyle)
} else {
// We have a character. Flush all previous runes.
flush()
screen.SetContent(finalX+offset, y, ' ', nil, finalStyle)
}
runeSequence = append(runeSequence, ch)
runeSeqWidth += chWidth
}
if drawnWidth+runeSeqWidth <= maxWidth {
flush()
}
return drawn, drawnWidth
// Advance.
drawn += length
drawnWidth += screenWidth
return false
})
return drawn + tagOffset + len(escapeIndices), drawnWidth
}
// PrintSimple prints white text to the screen at the given position.
@ -421,102 +394,83 @@ func WordWrap(text string, width int) (lines []string) {
colorTagIndices, _, escapeIndices, strippedText, _ := decomposeString(text)
// Find candidate breakpoints.
breakPoints := boundaryPattern.FindAllStringIndex(strippedText, -1)
breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1)
// Results in one entry for each candidate. Each entry is an array a of
// indices into strippedText where a[6] < 0 for newline/punctuation matches
// and a[4] < 0 for whitespace matches.
// This helper function adds a new line to the result slice. The provided
// positions are in stripped index space.
addLine := func(from, to int) {
// Shift indices back to original index space.
var colorTagIndex, escapeIndex int
for colorTagIndex < len(colorTagIndices) && to >= colorTagIndices[colorTagIndex][0] ||
escapeIndex < len(escapeIndices) && to >= escapeIndices[escapeIndex][0] {
past := 0
if colorTagIndex < len(colorTagIndices) {
tagWidth := colorTagIndices[colorTagIndex][1] - colorTagIndices[colorTagIndex][0]
if colorTagIndices[colorTagIndex][0] < from {
from += tagWidth
to += tagWidth
colorTagIndex++
} else if colorTagIndices[colorTagIndex][0] < to {
to += tagWidth
colorTagIndex++
} else {
past++
}
} else {
past++
}
if escapeIndex < len(escapeIndices) {
tagWidth := escapeIndices[escapeIndex][1] - escapeIndices[escapeIndex][0]
if escapeIndices[escapeIndex][0] < from {
from += tagWidth
to += tagWidth
escapeIndex++
} else if escapeIndices[escapeIndex][0] < to {
to += tagWidth
escapeIndex++
} else {
past++
}
} else {
past++
}
if past == 2 {
break // All other indices are beyond the requested string.
// Process stripped text one character at a time.
var (
colorPos, escapePos, breakpointPos, tagOffset int
lastBreakpoint, lastContinuation, currentLineStart int
lineWidth, continuationWidth int
newlineBreakpoint bool
)
unescape := func(substr string, startIndex int) string {
// A helper function to unescape escaped tags.
for index := escapePos; index >= 0; index-- {
if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 {
pos := escapeIndices[index][1] - 2 - startIndex
return substr[:pos] + substr[pos+1:]
}
}
lines = append(lines, text[from:to])
return substr
}
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
// Handle colour tags.
if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
colorPos++
}
// Determine final breakpoints.
var start, lastEnd, newStart, breakPoint int
for {
// What's our candidate string?
var candidate string
if breakPoint < len(breakPoints) {
candidate = text[start:breakPoints[breakPoint][1]]
} else {
candidate = text[start:]
// Handle escape tags.
if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
}
candidate = strings.TrimRightFunc(candidate, unicode.IsSpace)
if runewidth.StringWidth(candidate) >= width {
// We're past the available width.
if lastEnd > start {
// Use the previous candidate.
addLine(start, lastEnd)
start = newStart
} else {
// We have no previous candidate. Make a hard break.
var lineWidth int
for index, ch := range text {
if index < start {
continue
// Check if a break is warranted.
afterContinuation := lastContinuation > 0 && textPos+tagOffset >= lastContinuation
noBreakpoint := lastContinuation == 0
beyondWidth := lineWidth > 0 && lineWidth > width
if beyondWidth && noBreakpoint {
// We need a hard break without a breakpoint.
lines = append(lines, unescape(text[currentLineStart:textPos+tagOffset], currentLineStart))
currentLineStart = textPos + tagOffset
lineWidth = continuationWidth
} else if afterContinuation && (beyondWidth || newlineBreakpoint) {
// Break at last breakpoint or at newline.
lines = append(lines, unescape(text[currentLineStart:lastBreakpoint], currentLineStart))
currentLineStart = lastContinuation
lineWidth = continuationWidth
lastBreakpoint, lastContinuation, newlineBreakpoint = 0, 0, false
}
chWidth := runewidth.RuneWidth(ch)
if lineWidth > 0 && lineWidth+chWidth >= width {
addLine(start, index)
start = index
break
// Is this a breakpoint?
if breakpointPos < len(breakpoints) && textPos == breakpoints[breakpointPos][0] {
// Yes, it is. Set up breakpoint infos depending on its type.
lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
lastContinuation = breakpoints[breakpointPos][1] + tagOffset
newlineBreakpoint = main == '\n'
if breakpoints[breakpointPos][6] < 0 && !newlineBreakpoint {
lastBreakpoint++ // Don't skip punctuation.
}
lineWidth += chWidth
}
}
} else {
// We haven't hit the right border yet.
if breakPoint >= len(breakPoints) {
// It's the last line. We're done.
if len(candidate) > 0 {
addLine(start, len(strippedText))
}
break
} else {
// We have a new candidate.
lastEnd = start + len(candidate)
newStart = breakPoints[breakPoint][1]
breakPoint++
breakpointPos++
}
// Once we hit the continuation point, we start buffering widths.
if textPos+tagOffset < lastContinuation {
continuationWidth = 0
}
lineWidth += screenWidth
continuationWidth += screenWidth
return false
})
// Flush the rest.
if currentLineStart < len(text) {
lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
}
return
@ -531,3 +485,121 @@ func WordWrap(text string, width int) (lines []string) {
func Escape(text string) string {
return nonEscapePattern.ReplaceAllString(text, "$1[]")
}
// iterateString iterates through the given string one printed character at a
// time. For each such character, the callback function is called with the
// Unicode code points of the character (the first rune and any combining runes
// which may be nil if there aren't any), the starting position (in bytes)
// within the original string, its length in bytes, the screen position of the
// character, and the screen width of it. The iteration stops if the callback
// returns true. This function returns true if the iteration was stopped before
// the last character.
func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
var (
runes []rune
lastZeroWidthJoiner bool
startIndex int
startPos int
pos int
)
// Helper function which invokes the callback.
flush := func(index int) bool {
var comb []rune
if len(runes) > 1 {
comb = runes[1:]
}
return callback(runes[0], comb, startIndex, index-startIndex, startPos, pos-startPos)
}
for index, r := range text {
if unicode.In(r, unicode.Lm, unicode.M) || r == '\u200d' {
lastZeroWidthJoiner = r == '\u200d'
} else {
// We have a rune that's not a modifier. It could be the beginning of a
// new character.
if !lastZeroWidthJoiner {
if len(runes) > 0 {
// It is. Invoke callback.
if flush(index) {
return true // We're done.
}
// Reset rune store.
runes = runes[:0]
startIndex = index
startPos = pos
}
pos += runewidth.RuneWidth(r)
} else {
lastZeroWidthJoiner = false
}
}
runes = append(runes, r)
}
// Flush any remaining runes.
if len(runes) > 0 {
flush(len(text))
}
return false
}
// iterateStringReverse iterates through the given string in reverse, starting
// from the end of the string, one printed character at a time. For each such
// character, the callback function is called with the Unicode code points of
// the character (the first rune and any combining runes which may be nil if
// there aren't any), the starting position (in bytes) within the original
// string, its length in bytes, the screen position of the character, and the
// screen width of it. The iteration stops if the callback returns true. This
// function returns true if the iteration was stopped before the last character.
func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
type runePos struct {
r rune
pos int // The byte position of the rune in the original string.
width int // The screen width of the rune.
mod bool // Modifier or zero-width-joiner.
}
// We use the following:
// len(text) >= number of runes in text.
// Put all runes into a runePos slice in reverse.
runesReverse := make([]runePos, len(text))
index := len(text) - 1
for pos, ch := range text {
runesReverse[index].r = ch
runesReverse[index].pos = pos
runesReverse[index].width = runewidth.RuneWidth(ch)
runesReverse[index].mod = unicode.In(ch, unicode.Lm, unicode.M) || ch == '\u200d'
index--
}
runesReverse = runesReverse[index+1:]
// Parse reverse runes.
var screenWidth int
buffer := make([]rune, len(text)) // We fill this up from the back so it's forward again.
bufferPos := len(text)
stringWidth := runewidth.StringWidth(text)
for index, r := range runesReverse {
// Put this rune into the buffer.
bufferPos--
buffer[bufferPos] = r.r
// Do we need to flush the buffer?
if r.pos == 0 || !r.mod && runesReverse[index+1].r != '\u200d' {
// Yes, invoke callback.
var comb []rune
if len(text)-bufferPos > 1 {
comb = buffer[bufferPos+1:]
}
if callback(r.r, comb, r.pos, len(text)-r.pos, stringWidth-screenWidth, r.width) {
return true
}
screenWidth += r.width
bufferPos = len(text)
}
}
return false
}

674
vendor/github.com/sticreations/spotigopher/LICENCE.md generated vendored Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -3,6 +3,10 @@ package spotigopher
import (
"errors"
"log"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/godbus/dbus"
)
@ -51,29 +55,44 @@ func getSpotifyBus() dbus.BusObject {
PlayPause sends a PlayPause Command on the DBus
*/
func (s *SpotifyClient) PlayPause() {
if runtime.GOOS == "darwin" {
sendDarwinAction("playpause")
} else {
sendAction("org.mpris.MediaPlayer2.Player.PlayPause")
}
}
/*
Next sends a Next Command on the DBus
*/
func (s *SpotifyClient) Next() {
if runtime.GOOS == "darwin" {
sendDarwinAction("next")
} else {
sendAction("org.mpris.MediaPlayer2.Player.Next")
}
}
/*
Previous sends a Previous Command on the DBus
*/
func (s *SpotifyClient) Previous() {
if runtime.GOOS == "darwin" {
sendDarwinAction("previous")
} else {
sendAction("org.mpris.MediaPlayer2.Player.Previous")
}
}
/*
Stop sends a Stop Command on the DBus
*/
func (s *SpotifyClient) Stop() {
if runtime.GOOS == "darwin" {
sendDarwinAction("stop")
} else {
sendAction("org.mpris.MediaPlayer2.Player.Stop")
}
}
/*
@ -82,6 +101,7 @@ GetInfo returns all Spotify related Information, when Spotify is running
func (s *SpotifyClient) GetInfo() (Info, error) {
info := Info{}
if runtime.GOOS == "linux" {
spotifyBus := getSpotifyBus()
props, err := spotifyBus.GetProperty("org.mpris.MediaPlayer2.Player.Metadata")
if err != nil {
@ -102,9 +122,76 @@ func (s *SpotifyClient) GetInfo() (Info, error) {
}
info.Status = status.Value().(string)
} else if runtime.GOOS == "darwin" {
info.TrackID = getDarwinInfo("trackid")
info.Artist = strings.Split(getDarwinInfo("artist"), ",")
info.Album = getDarwinInfo("album")
info.Title = getDarwinInfo("track")
tmpTrackNumber, err := strconv.ParseInt(getDarwinInfo("tracknumber"), 0, 32)
if err != nil {
log.Fatalf("Could not get track number: %s", err)
}
info.TrackNumber = int32(tmpTrackNumber)
info.URL = getDarwinInfo("url")
info.ArtworkURL = getDarwinInfo("artworkurl")
info.Status = getDarwinInfo("status")
}
return info, nil
}
func getDarwinInfo(infoType string) string {
args := []string{`-etell application "Spotify" to name of current track as string`}
switch infoType {
case "trackid":
args = []string{`-etell application "Spotify" to id of current track as string`}
case "artist":
args = []string{`-etell application "Spotify" to artist of current track as string`}
case "title":
args = []string{`-etell application "Spotify" to name of current track as string`}
case "album":
args = []string{`-etell application "Spotify" to album of current track as string`}
case "tracknumber":
args = []string{`-etell application "Spotify" to track number of current track as string`}
case "url":
args = []string{`-etell application "Spotify" to spotify url of current track as string`}
case "artworkurl":
args = []string{`-etell application "Spotify" to artwork url of current track as string`}
case "status":
args = []string{`-etell application "Spotify" to player state as string`}
default:
args = []string{`-etell application "Spotify" to name of current track as string`}
}
info, err := exec.Command("osascript", args...).Output()
if err != nil {
log.Fatalf("Could not get info: %s", infoType)
}
return strings.Trim(string(info), "\n")
}
func sendDarwinAction(action string) {
args := []string{`-etell application "Spotify" to pause`}
switch action {
case "play":
args = []string{`-etell application "Spotify" to play`}
case "pause":
args = []string{`-etell application "Spotify" to pause`}
case "stop":
args = []string{`-etell application "Spotify" to pause`}
case "playpause":
args = []string{`-etell application "Spotify" to playpause`}
case "next":
args = []string{`-etell application "Spotify" to next track`}
case "previous":
args = []string{`-etell application "Spotify" to previous track`}
default:
args = []string{`-etell application "Spotify" to pause`}
}
err := exec.Command("osascript", args...).Run()
if err != nil {
log.Fatalf("Could not completed action: %s", action)
}
}
func sendAction(method string) error {
sdbus := getSpotifyBus()
call := sdbus.Call(method, 0)

View File

@ -1,9 +1,9 @@
language: go
go:
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
- master
stages:
@ -14,7 +14,7 @@ jobs:
include:
- stage: lint
script:
- go get github.com/golang/lint/golint
- go get golang.org/x/lint/golint
- golint -set_exit_status
- go vet -v
- stage: test

View File

@ -26,7 +26,7 @@ to add new and/or missing endpoints. Currently the following services are suppor
- [x] Project-level Variables
- [x] Group-level Variables
- [x] Commits
- [ ] Custom Attributes
- [x] Custom Attributes
- [x] Deployments
- [x] Deploy Keys
- [x] Environments
@ -36,9 +36,9 @@ to add new and/or missing endpoints. Currently the following services are suppor
- [x] Feature flags
- [ ] Geo Nodes
- [x] Gitignores templates
- [ ] GitLab CI Config templates
- [x] GitLab CI Config templates
- [x] Groups
- [ ] Group Access Requests
- [x] Group Access Requests
- [x] Group Members
- [x] Issues
- [x] Issue Boards
@ -50,19 +50,19 @@ to add new and/or missing endpoints. Currently the following services are suppor
- [x] Merge Requests
- [x] Merge Request Approvals
- [x] Project Milestones
- [ ] Group Milestones
- [x] Group Milestones
- [x] Namespaces
- [x] Notes (comments)
- [ ] Discussions (threaded comments)
- [x] Notification settings
- [ ] Open source license templates
- [x] Open source license templates
- [x] Pages Domains
- [x] Pipelines
- [x] Pipeline Triggers
- [x] Pipeline Schedules
- [x] Projects (including setting Webhooks)
- [ ] Project Access Requests
- [ ] Project badges
- [x] Project Access Requests
- [x] Project badges
- [ ] Project import/export
- [x] Project Members
- [x] Project Snippets
@ -70,11 +70,10 @@ to add new and/or missing endpoints. Currently the following services are suppor
- [x] Repositories
- [x] Repository Files
- [x] Runners
- [ ] Search
- [x] Search
- [x] Services
- [x] Settings
- [x] Sidekiq metrics
- [x] Session
- [x] System Hooks
- [x] Tags
- [x] Todos
@ -150,7 +149,6 @@ func main() {
log.Fatal(err)
}
}
```
For complete usage of go-gitlab, see the full [package docs](https://godoc.org/github.com/xanzy/go-gitlab).

237
vendor/github.com/xanzy/go-gitlab/access_requests.go generated vendored Normal file
View File

@ -0,0 +1,237 @@
package gitlab
import (
"fmt"
"net/url"
"time"
)
// AccessRequest represents a access request for a group or project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html
type AccessRequest struct {
ID int `json:"id"`
Username string `json:"username"`
Name string `json:"name"`
State string `json:"state"`
CreatedAt *time.Time `json:"created_at"`
RequestedAt *time.Time `json:"requested_at"`
AccessLevel AccessLevelValue `json:"access_level"`
}
// AccessRequestsService handles communication with the project/group
// access requests related methods of the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/access_requests.html
type AccessRequestsService struct {
client *Client
}
// ListAccessRequestsOptions represents the available
// ListProjectAccessRequests() or ListGroupAccessRequests() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#list-access-requests-for-a-group-or-project
type ListAccessRequestsOptions ListOptions
// ListProjectAccessRequests gets a list of access requests
// viewable by the authenticated user.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#list-access-requests-for-a-group-or-project
func (s *AccessRequestsService) ListProjectAccessRequests(pid interface{}, opt *ListAccessRequestsOptions, options ...OptionFunc) ([]*AccessRequest, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/access_requests", url.QueryEscape(project))
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
return nil, nil, err
}
var ars []*AccessRequest
resp, err := s.client.Do(req, &ars)
if err != nil {
return nil, resp, err
}
return ars, resp, err
}
// ListGroupAccessRequests gets a list of access requests
// viewable by the authenticated user.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#list-access-requests-for-a-group-or-project
func (s *AccessRequestsService) ListGroupAccessRequests(gid interface{}, opt *ListAccessRequestsOptions, options ...OptionFunc) ([]*AccessRequest, *Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("groups/%s/access_requests", url.QueryEscape(group))
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
return nil, nil, err
}
var ars []*AccessRequest
resp, err := s.client.Do(req, &ars)
if err != nil {
return nil, resp, err
}
return ars, resp, err
}
// RequestProjectAccess requests access for the authenticated user
// to a group or project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#request-access-to-a-group-or-project
func (s *AccessRequestsService) RequestProjectAccess(pid interface{}, options ...OptionFunc) (*AccessRequest, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/access_requests", url.QueryEscape(project))
req, err := s.client.NewRequest("POST", u, nil, options)
if err != nil {
return nil, nil, err
}
ar := new(AccessRequest)
resp, err := s.client.Do(req, ar)
if err != nil {
return nil, resp, err
}
return ar, resp, err
}
// RequestGroupAccess requests access for the authenticated user
// to a group or project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#request-access-to-a-group-or-project
func (s *AccessRequestsService) RequestGroupAccess(gid interface{}, options ...OptionFunc) (*AccessRequest, *Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("groups/%s/access_requests", url.QueryEscape(group))
req, err := s.client.NewRequest("POST", u, nil, options)
if err != nil {
return nil, nil, err
}
ar := new(AccessRequest)
resp, err := s.client.Do(req, ar)
if err != nil {
return nil, resp, err
}
return ar, resp, err
}
// ApproveAccessRequestOptions represents the available
// ApproveProjectAccessRequest() and ApproveGroupAccessRequest() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#approve-an-access-request
type ApproveAccessRequestOptions struct {
AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
}
// ApproveProjectAccessRequest approves an access request for the given user.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#approve-an-access-request
func (s *AccessRequestsService) ApproveProjectAccessRequest(pid interface{}, user int, opt *ApproveAccessRequestOptions, options ...OptionFunc) (*AccessRequest, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/access_requests/%d/approve", url.QueryEscape(project), user)
req, err := s.client.NewRequest("PUT", u, opt, options)
if err != nil {
return nil, nil, err
}
ar := new(AccessRequest)
resp, err := s.client.Do(req, ar)
if err != nil {
return nil, resp, err
}
return ar, resp, err
}
// ApproveGroupAccessRequest approves an access request for the given user.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#approve-an-access-request
func (s *AccessRequestsService) ApproveGroupAccessRequest(gid interface{}, user int, opt *ApproveAccessRequestOptions, options ...OptionFunc) (*AccessRequest, *Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("groups/%s/access_requests/%d/approve", url.QueryEscape(group), user)
req, err := s.client.NewRequest("PUT", u, opt, options)
if err != nil {
return nil, nil, err
}
ar := new(AccessRequest)
resp, err := s.client.Do(req, ar)
if err != nil {
return nil, resp, err
}
return ar, resp, err
}
// DenyProjectAccessRequest denies an access request for the given user.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#deny-an-access-request
func (s *AccessRequestsService) DenyProjectAccessRequest(pid interface{}, user int, options ...OptionFunc) (*Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/access_requests/%d", url.QueryEscape(project), user)
req, err := s.client.NewRequest("DELETE", u, nil, options)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}
// DenyGroupAccessRequest denies an access request for the given user.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/access_requests.html#deny-an-access-request
func (s *AccessRequestsService) DenyGroupAccessRequest(gid interface{}, user int, options ...OptionFunc) (*Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, err
}
u := fmt.Sprintf("groups/%s/access_requests/%d", url.QueryEscape(group), user)
req, err := s.client.NewRequest("DELETE", u, nil, options)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}

70
vendor/github.com/xanzy/go-gitlab/ci_yml_templates.go generated vendored Normal file
View File

@ -0,0 +1,70 @@
package gitlab
import (
"fmt"
"net/url"
)
// CIYMLTemplatesService handles communication with the gitlab
// CI YML templates related methods of the GitLab API.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html
type CIYMLTemplatesService struct {
client *Client
}
// CIYMLTemplate represents a GitLab CI YML template.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html
type CIYMLTemplate struct {
Name string `json:"name"`
Content string `json:"content"`
}
// ListCIYMLTemplatesOptions represents the available ListAllTemplates() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/gitignores.html#list-gitignore-templates
type ListCIYMLTemplatesOptions ListOptions
// ListAllTemplates get all GitLab CI YML templates.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html#list-gitlab-ci-yml-templates
func (s *CIYMLTemplatesService) ListAllTemplates(opt *ListCIYMLTemplatesOptions, options ...OptionFunc) ([]*CIYMLTemplate, *Response, error) {
req, err := s.client.NewRequest("GET", "templates/gitlab_ci_ymls", opt, options)
if err != nil {
return nil, nil, err
}
var cts []*CIYMLTemplate
resp, err := s.client.Do(req, &cts)
if err != nil {
return nil, resp, err
}
return cts, resp, err
}
// GetTemplate get a single GitLab CI YML template.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html#single-gitlab-ci-yml-template
func (s *CIYMLTemplatesService) GetTemplate(key string, options ...OptionFunc) (*CIYMLTemplate, *Response, error) {
u := fmt.Sprintf("templates/gitlab_ci_ymls/%s", url.QueryEscape(key))
req, err := s.client.NewRequest("GET", u, nil, options)
if err != nil {
return nil, nil, err
}
ct := new(CIYMLTemplate)
resp, err := s.client.Do(req, ct)
if err != nil {
return nil, resp, err
}
return ct, resp, err
}

171
vendor/github.com/xanzy/go-gitlab/custom_attributes.go generated vendored Normal file
View File

@ -0,0 +1,171 @@
package gitlab
import (
"fmt"
)
// CustomAttributesService handles communication with the group, project and
// user custom attributes related methods of the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/custom_attributes.html
type CustomAttributesService struct {
client *Client
}
// CustomAttribute struct is used to unmarshal response to api calls.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/custom_attributes.html
type CustomAttribute struct {
Key string `json:"key"`
Value string `json:"value"`
}
// ListCustomUserAttributes lists the custom attributes of the specified user.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#list-custom-attributes
func (s *CustomAttributesService) ListCustomUserAttributes(user int, options ...OptionFunc) ([]*CustomAttribute, *Response, error) {
return s.listCustomAttributes("users", user, options...)
}
// ListCustomGroupAttributes lists the custom attributes of the specified group.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#list-custom-attributes
func (s *CustomAttributesService) ListCustomGroupAttributes(group int, options ...OptionFunc) ([]*CustomAttribute, *Response, error) {
return s.listCustomAttributes("groups", group, options...)
}
// ListCustomProjectAttributes lists the custom attributes of the specified project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#list-custom-attributes
func (s *CustomAttributesService) ListCustomProjectAttributes(project int, options ...OptionFunc) ([]*CustomAttribute, *Response, error) {
return s.listCustomAttributes("projects", project, options...)
}
func (s *CustomAttributesService) listCustomAttributes(resource string, id int, options ...OptionFunc) ([]*CustomAttribute, *Response, error) {
u := fmt.Sprintf("%s/%d/custom_attributes", resource, id)
req, err := s.client.NewRequest("GET", u, nil, options)
if err != nil {
return nil, nil, err
}
var cas []*CustomAttribute
resp, err := s.client.Do(req, &cas)
if err != nil {
return nil, resp, err
}
return cas, resp, err
}
// GetCustomUserAttribute returns the user attribute with a speciifc key.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#single-custom-attribute
func (s *CustomAttributesService) GetCustomUserAttribute(user int, key string, options ...OptionFunc) (*CustomAttribute, *Response, error) {
return s.getCustomAttribute("users", user, key, options...)
}
// GetCustomGroupAttribute returns the group attribute with a speciifc key.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#single-custom-attribute
func (s *CustomAttributesService) GetCustomGroupAttribute(group int, key string, options ...OptionFunc) (*CustomAttribute, *Response, error) {
return s.getCustomAttribute("groups", group, key, options...)
}
// GetCustomProjectAttribute returns the project attribute with a speciifc key.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#single-custom-attribute
func (s *CustomAttributesService) GetCustomProjectAttribute(project int, key string, options ...OptionFunc) (*CustomAttribute, *Response, error) {
return s.getCustomAttribute("projects", project, key, options...)
}
func (s *CustomAttributesService) getCustomAttribute(resource string, id int, key string, options ...OptionFunc) (*CustomAttribute, *Response, error) {
u := fmt.Sprintf("%s/%d/custom_attributes/%s", resource, id, key)
req, err := s.client.NewRequest("GET", u, nil, options)
if err != nil {
return nil, nil, err
}
var ca *CustomAttribute
resp, err := s.client.Do(req, &ca)
if err != nil {
return nil, resp, err
}
return ca, resp, err
}
// SetCustomUserAttribute sets the custom attributes of the specified user.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#set-custom-attribute
func (s *CustomAttributesService) SetCustomUserAttribute(user int, c CustomAttribute, options ...OptionFunc) (*CustomAttribute, *Response, error) {
return s.setCustomAttribute("users", user, c, options...)
}
// SetCustomGroupAttribute sets the custom attributes of the specified group.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#set-custom-attribute
func (s *CustomAttributesService) SetCustomGroupAttribute(group int, c CustomAttribute, options ...OptionFunc) (*CustomAttribute, *Response, error) {
return s.setCustomAttribute("groups", group, c, options...)
}
// SetCustomProjectAttribute sets the custom attributes of the specified project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#set-custom-attribute
func (s *CustomAttributesService) SetCustomProjectAttribute(project int, c CustomAttribute, options ...OptionFunc) (*CustomAttribute, *Response, error) {
return s.setCustomAttribute("projects", project, c, options...)
}
func (s *CustomAttributesService) setCustomAttribute(resource string, id int, c CustomAttribute, options ...OptionFunc) (*CustomAttribute, *Response, error) {
u := fmt.Sprintf("%s/%d/custom_attributes/%s", resource, id, c.Key)
req, err := s.client.NewRequest("PUT", u, c, options)
if err != nil {
return nil, nil, err
}
ca := new(CustomAttribute)
resp, err := s.client.Do(req, ca)
if err != nil {
return nil, resp, err
}
return ca, resp, err
}
// DeleteCustomUserAttribute removes the custom attribute of the specified user.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#delete-custom-attribute
func (s *CustomAttributesService) DeleteCustomUserAttribute(user int, key string, options ...OptionFunc) (*Response, error) {
return s.deleteCustomAttribute("users", user, key, options...)
}
// DeleteCustomGroupAttribute removes the custom attribute of the specified group.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#delete-custom-attribute
func (s *CustomAttributesService) DeleteCustomGroupAttribute(group int, key string, options ...OptionFunc) (*Response, error) {
return s.deleteCustomAttribute("groups", group, key, options...)
}
// DeleteCustomProjectAttribute removes the custom attribute of the specified project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/custom_attributes.html#delete-custom-attribute
func (s *CustomAttributesService) DeleteCustomProjectAttribute(project int, key string, options ...OptionFunc) (*Response, error) {
return s.deleteCustomAttribute("projects", project, key, options...)
}
func (s *CustomAttributesService) deleteCustomAttribute(resource string, id int, key string, options ...OptionFunc) (*Response, error) {
u := fmt.Sprintf("%s/%d/custom_attributes/%s", resource, id, key)
req, err := s.client.NewRequest("DELETE", u, nil, options)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}

1112
vendor/github.com/xanzy/go-gitlab/discussions.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -477,6 +477,7 @@ type MergeEvent struct {
WorkInProgress bool `json:"work_in_progress"`
URL string `json:"url"`
Action string `json:"action"`
OldRev string `json:"oldrev"`
Assignee struct {
Name string `json:"name"`
Username string `json:"username"`
@ -489,6 +490,24 @@ type MergeEvent struct {
Username string `json:"username"`
AvatarURL string `json:"avatar_url"`
} `json:"assignee"`
Changes struct {
AssigneeID struct {
Previous int `json:"previous"`
Current int `json:"current"`
} `json:"assignee_id"`
Description struct {
Previous string `json:"previous"`
Current string `json:"current"`
} `json:"description"`
Labels struct {
Previous []Label `json:"previous"`
Current []Label `json:"current"`
} `json:"labels"`
UpdatedByID struct {
Previous int `json:"previous"`
Current int `json:"current"`
} `json:"updated_by_id"`
} `json:"changes"`
}
// WikiPageEvent represents a wiki page event.

View File

@ -68,6 +68,10 @@ const (
GuestPermissions AccessLevelValue = 10
ReporterPermissions AccessLevelValue = 20
DeveloperPermissions AccessLevelValue = 30
MaintainerPermissions AccessLevelValue = 40
OwnerPermissions AccessLevelValue = 50
// These are deprecated and should be removed in a future version
MasterPermissions AccessLevelValue = 40
OwnerPermission AccessLevelValue = 50
)
@ -273,13 +277,17 @@ type Client struct {
UserAgent string
// Services used for talking to different parts of the GitLab API.
AccessRequests *AccessRequestsService
AwardEmoji *AwardEmojiService
Branches *BranchesService
BuildVariables *BuildVariablesService
BroadcastMessage *BroadcastMessagesService
CIYMLTemplate *CIYMLTemplatesService
Commits *CommitsService
CustomAttribute *CustomAttributesService
DeployKeys *DeployKeysService
Deployments *DeploymentsService
Discussions *DiscussionsService
Environments *EnvironmentsService
Events *EventsService
Features *FeaturesService
@ -295,6 +303,7 @@ type Client struct {
Keys *KeysService
Boards *IssueBoardsService
Labels *LabelsService
LicenseTemplates *LicenseTemplatesService
MergeRequests *MergeRequestsService
MergeRequestApprovals *MergeRequestApprovalsService
Milestones *MilestonesService
@ -307,6 +316,7 @@ type Client struct {
PipelineTriggers *PipelineTriggersService
Projects *ProjectsService
ProjectMembers *ProjectMembersService
ProjectBadges *ProjectBadgesService
ProjectSnippets *ProjectSnippetsService
ProjectVariables *ProjectVariablesService
ProtectedBranches *ProtectedBranchesService
@ -315,7 +325,6 @@ type Client struct {
Runners *RunnersService
Search *SearchService
Services *ServicesService
Session *SessionService
Settings *SettingsService
Sidekiq *SidekiqService
Snippets *SnippetsService
@ -407,13 +416,17 @@ func newClient(httpClient *http.Client) *Client {
timeStats := &timeStatsService{client: c}
// Create all the public services.
c.AccessRequests = &AccessRequestsService{client: c}
c.AwardEmoji = &AwardEmojiService{client: c}
c.Branches = &BranchesService{client: c}
c.BuildVariables = &BuildVariablesService{client: c}
c.BroadcastMessage = &BroadcastMessagesService{client: c}
c.CIYMLTemplate = &CIYMLTemplatesService{client: c}
c.Commits = &CommitsService{client: c}
c.CustomAttribute = &CustomAttributesService{client: c}
c.DeployKeys = &DeployKeysService{client: c}
c.Deployments = &DeploymentsService{client: c}
c.Discussions = &DiscussionsService{client: c}
c.Environments = &EnvironmentsService{client: c}
c.Events = &EventsService{client: c}
c.Features = &FeaturesService{client: c}
@ -429,6 +442,7 @@ func newClient(httpClient *http.Client) *Client {
c.Keys = &KeysService{client: c}
c.Boards = &IssueBoardsService{client: c}
c.Labels = &LabelsService{client: c}
c.LicenseTemplates = &LicenseTemplatesService{client: c}
c.MergeRequests = &MergeRequestsService{client: c, timeStats: timeStats}
c.MergeRequestApprovals = &MergeRequestApprovalsService{client: c}
c.Milestones = &MilestonesService{client: c}
@ -441,6 +455,7 @@ func newClient(httpClient *http.Client) *Client {
c.PipelineTriggers = &PipelineTriggersService{client: c}
c.Projects = &ProjectsService{client: c}
c.ProjectMembers = &ProjectMembersService{client: c}
c.ProjectBadges = &ProjectBadgesService{client: c}
c.ProjectSnippets = &ProjectSnippetsService{client: c}
c.ProjectVariables = &ProjectVariablesService{client: c}
c.ProtectedBranches = &ProtectedBranchesService{client: c}
@ -449,7 +464,6 @@ func newClient(httpClient *http.Client) *Client {
c.Runners = &RunnersService{client: c}
c.Services = &ServicesService{client: c}
c.Search = &SearchService{client: c}
c.Session = &SessionService{client: c}
c.Settings = &SettingsService{client: c}
c.Sidekiq = &SidekiqService{client: c}
c.Snippets = &SnippetsService{client: c}
@ -500,8 +514,14 @@ func (c *Client) SetBaseURL(urlStr string) error {
// request body.
func (c *Client) NewRequest(method, path string, opt interface{}, options []OptionFunc) (*http.Request, error) {
u := *c.baseURL
// Set the encoded opaque data
u.Opaque = c.baseURL.Path + path
unescaped, err := url.PathUnescape(path)
if err != nil {
return nil, err
}
// Set the encoded path data
u.RawPath = c.baseURL.Path + path
u.Path = c.baseURL.Path + unescaped
if opt != nil {
q, err := query.Values(opt)
@ -681,7 +701,7 @@ type ErrorResponse struct {
}
func (e *ErrorResponse) Error() string {
path, _ := url.QueryUnescape(e.Response.Request.URL.Opaque)
path, _ := url.QueryUnescape(e.Response.Request.URL.Path)
u := fmt.Sprintf("%s://%s%s", e.Response.Request.URL.Scheme, e.Response.Request.URL.Host, path)
return fmt.Sprintf("%s %s: %d %s", e.Response.Request.Method, u, e.Response.StatusCode, e.Message)
}
@ -842,3 +862,24 @@ func MergeMethod(v MergeMethodValue) *MergeMethodValue {
*p = v
return p
}
// BoolValue is a boolean value with advanced json unmarshaling features.
type BoolValue bool
// UnmarshalJSON allows 1 and 0 to be considered as boolean values
// Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/50122
func (t *BoolValue) UnmarshalJSON(b []byte) error {
switch string(b) {
case `"1"`:
*t = true
return nil
case `"0"`:
*t = false
return nil
default:
var v bool
err := json.Unmarshal(b, &v)
*t = BoolValue(v)
return err
}
}

View File

@ -44,8 +44,8 @@ type GroupMember struct {
ExpiresAt *ISOTime `json:"expires_at"`
}
// ListGroupMembersOptions represents the available ListGroupMembers()
// options.
// ListGroupMembersOptions represents the available ListGroupMembers() and
// ListAllGroupMembers() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project
@ -55,7 +55,7 @@ type ListGroupMembersOptions struct {
}
// ListGroupMembers get a list of group members viewable by the authenticated
// user.
// user. Returns a list including inherited members through ancestor groups.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project
@ -80,6 +80,32 @@ func (s *GroupsService) ListGroupMembers(gid interface{}, opt *ListGroupMembersO
return gm, resp, err
}
// ListAllGroupMembers get a list of group members viewable by the authenticated
// user. Returns a list including inherited members through ancestor groups.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project-including-inherited-members
func (s *GroupsService) ListAllGroupMembers(gid interface{}, opt *ListGroupMembersOptions, options ...OptionFunc) ([]*GroupMember, *Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("groups/%s/members/all", url.QueryEscape(group))
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
return nil, nil, err
}
var gm []*GroupMember
resp, err := s.client.Do(req, &gm)
if err != nil {
return nil, resp, err
}
return gm, resp, err
}
// AddGroupMemberOptions represents the available AddGroupMember() options.
//
// GitLab API docs:

View File

@ -305,7 +305,7 @@ func (s *IssuesService) CreateIssue(pid interface{}, opt *CreateIssueOptions, op
// UpdateIssueOptions represents the available UpdateIssue() options.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#edit-issues
// GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#edit-issue
type UpdateIssueOptions struct {
Title *string `url:"title,omitempty" json:"title,omitempty"`
Description *string `url:"description,omitempty" json:"description,omitempty"`
@ -316,6 +316,7 @@ type UpdateIssueOptions struct {
StateEvent *string `url:"state_event,omitempty" json:"state_event,omitempty"`
UpdatedAt *time.Time `url:"updated_at,omitempty" json:"updated_at,omitempty"`
DueDate *ISOTime `url:"due_date,omitempty" json:"due_date,omitempty"`
Weight *int `url:"weight,omitempty" json:"weight,omitempty"`
DiscussionLocked *bool `url:"discussion_locked,omitempty" json:"discussion_locked,omitempty"`
}
@ -362,6 +363,60 @@ func (s *IssuesService) DeleteIssue(pid interface{}, issue int, options ...Optio
return s.client.Do(req, nil)
}
// SubscribeToIssue subscribes the authenticated user to the given issue to
// receive notifications. If the user is already subscribed to the issue, the
// status code 304 is returned.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/merge_requests.html#subscribe-to-a-merge-request
func (s *IssuesService) SubscribeToIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/issues/%d/subscribe", url.QueryEscape(project), issue)
req, err := s.client.NewRequest("POST", u, nil, options)
if err != nil {
return nil, nil, err
}
i := new(Issue)
resp, err := s.client.Do(req, i)
if err != nil {
return nil, resp, err
}
return i, resp, err
}
// UnsubscribeFromIssue unsubscribes the authenticated user from the given
// issue to not receive notifications from that merge request. If the user
// is not subscribed to the issue, status code 304 is returned.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/merge_requests.html#unsubscribe-from-a-merge-request
func (s *IssuesService) UnsubscribeFromIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/issues/%d/unsubscribe", url.QueryEscape(project), issue)
req, err := s.client.NewRequest("POST", u, nil, options)
if err != nil {
return nil, nil, err
}
i := new(Issue)
resp, err := s.client.Do(req, i)
if err != nil {
return nil, resp, err
}
return i, resp, err
}
// ListMergeRequestsClosingIssueOptions represents the available
// ListMergeRequestsClosingIssue() options.
//

View File

@ -59,6 +59,7 @@ type Job struct {
Status string `json:"status"`
Tag bool `json:"tag"`
User *User `json:"user"`
WebURL string `json:"web_url"`
}
// ListJobsOptions are options for two list apis
@ -197,6 +198,40 @@ func (s *JobsService) DownloadArtifactsFile(pid interface{}, refName string, job
return artifactsBuf, resp, err
}
// DownloadSingleArtifactsFile download a file from the artifacts from the
// given reference name and job provided the job finished successfully.
// Only a single file is going to be extracted from the archive and streamed
// to a client.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/jobs.html#download-a-single-artifact-file
func (s *JobsService) DownloadSingleArtifactsFile(pid interface{}, jobID int, artifactPath string, options ...OptionFunc) (io.Reader, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf(
"projects/%s/jobs/%d/artifacts/%s",
url.QueryEscape(project),
jobID,
artifactPath,
)
req, err := s.client.NewRequest("GET", u, nil, options)
if err != nil {
return nil, nil, err
}
artifactBuf := new(bytes.Buffer)
resp, err := s.client.Do(req, artifactBuf)
if err != nil {
return nil, resp, err
}
return artifactBuf, resp, err
}
// GetTraceFile gets a trace of a specific job of a project
//
// GitLab API docs:

View File

@ -17,6 +17,7 @@
package gitlab
import (
"encoding/json"
"fmt"
"net/url"
)
@ -44,6 +45,26 @@ type Label struct {
Priority int `json:"priority"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (l *Label) UnmarshalJSON(data []byte) error {
type alias Label
if err := json.Unmarshal(data, (*alias)(l)); err != nil {
return err
}
if l.Name == "" {
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
if title, ok := raw["title"].(string); ok {
l.Name = title
}
}
return nil
}
func (l Label) String() string {
return Stringify(l)
}

92
vendor/github.com/xanzy/go-gitlab/license_templates.go generated vendored Normal file
View File

@ -0,0 +1,92 @@
package gitlab
import (
"fmt"
)
// LicenseTemplate represents a license template.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/licenses.html
type LicenseTemplate struct {
Key string `json:"key"`
Name string `json:"name"`
Nickname string `json:"nickname"`
Featured bool `json:"featured"`
HTMLURL string `json:"html_url"`
SourceURL string `json:"source_url"`
Description string `json:"description"`
Conditions []string `json:"conditions"`
Permissions []string `json:"permissions"`
Limitations []string `json:"limitations"`
Content string `json:"content"`
}
// LicenseTemplatesService handles communication with the license templates
// related methods of the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/templates/licenses.html
type LicenseTemplatesService struct {
client *Client
}
// ListLicenseTemplatesOptions represents the available
// ListLicenseTemplates() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/licenses.html#list-license-templates
type ListLicenseTemplatesOptions struct {
ListOptions
Popular *bool `url:"popular,omitempty" json:"popular,omitempty"`
}
// ListLicenseTemplates get all license templates.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/licenses.html#list-license-templates
func (s *LicenseTemplatesService) ListLicenseTemplates(opt *ListLicenseTemplatesOptions, options ...OptionFunc) ([]*LicenseTemplate, *Response, error) {
req, err := s.client.NewRequest("GET", "templates/licenses", opt, options)
if err != nil {
return nil, nil, err
}
var lts []*LicenseTemplate
resp, err := s.client.Do(req, &lts)
if err != nil {
return nil, resp, err
}
return lts, resp, err
}
// GetLicenseTemplateOptions represents the available
// GetLicenseTemplate() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/licenses.html#single-license-template
type GetLicenseTemplateOptions struct {
Project *string `url:"project,omitempty" json:"project,omitempty"`
Fullname *string `url:"fullname,omitempty" json:"fullname,omitempty"`
}
// GetLicenseTemplate get a single license template. You can pass parameters
// to replace the license placeholder.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/templates/licenses.html#single-license-template
func (s *LicenseTemplatesService) GetLicenseTemplate(template string, opt *GetLicenseTemplateOptions, options ...OptionFunc) (*LicenseTemplate, *Response, error) {
u := fmt.Sprintf("templates/licenses/%s", template)
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
return nil, nil, err
}
lt := new(LicenseTemplate)
resp, err := s.client.Do(req, lt)
if err != nil {
return nil, resp, err
}
return lt, resp, err
}

View File

@ -28,6 +28,7 @@ type MergeRequestApprovals struct {
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
MergeStatus string `json:"merge_status"`
ApprovalsBeforeMerge int `json:"approvals_before_merge"`
ApprovalsRequired int `json:"approvals_required"`
ApprovalsLeft int `json:"approvals_left"`
ApprovedBy []struct {

View File

@ -153,10 +153,15 @@ type ListMergeRequestsOptions struct {
Labels Labels `url:"labels,omitempty" json:"labels,omitempty"`
CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
UpdatedAfter *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
Scope *string `url:"scope,omitempty" json:"scope,omitempty"`
AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"`
AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
MyReactionEmoji *string `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
SourceBranch *string `url:"source_branch,omitempty" json:"source_branch,omitempty"`
TargetBranch *string `url:"target_branch,omitempty" json:"target_branch,omitempty"`
Search *string `url:"search,omitempty" json:"search,omitempty"`
}
// ListMergeRequests gets all merge requests. The state parameter can be used
@ -181,6 +186,57 @@ func (s *MergeRequestsService) ListMergeRequests(opt *ListMergeRequestsOptions,
return m, resp, err
}
// ListGroupMergeRequestsOptions represents the available ListGroupMergeRequests()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/merge_requests.html#list-group-merge-requests
type ListGroupMergeRequestsOptions struct {
ListOptions
State *string `url:"state,omitempty" json:"state,omitempty"`
OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"`
Sort *string `url:"sort,omitempty" json:"sort,omitempty"`
Milestone *string `url:"milestone,omitempty" json:"milestone,omitempty"`
View *string `url:"view,omitempty" json:"view,omitempty"`
Labels Labels `url:"labels,omitempty" json:"labels,omitempty"`
CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
UpdatedAfter *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
Scope *string `url:"scope,omitempty" json:"scope,omitempty"`
AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"`
AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
MyReactionEmoji *string `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
SourceBranch *string `url:"source_branch,omitempty" json:"source_branch,omitempty"`
TargetBranch *string `url:"target_branch,omitempty" json:"target_branch,omitempty"`
Search *string `url:"search,omitempty" json:"search,omitempty"`
}
// ListGroupMergeRequests gets all merge requests for this group.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/merge_requests.html#list-group-merge-requests
func (s *MergeRequestsService) ListGroupMergeRequests(gid interface{}, opt *ListGroupMergeRequestsOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("groups/%s/merge_requests", url.QueryEscape(group))
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
return nil, nil, err
}
var m []*MergeRequest
resp, err := s.client.Do(req, &m)
if err != nil {
return nil, resp, err
}
return m, resp, err
}
// ListProjectMergeRequestsOptions represents the available ListMergeRequests()
// options.
//
@ -197,19 +253,21 @@ type ListProjectMergeRequestsOptions struct {
Labels Labels `url:"labels,omitempty" json:"labels,omitempty"`
CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
UpdatedAfter *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
Scope *string `url:"scope,omitempty" json:"scope,omitempty"`
AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"`
AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
MyReactionEmoji *string `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
SourceBranch *string `url:"source_branch,omitempty" json:"source_branch,omitempty"`
TargetBranch *string `url:"target_branch,omitempty" json:"target_branch,omitempty"`
Search *string `url:"search,omitempty" json:"search,omitempty"`
}
// ListProjectMergeRequests gets all merge requests for this project. The state
// parameter can be used to get only merge requests with a given state (opened,
// closed, or merged) or all of them (all). The pagination parameters page and
// per_page can be used to restrict the list of merge requests.
// ListProjectMergeRequests gets all merge requests for this project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/merge_requests.html#list-merge-requests
// https://docs.gitlab.com/ce/api/merge_requests.html#list-project-merge-requests
func (s *MergeRequestsService) ListProjectMergeRequests(pid interface{}, opt *ListProjectMergeRequestsOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) {
project, err := parseID(pid)
if err != nil {
@ -454,6 +512,10 @@ type UpdateMergeRequestOptions struct {
Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"`
MilestoneID *int `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
StateEvent *string `url:"state_event,omitempty" json:"state_event,omitempty"`
RemoveSourceBranch *bool `url:"remove_source_branch,omitempty" json:"remove_source_branch,omitempty"`
Squash *bool `url:"squash,omitempty" json:"squash,omitempty"`
DiscussionLocked *bool `url:"discussion_locked,omitempty" json:"discussion_locked,omitempty"`
AllowCollaboration *bool `url:"allow_collaboration,omitempty" json:"allow_collaboration,omitempty"`
}
// UpdateMergeRequest updates an existing project milestone.
@ -626,8 +688,8 @@ func (s *MergeRequestsService) GetSingleMergeRequestDiffVersion(pid interface{},
return v, resp, err
}
// SubscribeToMergeRequest subscribes the authenticated user to the given merge request
// to receive notifications. If the user is already subscribed to the
// SubscribeToMergeRequest subscribes the authenticated user to the given merge
// request to receive notifications. If the user is already subscribed to the
// merge request, the status code 304 is returned.
//
// GitLab API docs:
@ -653,9 +715,10 @@ func (s *MergeRequestsService) SubscribeToMergeRequest(pid interface{}, mergeReq
return m, resp, err
}
// UnsubscribeFromMergeRequest unsubscribes the authenticated user from the given merge request
// to not receive notifications from that merge request. If the user is
// not subscribed to the merge request, status code 304 is returned.
// UnsubscribeFromMergeRequest unsubscribes the authenticated user from the
// given merge request to not receive notifications from that merge request.
// If the user is not subscribed to the merge request, status code 304 is
// returned.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/merge_requests.html#unsubscribe-from-a-merge-request

View File

@ -45,7 +45,6 @@ type Note struct {
Email string `json:"email"`
Name string `json:"name"`
State string `json:"state"`
CreatedAt *time.Time `json:"created_at"`
AvatarURL string `json:"avatar_url"`
WebURL string `json:"web_url"`
} `json:"author"`
@ -55,9 +54,37 @@ type Note struct {
CreatedAt *time.Time `json:"created_at"`
NoteableID int `json:"noteable_id"`
NoteableType string `json:"noteable_type"`
Position *NotePosition `json:"position"`
Resolvable bool `json:"resolvable"`
Resolved bool `json:"resolved"`
ResolvedBy struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Name string `json:"name"`
State string `json:"state"`
AvatarURL string `json:"avatar_url"`
WebURL string `json:"web_url"`
} `json:"resolved_by"`
NoteableIID int `json:"noteable_iid"`
}
// NotePosition represents the position attributes of a note.
type NotePosition struct {
BaseSHA string `json:"base_sha"`
StartSHA string `json:"start_sha"`
HeadSHA string `json:"head_sha"`
PositionType string `json:"position_type"`
NewPath string `json:"new_path,omitempty"`
NewLine int `json:"new_line,omitempty"`
OldPath string `json:"old_path,omitempty"`
OldLine int `json:"old_line,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
}
func (n Note) String() string {
return Stringify(n)
}
@ -66,7 +93,11 @@ func (n Note) String() string {
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/notes.html#list-project-issue-notes
type ListIssueNotesOptions ListOptions
type ListIssueNotesOptions struct {
ListOptions
OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"`
Sort *string `url:"sort,omitempty" json:"sort,omitempty"`
}
// ListIssueNotes gets a list of all notes for a single issue.
//

View File

@ -61,6 +61,25 @@ func (s *PagesDomainsService) ListPagesDomains(pid interface{}, opt *ListPagesDo
return pd, resp, err
}
// ListAllPagesDomains gets a list of all pages domains.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/pages_domains.html#list-all-pages-domains
func (s *PagesDomainsService) ListAllPagesDomains(options ...OptionFunc) ([]*PagesDomain, *Response, error) {
req, err := s.client.NewRequest("GET", "pages/domains", nil, options)
if err != nil {
return nil, nil, err
}
var pd []*PagesDomain
resp, err := s.client.Do(req, &pd)
if err != nil {
return nil, resp, err
}
return pd, resp, err
}
// GetPagesDomain get a specific pages domain for a project.
//
// GitLab API docs:

View File

@ -30,15 +30,6 @@ type PipelineSchedulesService struct {
client *Client
}
// PipelineVariable represents a pipeline schedule variable.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/pipeline_schedules.html#pipeline-schedule-variable
type PipelineVariable struct {
Key string `json:"key"`
Value string `json:"value"`
}
// PipelineSchedule represents a pipeline schedule.
//
// GitLab API docs:

View File

@ -30,6 +30,14 @@ type PipelinesService struct {
client *Client
}
// PipelineVariable represents a pipeline variable.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html
type PipelineVariable struct {
Key string `json:"key"`
Value string `json:"value"`
}
// Pipeline represents a GitLab pipeline.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html
@ -84,6 +92,7 @@ type ListProjectPipelinesOptions struct {
Scope *string `url:"scope,omitempty" json:"scope,omitempty"`
Status *BuildStateValue `url:"status,omitempty" json:"status,omitempty"`
Ref *string `url:"ref,omitempty" json:"ref,omitempty"`
SHA *string `url:"sha,omitempty" json:"sha,omitempty"`
YamlErrors *bool `url:"yaml_errors,omitempty" json:"yaml_errors,omitempty"`
Name *string `url:"name,omitempty" json:"name,omitempty"`
Username *string `url:"username,omitempty" json:"username,omitempty"`
@ -142,7 +151,8 @@ func (s *PipelinesService) GetPipeline(pid interface{}, pipeline int, options ..
//
// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#create-a-new-pipeline
type CreatePipelineOptions struct {
Ref *string `url:"ref,omitempty" json:"ref"`
Ref *string `url:"ref" json:"ref"`
Variables []*PipelineVariable `url:"variables,omitempty" json:"variables,omitempty"`
}
// CreatePipeline creates a new project pipeline.

208
vendor/github.com/xanzy/go-gitlab/project_badges.go generated vendored Normal file
View File

@ -0,0 +1,208 @@
package gitlab
import (
"fmt"
"net/url"
)
// ProjectBadge represents a project badge.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#list-all-badges-of-a-project
type ProjectBadge struct {
ID int `json:"id"`
LinkURL string `json:"link_url"`
ImageURL string `json:"image_url"`
RenderedLinkURL string `json:"rendered_link_url"`
RenderedImageURL string `json:"rendered_image_url"`
// Kind represents a project badge kind. Can be empty, when used PreviewProjectBadge().
Kind string `json:"kind"`
}
// ProjectBadgesService handles communication with the project badges
// related methods of the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/ee/api/project_badges.html
type ProjectBadgesService struct {
client *Client
}
// ListProjectBadgesOptions represents the available ListProjectBadges()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#list-all-badges-of-a-project
type ListProjectBadgesOptions ListOptions
// ListProjectBadges gets a list of a project's badges and its group badges.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#list-all-badges-of-a-project
func (s *ProjectBadgesService) ListProjectBadges(pid interface{}, opt *ListProjectBadgesOptions, options ...OptionFunc) ([]*ProjectBadge, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/badges", url.QueryEscape(project))
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
return nil, nil, err
}
var pb []*ProjectBadge
resp, err := s.client.Do(req, &pb)
if err != nil {
return nil, resp, err
}
return pb, resp, err
}
// GetProjectBadge gets a project badge.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#get-a-badge-of-a-project
func (s *ProjectBadgesService) GetProjectBadge(pid interface{}, badge int, options ...OptionFunc) (*ProjectBadge, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/badges/%d", url.QueryEscape(project), badge)
req, err := s.client.NewRequest("GET", u, nil, options)
if err != nil {
return nil, nil, err
}
pb := new(ProjectBadge)
resp, err := s.client.Do(req, pb)
if err != nil {
return nil, resp, err
}
return pb, resp, err
}
// AddProjectBadgeOptions represents the available AddProjectBadge() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#add-a-badge-to-a-project
type AddProjectBadgeOptions struct {
LinkURL *string `url:"link_url,omitempty" json:"link_url,omitempty"`
ImageURL *string `url:"image_url,omitempty" json:"image_url,omitempty"`
}
// AddProjectBadge adds a badge to a project.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#add-a-badge-to-a-project
func (s *ProjectBadgesService) AddProjectBadge(pid interface{}, opt *AddProjectBadgeOptions, options ...OptionFunc) (*ProjectBadge, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/badges", url.QueryEscape(project))
req, err := s.client.NewRequest("POST", u, opt, options)
if err != nil {
return nil, nil, err
}
pb := new(ProjectBadge)
resp, err := s.client.Do(req, pb)
if err != nil {
return nil, resp, err
}
return pb, resp, err
}
// EditProjectBadgeOptions represents the available EditProjectBadge() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#edit-a-badge-of-a-project
type EditProjectBadgeOptions struct {
LinkURL *string `url:"link_url,omitempty" json:"link_url,omitempty"`
ImageURL *string `url:"image_url,omitempty" json:"image_url,omitempty"`
}
// EditProjectBadge updates a badge of a project.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#edit-a-badge-of-a-project
func (s *ProjectBadgesService) EditProjectBadge(pid interface{}, badge int, opt *EditProjectBadgeOptions, options ...OptionFunc) (*ProjectBadge, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/badges/%d", url.QueryEscape(project), badge)
req, err := s.client.NewRequest("PUT", u, opt, options)
if err != nil {
return nil, nil, err
}
pb := new(ProjectBadge)
resp, err := s.client.Do(req, pb)
if err != nil {
return nil, resp, err
}
return pb, resp, err
}
// DeleteProjectBadge removes a badge from a project. Only project's
// badges will be removed by using this endpoint.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#remove-a-badge-from-a-project
func (s *ProjectBadgesService) DeleteProjectBadge(pid interface{}, badge int, options ...OptionFunc) (*Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/badges/%d", url.QueryEscape(project), badge)
req, err := s.client.NewRequest("DELETE", u, nil, options)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}
// ProjectBadgePreviewOptions represents the available PreviewProjectBadge() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#preview-a-badge-from-a-project
type ProjectBadgePreviewOptions struct {
LinkURL *string `url:"link_url,omitempty" json:"link_url,omitempty"`
ImageURL *string `url:"image_url,omitempty" json:"image_url,omitempty"`
}
// PreviewProjectBadge returns how the link_url and image_url final URLs would be after
// resolving the placeholder interpolation.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/project_badges.html#preview-a-badge-from-a-project
func (s *ProjectBadgesService) PreviewProjectBadge(pid interface{}, opt *ProjectBadgePreviewOptions, options ...OptionFunc) (*ProjectBadge, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/badges/render", url.QueryEscape(project))
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
return nil, nil, err
}
pb := new(ProjectBadge)
resp, err := s.client.Do(req, &pb)
if err != nil {
return nil, resp, err
}
return pb, resp, err
}

View File

@ -29,8 +29,8 @@ type ProjectMembersService struct {
client *Client
}
// ListProjectMembersOptions represents the available ListProjectMembers()
// options.
// ListProjectMembersOptions represents the available ListProjectMembers() and
// ListAllProjectMembers() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project
@ -39,7 +39,9 @@ type ListProjectMembersOptions struct {
Query *string `url:"query,omitempty" json:"query,omitempty"`
}
// ListProjectMembers gets a list of a project's team members.
// ListProjectMembers gets a list of a project's team members viewable by the
// authenticated user. Returns only direct members and not inherited members
// through ancestors groups.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project
@ -64,6 +66,33 @@ func (s *ProjectMembersService) ListProjectMembers(pid interface{}, opt *ListPro
return pm, resp, err
}
// ListAllProjectMembers gets a list of a project's team members viewable by the
// authenticated user. Returns a list including inherited members through
// ancestor groups.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project-including-inherited-members
func (s *ProjectMembersService) ListAllProjectMembers(pid interface{}, opt *ListProjectMembersOptions, options ...OptionFunc) ([]*ProjectMember, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/members/all", url.QueryEscape(project))
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
return nil, nil, err
}
var pm []*ProjectMember
resp, err := s.client.Do(req, &pm)
if err != nil {
return nil, resp, err
}
return pm, resp, err
}
// GetProjectMember gets a project team member.
//
// GitLab API docs:

View File

@ -194,6 +194,7 @@ type ListProjectsOptions struct {
Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"`
WithIssuesEnabled *bool `url:"with_issues_enabled,omitempty" json:"with_issues_enabled,omitempty"`
WithMergeRequestsEnabled *bool `url:"with_merge_requests_enabled,omitempty" json:"with_merge_requests_enabled,omitempty"`
MinAccessLevel *AccessLevelValue `url:"min_access_level,omitempty" json:"min_access_level,omitempty"`
}
// ListProjects gets a list of projects accessible by the authenticated user.
@ -282,6 +283,35 @@ func (s *ProjectsService) ListProjectsUsers(pid interface{}, opt *ListProjectUse
return p, resp, err
}
// ProjectLanguages is a map of strings because the response is arbitrary
//
// Gitlab API docs: https://docs.gitlab.com/ce/api/projects.html#languages
type ProjectLanguages map[string]float32
// GetProjectLanguages gets a list of languages used by the project
//
// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#languages
func (s *ProjectsService) GetProjectLanguages(pid interface{}, options ...OptionFunc) (*ProjectLanguages, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/languages", url.QueryEscape(project))
req, err := s.client.NewRequest("GET", u, nil, options)
if err != nil {
return nil, nil, err
}
p := new(ProjectLanguages)
resp, err := s.client.Do(req, p)
if err != nil {
return nil, resp, err
}
return p, resp, err
}
// GetProject gets a specific project, identified by project ID or
// NAMESPACE/PROJECT_NAME, which is owned by the authenticated user.
//
@ -371,7 +401,7 @@ func (s *ProjectsService) GetProjectEvents(pid interface{}, opt *GetProjectEvent
// CreateProjectOptions represents the available CreateProjects() options.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#create-project
// GitLab API docs: https://docs.gitlab.com/ee/api/projects.html#create-project
type CreateProjectOptions struct {
Name *string `url:"name,omitempty" json:"name,omitempty"`
Path *string `url:"path,omitempty" json:"path,omitempty"`
@ -397,6 +427,7 @@ type CreateProjectOptions struct {
TagList *[]string `url:"tag_list,omitempty" json:"tag_list,omitempty"`
PrintingMergeRequestLinkEnabled *bool `url:"printing_merge_request_link_enabled,omitempty" json:"printing_merge_request_link_enabled,omitempty"`
CIConfigPath *string `url:"ci_config_path,omitempty" json:"ci_config_path,omitempty"`
ApprovalsBeforeMerge *int `url:"approvals_before_merge" json:"approvals_before_merge"`
}
// CreateProject creates a new project owned by the authenticated user.

View File

@ -66,7 +66,7 @@ func (s *RepositoryFilesService) GetFile(pid interface{}, fileName string, opt *
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/repository/files/%s", url.QueryEscape(project), url.QueryEscape(fileName))
u := fmt.Sprintf("projects/%s/repository/files/%s", url.QueryEscape(project), url.PathEscape(fileName))
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
@ -99,7 +99,7 @@ func (s *RepositoryFilesService) GetRawFile(pid interface{}, fileName string, op
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/repository/files/%s/raw", url.QueryEscape(project), url.QueryEscape(fileName))
u := fmt.Sprintf("projects/%s/repository/files/%s/raw", url.QueryEscape(project), url.PathEscape(fileName))
req, err := s.client.NewRequest("GET", u, opt, options)
if err != nil {
@ -149,7 +149,7 @@ func (s *RepositoryFilesService) CreateFile(pid interface{}, fileName string, op
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/repository/files/%s", url.QueryEscape(project), url.QueryEscape(fileName))
u := fmt.Sprintf("projects/%s/repository/files/%s", url.QueryEscape(project), url.PathEscape(fileName))
req, err := s.client.NewRequest("POST", u, opt, options)
if err != nil {
@ -188,7 +188,7 @@ func (s *RepositoryFilesService) UpdateFile(pid interface{}, fileName string, op
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/repository/files/%s", url.QueryEscape(project), url.QueryEscape(fileName))
u := fmt.Sprintf("projects/%s/repository/files/%s", url.QueryEscape(project), url.PathEscape(fileName))
req, err := s.client.NewRequest("PUT", u, opt, options)
if err != nil {
@ -224,7 +224,7 @@ func (s *RepositoryFilesService) DeleteFile(pid interface{}, fileName string, op
if err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/repository/files/%s", url.QueryEscape(project), url.QueryEscape(fileName))
u := fmt.Sprintf("projects/%s/repository/files/%s", url.QueryEscape(project), url.PathEscape(fileName))
req, err := s.client.NewRequest("DELETE", u, opt, options)
if err != nil {

View File

@ -18,6 +18,7 @@ package gitlab
import (
"fmt"
"net"
"net/url"
"time"
)
@ -38,9 +39,11 @@ type Runner struct {
Description string `json:"description"`
Active bool `json:"active"`
IsShared bool `json:"is_shared"`
IPAddress *net.IP `json:"ip_address"`
Name string `json:"name"`
Online bool `json:"online"`
Status string `json:"status"`
Token string `json:"token"`
}
// RunnerDetails represents the GitLab CI runner details.
@ -52,11 +55,11 @@ type RunnerDetails struct {
Description string `json:"description"`
ID int `json:"id"`
IsShared bool `json:"is_shared"`
ContactedAt *time.Time `json:"contacted_at,omitempty"`
ContactedAt *time.Time `json:"contacted_at"`
Name string `json:"name"`
Online bool `json:"online"`
Status string `json:"status"`
Platform string `json:"platform,omitempty"`
Platform string `json:"platform"`
Projects []struct {
ID int `json:"id"`
Name string `json:"name"`
@ -64,11 +67,12 @@ type RunnerDetails struct {
Path string `json:"path"`
PathWithNamespace string `json:"path_with_namespace"`
} `json:"projects"`
Token string `json:"Token"`
Revision string `json:"revision,omitempty"`
Token string `json:"token"`
Revision string `json:"revision"`
TagList []string `json:"tag_list"`
Version string `json:"version,omitempty"`
Version string `json:"version"`
AccessLevel string `json:"access_level"`
MaximumTimeout int `json:"maximum_timeout"`
}
// ListRunnersOptions represents the available ListRunners() options.
@ -155,6 +159,7 @@ type UpdateRunnerDetailsOptions struct {
RunUntagged *bool `url:"run_untagged,omitempty" json:"run_untagged,omitempty"`
Locked *bool `url:"locked,omitempty" json:"locked,omitempty"`
AccessLevel *string `url:"access_level,omitempty" json:"access_level,omitempty"`
MaximumTimeout *int `url:"maximum_timeout,omitempty" json:"maximum_timeout,omitempty"`
}
// UpdateRunnerDetails updates details for a given runner.
@ -324,3 +329,82 @@ func (s *RunnersService) DisableProjectRunner(pid interface{}, rid interface{},
return s.client.Do(req, nil)
}
// RegisterNewRunnerOptions represents the available RegisterNewRunner()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/runners.html#register-a-new-runner
type RegisterNewRunnerOptions struct {
Token *string `url:"token" json:"token"`
Description *string `url:"description,omitempty" json:"description,omitempty"`
Info *string `url:"info,omitempty" json:"info,omitempty"`
Active *bool `url:"active,omitempty" json:"active,omitempty"`
Locked *bool `url:"locked,omitempty" json:"locked,omitempty"`
RunUntagged *bool `url:"run_untagged,omitempty" json:"run_untagged,omitempty"`
TagList []string `url:"tag_list[],omitempty" json:"tag_list,omitempty"`
MaximumTimeout *int `url:"maximum_timeout,omitempty" json:"maximum_timeout,omitempty"`
}
// RegisterNewRunner registers a new Runner for the instance.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/runners.html#register-a-new-runner
func (s *RunnersService) RegisterNewRunner(opt *RegisterNewRunnerOptions, options ...OptionFunc) (*Runner, *Response, error) {
req, err := s.client.NewRequest("POST", "runners", opt, options)
if err != nil {
return nil, nil, err
}
var r *Runner
resp, err := s.client.Do(req, &r)
if err != nil {
return nil, resp, err
}
return r, resp, err
}
// DeleteRegisteredRunnerOptions represents the available
// DeleteRegisteredRunner() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/runners.html#delete-a-registered-runner
type DeleteRegisteredRunnerOptions struct {
Token *string `url:"token" json:"token"`
}
// DeleteRegisteredRunner registers a new Runner for the instance.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/runners.html#delete-a-registered-runner
func (s *RunnersService) DeleteRegisteredRunner(opt *DeleteRegisteredRunnerOptions, options ...OptionFunc) (*Response, error) {
req, err := s.client.NewRequest("DELETE", "runners", opt, options)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}
// VerifyRegisteredRunnerOptions represents the available
// VerifyRegisteredRunner() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/runners.html#verify-authentication-for-a-registered-runner
type VerifyRegisteredRunnerOptions struct {
Token *string `url:"token" json:"token"`
}
// VerifyRegisteredRunner registers a new Runner for the instance.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/runners.html#verify-authentication-for-a-registered-runner
func (s *RunnersService) VerifyRegisteredRunner(opt *VerifyRegisteredRunnerOptions, options ...OptionFunc) (*Response, error) {
req, err := s.client.NewRequest("POST", "runners/verify", opt, options)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}

View File

@ -45,8 +45,10 @@ type Service struct {
MergeRequestsEvents bool `json:"merge_requests_events"`
TagPushEvents bool `json:"tag_push_events"`
NoteEvents bool `json:"note_events"`
ConfidentialNoteEvents bool `json:"confidential_note_events"`
PipelineEvents bool `json:"pipeline_events"`
JobEvents bool `json:"job_events"`
WikiPageEvents bool `json:"wiki_page_events"`
}
// SetGitLabCIServiceOptions represents the available SetGitLabCIService()
@ -252,7 +254,28 @@ type SlackService struct {
// GitLab API docs:
// https://docs.gitlab.com/ce/api/services.html#slack
type SlackServiceProperties struct {
NotifyOnlyBrokenPipelines bool `json:"notify_only_broken_pipelines"`
// Note: NotifyOnlyBrokenPipelines and NotifyOnlyDefaultBranch are not
// just "bool" because in some cases gitlab returns
// "notify_only_broken_pipelines": true, and in other cases
// "notify_only_broken_pipelines": "1". The same is for
// "notify_only_default_branch" field.
// We need to handle this, until the bug will be fixed.
// Ref: https://gitlab.com/gitlab-org/gitlab-ce/issues/50122
NotifyOnlyBrokenPipelines BoolValue `url:"notify_only_broken_pipelines,omitempty" json:"notify_only_broken_pipelines,omitempty"`
NotifyOnlyDefaultBranch BoolValue `url:"notify_only_default_branch,omitempty" json:"notify_only_default_branch,omitempty"`
WebHook string `url:"webhook,omitempty" json:"webhook,omitempty"`
Username string `url:"username,omitempty" json:"username,omitempty"`
Channel string `url:"channel,omitempty" json:"channel,omitempty"`
PushChannel string `url:"push_channel,omitempty" json:"push_channel,omitempty"`
IssueChannel string `url:"issue_channel,omitempty" json:"issue_channel,omitempty"`
ConfidentialIssueChannel string `url:"confidential_issue_channel,omitempty" json:"confidential_issue_channel,omitempty"`
MergeRequestChannel string `url:"merge_request_channel,omitempty" json:"merge_request_channel,omitempty"`
NoteChannel string `url:"note_channel,omitempty" json:"note_channel,omitempty"`
ConfidentialNoteChannel string `url:"confidential_note_channel,omitempty" json:"confidential_note_channel,omitempty"`
TagPushChannel string `url:"tag_push_channel,omitempty" json:"tag_push_channel,omitempty"`
PipelineChannel string `url:"pipeline_channel,omitempty" json:"pipeline_channel,omitempty"`
WikiPageChannel string `url:"wiki_page_channel,omitempty" json:"wiki_page_channel,omitempty"`
}
// GetSlackService gets Slack service settings for a project.
@ -289,6 +312,29 @@ type SetSlackServiceOptions struct {
WebHook *string `url:"webhook,omitempty" json:"webhook,omitempty"`
Username *string `url:"username,omitempty" json:"username,omitempty"`
Channel *string `url:"channel,omitempty" json:"channel,omitempty"`
NotifyOnlyBrokenPipelines *bool `url:"notify_only_broken_pipelines,omitempty" json:"notify_only_broken_pipelines,omitempty"`
NotifyOnlyDefaultBranch *bool `url:"notify_only_default_branch,omitempty" json:"notify_only_default_branch,omitempty"`
PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"`
PushChannel *string `url:"push_channel,omitempty" json:"push_channel,omitempty"`
IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"`
IssueChannel *string `url:"issue_channel,omitempty" json:"issue_channel,omitempty"`
ConfidentialIssuesEvents *bool `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"`
ConfidentialIssueChannel *string `url:"confidential_issue_channel,omitempty" json:"confidential_issue_channel,omitempty"`
MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"`
MergeRequestChannel *string `url:"merge_request_channel,omitempty" json:"merge_request_channel,omitempty"`
TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"`
TagPushChannel *string `url:"tag_push_channel,omitempty" json:"tag_push_channel,omitempty"`
NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"`
NoteChannel *string `url:"note_channel,omitempty" json:"note_channel,omitempty"`
ConfidentialNoteEvents *bool `url:"confidential_note_events" json:"confidential_note_events"`
// TODO: Currently, GitLab ignores this option (not implemented yet?), so
// there is no way to set it. Uncomment when this is fixed.
// See: https://gitlab.com/gitlab-org/gitlab-ce/issues/49730
//ConfidentialNoteChannel *string `json:"confidential_note_channel,omitempty"`
PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"`
PipelineChannel *string `url:"pipeline_channel,omitempty" json:"pipeline_channel,omitempty"`
WikiPageChannel *string `url:"wiki_page_channel,omitempty" json:"wiki_page_channel,omitempty"`
WikiPageEvents *bool `url:"wiki_page_events" json:"wiki_page_events"`
}
// SetSlackService sets Slack service for a project
@ -513,3 +559,91 @@ func (s *ServicesService) DeleteJenkinsCIService(pid interface{}, options ...Opt
return s.client.Do(req, nil)
}
// MicrosoftTeamsService represents Microsoft Teams service settings.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/services.html#microsoft-teams
type MicrosoftTeamsService struct {
Service
Properties *MicrosoftTeamsServiceProperties `json:"properties"`
}
// MicrosoftTeamsServiceProperties represents Microsoft Teams specific properties.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/services.html#microsoft-teams
type MicrosoftTeamsServiceProperties struct {
WebHook string `json:"webhook"`
}
// GetMicrosoftTeamsService gets MicrosoftTeams service settings for a project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/services.html#get-microsoft-teams-service-settings
func (s *ServicesService) GetMicrosoftTeamsService(pid interface{}, options ...OptionFunc) (*MicrosoftTeamsService, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/services/microsoft-teams", url.QueryEscape(project))
req, err := s.client.NewRequest("GET", u, nil, options)
if err != nil {
return nil, nil, err
}
svc := new(MicrosoftTeamsService)
resp, err := s.client.Do(req, svc)
if err != nil {
return nil, resp, err
}
return svc, resp, err
}
// SetMicrosoftTeamsServiceOptions represents the available SetMicrosoftTeamsService()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/services.html#create-edit-microsoft-teams-service
type SetMicrosoftTeamsServiceOptions struct {
WebHook *string `url:"webhook,omitempty" json:"webhook,omitempty"`
}
// SetMicrosoftTeamsService sets Microsoft Teams service for a project
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/services.html#create-edit-microsoft-teams-service
func (s *ServicesService) SetMicrosoftTeamsService(pid interface{}, opt *SetMicrosoftTeamsServiceOptions, options ...OptionFunc) (*Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/services/microsoft-teams", url.QueryEscape(project))
req, err := s.client.NewRequest("PUT", u, opt, options)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}
// DeleteMicrosoftTeamsService deletes Microsoft Teams service for project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/services.html#delete-microsoft-teams-service
func (s *ServicesService) DeleteMicrosoftTeamsService(pid interface{}, options ...OptionFunc) (*Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/services/microsoft-teams", url.QueryEscape(project))
req, err := s.client.NewRequest("DELETE", u, nil, options)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}

View File

@ -1,78 +0,0 @@
//
// Copyright 2017, Sander van Harmelen
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package gitlab
import "time"
// SessionService handles communication with the session related methods of
// the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/session.html
type SessionService struct {
client *Client
}
// Session represents a GitLab session.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/session.html#session
type Session struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Name string `json:"name"`
PrivateToken string `json:"private_token"`
Blocked bool `json:"blocked"`
CreatedAt *time.Time `json:"created_at"`
Bio interface{} `json:"bio"`
Skype string `json:"skype"`
Linkedin string `json:"linkedin"`
Twitter string `json:"twitter"`
WebsiteURL string `json:"website_url"`
DarkScheme bool `json:"dark_scheme"`
ThemeID int `json:"theme_id"`
IsAdmin bool `json:"is_admin"`
CanCreateGroup bool `json:"can_create_group"`
CanCreateTeam bool `json:"can_create_team"`
CanCreateProject bool `json:"can_create_project"`
}
// GetSessionOptions represents the available Session() options.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/session.html#session
type GetSessionOptions struct {
Login *string `url:"login,omitempty" json:"login,omitempty"`
Email *string `url:"email,omitempty" json:"email,omitempty"`
Password *string `url:"password,omitempty" json:"password,omitempty"`
}
// GetSession logs in to get private token.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/session.html#session
func (s *SessionService) GetSession(opt *GetSessionOptions, options ...OptionFunc) (*Session, *Response, error) {
req, err := s.client.NewRequest("POST", "session", opt, options)
if err != nil {
return nil, nil, err
}
session := new(Session)
resp, err := s.client.Do(req, session)
if err != nil {
return nil, resp, err
}
return session, resp, err
}

View File

@ -32,7 +32,7 @@ type UsersService struct {
// User represents a GitLab user.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/users.html
// GitLab API docs: https://docs.gitlab.com/ee/api/users.html
type User struct {
ID int `json:"id"`
Username string `json:"username"`
@ -42,6 +42,7 @@ type User struct {
CreatedAt *time.Time `json:"created_at"`
Bio string `json:"bio"`
Location string `json:"location"`
PublicEmail string `json:"public_email"`
Skype string `json:"skype"`
Linkedin string `json:"linkedin"`
Twitter string `json:"twitter"`
@ -59,9 +60,12 @@ type User struct {
ProjectsLimit int `json:"projects_limit"`
CurrentSignInAt *time.Time `json:"current_sign_in_at"`
LastSignInAt *time.Time `json:"last_sign_in_at"`
ConfirmedAt *time.Time `json:"confirmed_at"`
TwoFactorEnabled bool `json:"two_factor_enabled"`
Identities []*UserIdentity `json:"identities"`
External bool `json:"external"`
PrivateProfile bool `json:"private_profile"`
SharedRunnersMinutesLimit int `json:"shared_runners_minutes_limit"`
}
// UserIdentity represents a user identity.

5
vendor/github.com/zmb3/spotify/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,5 @@
language: go
go:
- 1.8
- 1.9

201
vendor/github.com/zmb3/spotify/LICENSE generated vendored Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

106
vendor/github.com/zmb3/spotify/README.md generated vendored Normal file
View File

@ -0,0 +1,106 @@
Spotify
=======
[![GoDoc](https://godoc.org/github.com/zmb3/spotify?status.svg)](http://godoc.org/github.com/zmb3/spotify)
[![Build status](https://ci.appveyor.com/api/projects/status/1nr9vv0jqq438nj2?svg=true)](https://ci.appveyor.com/project/zmb3/spotify)
[![Build Status](https://travis-ci.org/zmb3/spotify.svg)](https://travis-ci.org/zmb3/spotify)
This is a Go wrapper for working with Spotify's
[Web API](https://developer.spotify.com/web-api/).
It aims to support every task listed in the Web API Endpoint Reference,
located [here](https://developer.spotify.com/web-api/endpoint-reference/).
By using this library you agree to Spotify's
[Developer Terms of Use](https://developer.spotify.com/developer-terms-of-use/).
## Installation
To install the library, simply
`go get github.com/zmb3/spotify`
## Authentication
Spotify uses OAuth2 for authentication and authorization.
As of May 29, 2017 _all_ Web API endpoints require an access token.
You can authenticate using a client credentials flow, but this does not provide
any authorization to access a user's private data. For most use cases, you'll
want to use the authorization code flow. This package includes an `Authenticator`
type to handle the details for you.
Start by registering your application at the following page:
https://developer.spotify.com/my-applications/.
You'll get a __client ID__ and __secret key__ for your application. An easy way to
provide this data to your application is to set the SPOTIFY_ID and SPOTIFY_SECRET
environment variables. If you choose not to use environment variables, you can
provide this data manually.
````Go
// the redirect URL must be an exact match of a URL you've registered for your application
// scopes determine which permissions the user is prompted to authorize
auth := spotify.NewAuthenticator(redirectURL, spotify.ScopeUserReadPrivate)
// if you didn't store your ID and secret key in the specified environment variables,
// you can set them manually here
auth.SetAuthInfo(clientID, secretKey)
// get the user to this URL - how you do that is up to you
// you should specify a unique state string to identify the session
url := auth.AuthURL(state)
// the user will eventually be redirected back to your redirect URL
// typically you'll have a handler set up like the following:
func redirectHandler(w http.ResponseWriter, r *http.Request) {
// use the same state string here that you used to generate the URL
token, err := auth.Token(state, r)
if err != nil {
http.Error(w, "Couldn't get token", http.StatusNotFound)
return
}
// create a client using the specified token
client := auth.NewClient(token)
// the client can now be used to make authenticated requests
}
````
You may find the following resources useful:
1. Spotify's Web API Authorization Guide:
https://developer.spotify.com/web-api/authorization-guide/
2. Go's OAuth2 package:
https://godoc.org/golang.org/x/oauth2/google
## Helpful Hints
### Optional Parameters
Many of the functions in this package come in two forms - a simple version that
omits optional parameters and uses reasonable defaults, and a more sophisticated
version that accepts additional parameters. The latter is suffixed with `Opt`
to indicate that it accepts some optional parameters.
### Automatic Retries
The API will throttle your requests if you are sending them too rapidly.
The client can be configured to wait and re-attempt the request.
To enable this, set the `AutoRetry` field on the `Client` struct to `true`.
For more information, see Spotify [rate-limits](https://developer.spotify.com/web-api/user-guide/#rate-limiting).
## API Examples
Examples of the API can be found in the [examples](examples) directory.
You may find tools such as [Spotify's Web API Console](https://developer.spotify.com/web-api/console/)
or [Rapid API](https://rapidapi.com/package/SpotifyPublicAPI/functions?utm_source=SpotifyGitHub&utm_medium=button&utm_content=Vendor_GitHub)
valuable for experimenting with the API.

220
vendor/github.com/zmb3/spotify/album.go generated vendored Normal file
View File

@ -0,0 +1,220 @@
package spotify
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
// SimpleAlbum contains basic data about an album.
type SimpleAlbum struct {
// The name of the album.
Name string `json:"name"`
// A slice of SimpleArtists
Artists []SimpleArtist `json:"artists"`
// The field is present when getting an artists
// albums. Possible values are “album”, “single”,
// “compilation”, “appears_on”. Compare to album_type
// this field represents relationship between the artist
// and the album.
AlbumGroup string `json:"album_group"`
// The type of the album: one of "album",
// "single", or "compilation".
AlbumType string `json:"album_type"`
// The SpotifyID for the album.
ID ID `json:"id"`
// The SpotifyURI for the album.
URI URI `json:"uri"`
// The markets in which the album is available,
// identified using ISO 3166-1 alpha-2 country
// codes. Note that al album is considered
// available in a market when at least 1 of its
// tracks is available in that market.
AvailableMarkets []string `json:"available_markets"`
// A link to the Web API enpoint providing full
// details of the album.
Endpoint string `json:"href"`
// The cover art for the album in various sizes,
// widest first.
Images []Image `json:"images"`
// Known external URLs for this album.
ExternalURLs map[string]string `json:"external_urls"`
// The date the album was first released. For example, "1981-12-15".
// Depending on the ReleaseDatePrecision, it might be shown as
// "1981" or "1981-12". You can use ReleaseDateTime to convert this
// to a time.Time value.
ReleaseDate string `json:"release_date"`
// The precision with which ReleaseDate value is known: "year", "month", or "day"
ReleaseDatePrecision string `json:"release_date_precision"`
}
// Copyright contains the copyright statement associated with an album.
type Copyright struct {
// The copyright text for the album.
Text string `json:"text"`
// The type of copyright.
Type string `json:"type"`
}
// FullAlbum provides extra album data in addition to the data provided by SimpleAlbum.
type FullAlbum struct {
SimpleAlbum
Artists []SimpleArtist `json:"artists"`
Copyrights []Copyright `json:"copyrights"`
Genres []string `json:"genres"`
// The popularity of the album, represented as an integer between 0 and 100,
// with 100 being the most popular. Popularity of an album is calculated
// from the popularify of the album's individual tracks.
Popularity int `json:"popularity"`
// The date the album was first released. For example, "1981-12-15".
// Depending on the ReleaseDatePrecision, it might be shown as
// "1981" or "1981-12". You can use ReleaseDateTime to convert this
// to a time.Time value.
ReleaseDate string `json:"release_date"`
// The precision with which ReleaseDate value is known: "year", "month", or "day"
ReleaseDatePrecision string `json:"release_date_precision"`
Tracks SimpleTrackPage `json:"tracks"`
ExternalIDs map[string]string `json:"external_ids"`
}
// SavedAlbum provides info about an album saved to an user's account.
type SavedAlbum struct {
// The date and time the track was saved, represented as an ISO
// 8601 UTC timestamp with a zero offset (YYYY-MM-DDTHH:MM:SSZ).
// You can use the TimestampLayout constant to convert this to
// a time.Time value.
AddedAt string `json:"added_at"`
FullAlbum `json:"album"`
}
// ReleaseDateTime converts the album's ReleaseDate to a time.TimeValue.
// All of the fields in the result may not be valid. For example, if
// f.ReleaseDatePrecision is "month", then only the month and year
// (but not the day) of the result are valid.
func (f *FullAlbum) ReleaseDateTime() time.Time {
if f.ReleaseDatePrecision == "day" {
result, _ := time.Parse(DateLayout, f.ReleaseDate)
return result
}
if f.ReleaseDatePrecision == "month" {
ym := strings.Split(f.ReleaseDate, "-")
year, _ := strconv.Atoi(ym[0])
month, _ := strconv.Atoi(ym[1])
return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
}
year, _ := strconv.Atoi(f.ReleaseDate)
return time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
}
// GetAlbum gets Spotify catalog information for a single album, given its Spotify ID.
func (c *Client) GetAlbum(id ID) (*FullAlbum, error) {
spotifyURL := fmt.Sprintf("%salbums/%s", c.baseURL, id)
var a FullAlbum
err := c.get(spotifyURL, &a)
if err != nil {
return nil, err
}
return &a, nil
}
func toStringSlice(ids []ID) []string {
result := make([]string, len(ids))
for i, str := range ids {
result[i] = str.String()
}
return result
}
// GetAlbums gets Spotify Catalog information for multiple albums, given their
// Spotify IDs. It supports up to 20 IDs in a single call. Albums are returned
// in the order requested. If an album is not found, that position in the
// result slice will be nil.
func (c *Client) GetAlbums(ids ...ID) ([]*FullAlbum, error) {
if len(ids) > 20 {
return nil, errors.New("spotify: exceeded maximum number of albums")
}
spotifyURL := fmt.Sprintf("%salbums?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
var a struct {
Albums []*FullAlbum `json:"albums"`
}
err := c.get(spotifyURL, &a)
if err != nil {
return nil, err
}
return a.Albums, nil
}
// AlbumType represents the type of an album. It can be used to filter
// results when searching for albums.
type AlbumType int
// AlbumType values that can be used to filter which types of albums are
// searched for. These are flags that can be bitwise OR'd together
// to search for multiple types of albums simultaneously.
const (
AlbumTypeAlbum AlbumType = 1 << iota
AlbumTypeSingle = 1 << iota
AlbummTypeAppearsOn = 1 << iota
AlbumTypeCompilation = 1 << iota
)
func (at AlbumType) encode() string {
types := []string{}
if at&AlbumTypeAlbum != 0 {
types = append(types, "album")
}
if at&AlbumTypeSingle != 0 {
types = append(types, "single")
}
if at&AlbummTypeAppearsOn != 0 {
types = append(types, "appears_on")
}
if at&AlbumTypeCompilation != 0 {
types = append(types, "compilation")
}
return strings.Join(types, ",")
}
// GetAlbumTracks gets the tracks for a particular album.
// If you only care about the tracks, this call is more efficient
// than GetAlbum.
func (c *Client) GetAlbumTracks(id ID) (*SimpleTrackPage, error) {
return c.GetAlbumTracksOpt(id, -1, -1)
}
// GetAlbumTracksOpt behaves like GetAlbumTracks, with the exception that it
// allows you to specify extra parameters that limit the number of results returned.
// The maximum number of results to return is specified by limit.
// The offset argument can be used to specify the index of the first track to return.
// It can be used along with limit to reqeust the next set of results.
func (c *Client) GetAlbumTracksOpt(id ID, limit, offset int) (*SimpleTrackPage, error) {
spotifyURL := fmt.Sprintf("%salbums/%s/tracks", c.baseURL, id)
v := url.Values{}
if limit != -1 {
v.Set("limit", strconv.Itoa(limit))
}
if offset != -1 {
v.Set("offset", strconv.Itoa(offset))
}
optional := v.Encode()
if optional != "" {
spotifyURL = spotifyURL + "?" + optional
}
var result SimpleTrackPage
err := c.get(spotifyURL, &result)
if err != nil {
return nil, err
}
return &result, nil
}

152
vendor/github.com/zmb3/spotify/artist.go generated vendored Normal file
View File

@ -0,0 +1,152 @@
package spotify
import (
"fmt"
"net/url"
"strconv"
"strings"
)
// SimpleArtist contains basic info about an artist.
type SimpleArtist struct {
Name string `json:"name"`
ID ID `json:"id"`
// The Spotify URI for the artist.
URI URI `json:"uri"`
// A link to the Web API enpoint providing full details of the artist.
Endpoint string `json:"href"`
ExternalURLs map[string]string `json:"external_urls"`
}
// FullArtist provides extra artist data in addition to what is provided by SimpleArtist.
type FullArtist struct {
SimpleArtist
// The popularity of the artist, expressed as an integer between 0 and 100.
// The artist's popularity is calculated from the popularity of the artist's tracks.
Popularity int `json:"popularity"`
// A list of genres the artist is associated with. For example, "Prog Rock"
// or "Post-Grunge". If not yet classified, the slice is empty.
Genres []string `json:"genres"`
Followers Followers
// Images of the artist in various sizes, widest first.
Images []Image `json:"images"`
}
// GetArtist gets Spotify catalog information for a single artist, given its Spotify ID.
func (c *Client) GetArtist(id ID) (*FullArtist, error) {
spotifyURL := fmt.Sprintf("%sartists/%s", c.baseURL, id)
var a FullArtist
err := c.get(spotifyURL, &a)
if err != nil {
return nil, err
}
return &a, nil
}
// GetArtists gets spotify catalog information for several artists based on their
// Spotify IDs. It supports up to 50 artists in a single call. Artists are
// returned in the order requested. If an artist is not found, that position
// in the result will be nil. Duplicate IDs will result in duplicate artists
// in the result.
func (c *Client) GetArtists(ids ...ID) ([]*FullArtist, error) {
spotifyURL := fmt.Sprintf("%sartists?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
var a struct {
Artists []*FullArtist
}
err := c.get(spotifyURL, &a)
if err != nil {
return nil, err
}
return a.Artists, nil
}
// GetArtistsTopTracks gets Spotify catalog information about an artist's top
// tracks in a particular country. It returns a maximum of 10 tracks. The
// country is specified as an ISO 3166-1 alpha-2 country code.
func (c *Client) GetArtistsTopTracks(artistID ID, country string) ([]FullTrack, error) {
spotifyURL := fmt.Sprintf("%sartists/%s/top-tracks?country=%s", c.baseURL, artistID, country)
var t struct {
Tracks []FullTrack `json:"tracks"`
}
err := c.get(spotifyURL, &t)
if err != nil {
return nil, err
}
return t.Tracks, nil
}
// GetRelatedArtists gets Spotify catalog information about artists similar to a
// given artist. Similarity is based on analysis of the Spotify community's
// listening history. This function returns up to 20 artists that are considered
// related to the specified artist.
func (c *Client) GetRelatedArtists(id ID) ([]FullArtist, error) {
spotifyURL := fmt.Sprintf("%sartists/%s/related-artists", c.baseURL, id)
var a struct {
Artists []FullArtist `json:"artists"`
}
err := c.get(spotifyURL, &a)
if err != nil {
return nil, err
}
return a.Artists, nil
}
// GetArtistAlbums gets Spotify catalog information about an artist's albums.
// It is equivalent to GetArtistAlbumsOpt(artistID, nil).
func (c *Client) GetArtistAlbums(artistID ID) (*SimpleAlbumPage, error) {
return c.GetArtistAlbumsOpt(artistID, nil, nil)
}
// GetArtistAlbumsOpt is just like GetArtistAlbums, but it accepts optional
// parameters used to filter and sort the result.
//
// The AlbumType argument can be used to find a particular type of album. Search
// for multiple types by OR-ing the types together.
func (c *Client) GetArtistAlbumsOpt(artistID ID, options *Options, t *AlbumType) (*SimpleAlbumPage, error) {
spotifyURL := fmt.Sprintf("%sartists/%s/albums", c.baseURL, artistID)
// add optional query string if options were specified
values := url.Values{}
if t != nil {
values.Set("album_type", t.encode())
}
if options != nil {
if options.Country != nil {
values.Set("market", *options.Country)
} else {
// if the market is not specified, Spotify will likely return a lot
// of duplicates (one for each market in which the album is available)
// - prevent this behavior by falling back to the US by default
// TODO: would this ever be the desired behavior?
values.Set("market", CountryUSA)
}
if options.Limit != nil {
values.Set("limit", strconv.Itoa(*options.Limit))
}
if options.Offset != nil {
values.Set("offset", strconv.Itoa(*options.Offset))
}
}
if query := values.Encode(); query != "" {
spotifyURL += "?" + query
}
var p SimpleAlbumPage
err := c.get(spotifyURL, &p)
if err != nil {
return nil, err
}
return &p, nil
}

110
vendor/github.com/zmb3/spotify/audio_analysis.go generated vendored Normal file
View File

@ -0,0 +1,110 @@
package spotify
import (
"fmt"
)
// AudioAnalysis contains a detailed audio analysis for a single track
// identified by its unique Spotify ID. See:
// https://developer.spotify.com/web-api/get-audio-analysis/
type AudioAnalysis struct {
Bars []Marker `json:"bars"`
Beats []Marker `json:"beats"`
Meta AnalysisMeta `json:"meta"`
Sections []Section `json:"sections"`
Segments []Segment `json:"segments"`
Tatums []Marker `json:"tatums"`
Track AnalysisTrack `json:"track"`
}
// Marker represents beats, bars, tatums and are used in segment and section
// descriptions.
type Marker struct {
Start float64 `json:"start"`
Duration float64 `json:"duration"`
Confidence float64 `json:"confidence"`
}
// AnalysisMeta describes details about Spotify's audio analysis of the track
type AnalysisMeta struct {
AnalyzerVersion string `json:"analyzer_version"`
Platform string `json:"platform"`
DetailedStatus string `json:"detailed_status"`
StatusCode int `json:"status"`
Timestamp int64 `json:"timestamp"`
AnalysisTime float64 `json:"analysis_time"`
InputProcess string `json:"input_process"`
}
// Section represents a large variation in rhythm or timbre, e.g. chorus, verse,
// bridge, guitar solo, etc. Each section contains its own descriptions of
// tempo, key, mode, time_signature, and loudness.
type Section struct {
Marker
Loudness float64 `json:"loudness"`
Tempo float64 `json:"tempo"`
TempoConfidence float64 `json:"tempo_confidence"`
Key Key `json:"key"`
KeyConfidence float64 `json:"key_confidence"`
Mode Mode `json:"mode"`
ModeConfidence float64 `json:"mode_confidence"`
TimeSignature int `json:"time_signature"`
TimeSignatureConfidence float64 `json:"time_signature_confidence"`
}
// Segment is characterized by it's perceptual onset and duration in seconds,
// loudness (dB), pitch and timbral content.
type Segment struct {
Marker
LoudnessStart float64 `json:"loudness_start"`
LoudnessMaxTime float64 `json:"loudness_max_time"`
LoudnessMax float64 `json:"loudness_max"`
LoudnessEnd float64 `json:"loudness_end"`
Pitches []float64 `json:"pitches"`
Timbre []float64 `json:"timbre"`
}
// AnalysisTrack contains audio analysis data about the track as a whole
type AnalysisTrack struct {
NumSamples int64 `json:"num_samples"`
Duration float64 `json:"duration"`
SampleMD5 string `json:"sample_md5"`
OffsetSeconds int `json:"offset_seconds"`
WindowSeconds int `json:"window_seconds"`
AnalysisSampleRate int64 `json:"analysis_sample_rate"`
AnalysisChannels int `json:"analysis_channels"`
EndOfFadeIn float64 `json:"end_of_fade_in"`
StartOfFadeOut float64 `json:"start_of_fade_out"`
Loudness float64 `json:"loudness"`
Tempo float64 `json:"tempo"`
TempoConfidence float64 `json:"tempo_confidence"`
TimeSignature int `json:"time_signature"`
TimeSignatureConfidence float64 `json:"time_signature_confidence"`
Key Key `json:"key"`
KeyConfidence float64 `json:"key_confidence"`
Mode Mode `json:"mode"`
ModeConfidence float64 `json:"mode_confidence"`
CodeString string `json:"codestring"`
CodeVersion float64 `json:"code_version"`
EchoprintString string `json:"echoprintstring"`
EchoprintVersion float64 `json:"echoprint_version"`
SynchString string `json:"synchstring"`
SynchVersion float64 `json:"synch_version"`
RhythmString string `json:"rhythmstring"`
RhythmVersion float64 `json:"rhythm_version"`
}
// GetAudioAnalysis queries the Spotify web API for an audio analysis of a
// single track.
func (c *Client) GetAudioAnalysis(id ID) (*AudioAnalysis, error) {
url := fmt.Sprintf("%saudio-analysis/%s", c.baseURL, id)
temp := AudioAnalysis{}
err := c.get(url, &temp)
if err != nil {
return nil, err
}
return &temp, nil
}

123
vendor/github.com/zmb3/spotify/audio_features.go generated vendored Normal file
View File

@ -0,0 +1,123 @@
package spotify
import (
"fmt"
"strings"
)
// AudioFeatures contains various high-level acoustic attributes
// for a particular track.
type AudioFeatures struct {
//Acousticness is a confidence measure from 0.0 to 1.0 of whether
// the track is acoustic. A value of 1.0 represents high confidence
// that the track is acoustic.
Acousticness float32 `json:"acousticness"`
// An HTTP URL to access the full audio analysis of the track.
// The URL is cryptographically signed and configured to expire
// after roughly 10 minutes. Do not store these URLs for later use.
AnalysisURL string `json:"analysis_url"`
// Danceability describes how suitable a track is for dancing based
// on a combination of musical elements including tempo, rhythm stability,
// beat strength, and overall regularity. A value of 0.0 is least danceable
// and 1.0 is most danceable.
Danceability float32 `json:"danceability"`
// The length of the track in milliseconds.
Duration int `json:"duration_ms"`
// Energy is a measure from 0.0 to 1.0 and represents a perceptual mesaure
// of intensity and activity. Typically, energetic tracks feel fast, loud,
// and noisy.
Energy float32 `json:"energy"`
// The Spotify ID for the track.
ID ID `json:"id"`
// Predicts whether a track contains no vocals. "Ooh" and "aah" sounds are
// treated as instrumental in this context. Rap or spoken words are clearly
// "vocal". The closer the Instrumentalness value is to 1.0, the greater
// likelihood the track contains no vocal content. Values above 0.5 are
// intended to represent instrumental tracks, but confidence is higher as the
// value approaches 1.0.
Instrumentalness float32 `json:"instrumentalness"`
// The key the track is in. Integers map to pitches using standard Pitch Class notation
// (https://en.wikipedia.org/wiki/Pitch_class).
Key int `json:"key"`
// Detects the presence of an audience in the recording. Higher liveness
// values represent an increased probability that the track was performed live.
// A value above 0.8 provides strong likelihook that the track is live.
Liveness float32 `json:"liveness"`
// The overall loudness of a track in decibels (dB). Loudness values are
// averaged across the entire track and are useful for comparing the relative
// loudness of tracks. Typical values range between -60 and 0 dB.
Loudness float32 `json:"loudness"`
// Mode indicates the modality (major or minor) of a track.
Mode int `json:"mode"`
// Detects the presence of spoken words in a track. The more exclusively
// speech-like the recording, the closer to 1.0 the speechiness will be.
// Values above 0.66 describe tracks that are probably made entirely of
// spoken words. Values between 0.33 and 0.66 describe tracks that may
// contain both music and speech, including such cases as rap music.
// Values below 0.33 most likely represent music and other non-speech-like tracks.
Speechiness float32 `json:"speechiness"`
// The overall estimated tempo of the track in beats per minute (BPM).
Tempo float32 `json:"tempo"`
// An estimated overall time signature of a track. The time signature (meter)
// is a notational convention to specify how many beats are in each bar (or measure).
TimeSignature int `json:"time_signature"`
// A link to the Web API endpoint providing full details of the track.
TrackURL string `json:"track_href"`
// The Spotify URI for the track.
URI URI `json:"uri"`
// A measure from 0.0 to 1.0 describing the musical positiveness conveyed
// by a track. Tracks with high valence sound more positive (e.g. happy,
// cheerful, euphoric), while tracks with low valence sound more negative
// (e.g. sad, depressed, angry).
Valence float32 `json:"valence"`
}
// Key represents a pitch using Pitch Class notation.
type Key int
const (
C Key = iota
CSharp
D
DSharp
E
F
FSharp
G
GSharp
A
ASharp
B
DFlat = CSharp
EFlat = DSharp
GFlat = FSharp
AFlat = GSharp
BFlat = ASharp
)
// Mode indicates the modality (major or minor) of a track.
type Mode int
const (
Minor Mode = iota
Major
)
// GetAudioFeatures queries the Spotify Web API for various
// high-level acoustic attributes of audio tracks.
// Objects are returned in the order requested. If an object
// is not found, a nil value is returned in the appropriate position.
func (c *Client) GetAudioFeatures(ids ...ID) ([]*AudioFeatures, error) {
url := fmt.Sprintf("%saudio-features?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
temp := struct {
F []*AudioFeatures `json:"audio_features"`
}{}
err := c.get(url, &temp)
if err != nil {
return nil, err
}
return temp.F, nil
}

180
vendor/github.com/zmb3/spotify/auth.go generated vendored Normal file
View File

@ -0,0 +1,180 @@
package spotify
import (
"context"
"crypto/tls"
"errors"
"net/http"
"os"
"golang.org/x/oauth2"
)
const (
// AuthURL is the URL to Spotify Accounts Service's OAuth2 endpoint.
AuthURL = "https://accounts.spotify.com/authorize"
// TokenURL is the URL to the Spotify Accounts Service's OAuth2
// token endpoint.
TokenURL = "https://accounts.spotify.com/api/token"
)
// Scopes let you specify exactly which types of data your application wants to access.
// The set of scopes you pass in your authentication request determines what access the
// permissions the user is asked to grant.
const (
// ScopeImageUpload seeks permission to upload images to Spotify on your behalf.
ScopeImageUpload = "ugc-image-upload"
// ScopePlaylistReadPrivate seeks permission to read
// a user's private playlists.
ScopePlaylistReadPrivate = "playlist-read-private"
// ScopePlaylistModifyPublic seeks write access
// to a user's public playlists.
ScopePlaylistModifyPublic = "playlist-modify-public"
// ScopePlaylistModifyPrivate seeks write access to
// a user's private playlists.
ScopePlaylistModifyPrivate = "playlist-modify-private"
// ScopePlaylistReadCollaborative seeks permission to
// access a user's collaborative playlists.
ScopePlaylistReadCollaborative = "playlist-read-collaborative"
// ScopeUserFollowModify seeks write/delete access to
// the list of artists and other users that a user follows.
ScopeUserFollowModify = "user-follow-modify"
// ScopeUserFollowRead seeks read access to the list of
// artists and other users that a user follows.
ScopeUserFollowRead = "user-follow-read"
// ScopeUserLibraryModify seeks write/delete access to a
// user's "Your Music" library.
ScopeUserLibraryModify = "user-library-modify"
// ScopeUserLibraryRead seeks read access to a user's "Your Music" library.
ScopeUserLibraryRead = "user-library-read"
// ScopeUserReadPrivate seeks read access to a user's
// subsription details (type of user account).
ScopeUserReadPrivate = "user-read-private"
// ScopeUserReadEmail seeks read access to a user's email address.
ScopeUserReadEmail = "user-read-email"
// ScopeUserReadBirthdate seeks read access to a user's birthdate.
ScopeUserReadBirthdate = "user-read-birthdate"
// ScopeUserReadCurrentlyPlaying seeks read access to a user's currently playing track
ScopeUserReadCurrentlyPlaying = "user-read-currently-playing"
// ScopeUserReadPlaybackState seeks read access to the user's current playback state
ScopeUserReadPlaybackState = "user-read-playback-state"
// ScopeUserModifyPlaybackState seeks write access to the user's current playback state
ScopeUserModifyPlaybackState = "user-modify-playback-state"
// ScopeUserReadRecentlyPlayed allows access to a user's recently-played songs
ScopeUserReadRecentlyPlayed = "user-read-recently-played"
// ScopeUserTopRead seeks read access to a user's top tracks and artists
ScopeUserTopRead = "user-top-read"
)
// Authenticator provides convenience functions for implementing the OAuth2 flow.
// You should always use `NewAuthenticator` to make them.
//
// Example:
//
// a := spotify.NewAuthenticator(redirectURL, spotify.ScopeUserLibaryRead, spotify.ScopeUserFollowRead)
// // direct user to Spotify to log in
// http.Redirect(w, r, a.AuthURL("state-string"), http.StatusFound)
//
// // then, in redirect handler:
// token, err := a.Token(state, r)
// client := a.NewClient(token)
//
type Authenticator struct {
config *oauth2.Config
context context.Context
}
// NewAuthenticator creates an authenticator which is used to implement the
// OAuth2 authorization flow. The redirectURL must exactly match one of the
// URLs specified in your Spotify developer account.
//
// By default, NewAuthenticator pulls your client ID and secret key from the
// SPOTIFY_ID and SPOTIFY_SECRET environment variables. If you'd like to provide
// them from some other source, you can call `SetAuthInfo(id, key)` on the
// returned authenticator.
func NewAuthenticator(redirectURL string, scopes ...string) Authenticator {
cfg := &oauth2.Config{
ClientID: os.Getenv("SPOTIFY_ID"),
ClientSecret: os.Getenv("SPOTIFY_SECRET"),
RedirectURL: redirectURL,
Scopes: scopes,
Endpoint: oauth2.Endpoint{
AuthURL: AuthURL,
TokenURL: TokenURL,
},
}
// disable HTTP/2 for DefaultClient, see: https://github.com/zmb3/spotify/issues/20
tr := &http.Transport{
TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: tr})
return Authenticator{
config: cfg,
context: ctx,
}
}
// SetAuthInfo overwrites the client ID and secret key used by the authenticator.
// You can use this if you don't want to store this information in environment variables.
func (a *Authenticator) SetAuthInfo(clientID, secretKey string) {
a.config.ClientID = clientID
a.config.ClientSecret = secretKey
}
// AuthURL returns a URL to the the Spotify Accounts Service's OAuth2 endpoint.
//
// State is a token to protect the user from CSRF attacks. You should pass the
// same state to `Token`, where it will be validated. For more info, refer to
// http://tools.ietf.org/html/rfc6749#section-10.12.
func (a Authenticator) AuthURL(state string) string {
return a.config.AuthCodeURL(state)
}
// Token pulls an authorization code from an HTTP request and attempts to exchange
// it for an access token. The standard use case is to call Token from the handler
// that handles requests to your application's redirect URL.
func (a Authenticator) Token(state string, r *http.Request) (*oauth2.Token, error) {
values := r.URL.Query()
if e := values.Get("error"); e != "" {
return nil, errors.New("spotify: auth failed - " + e)
}
code := values.Get("code")
if code == "" {
return nil, errors.New("spotify: didn't get access code")
}
actualState := values.Get("state")
if actualState != state {
return nil, errors.New("spotify: redirect state parameter doesn't match")
}
return a.config.Exchange(a.context, code)
}
// Exchange is like Token, except it allows you to manually specify the access
// code instead of pulling it out of an HTTP request.
func (a Authenticator) Exchange(code string) (*oauth2.Token, error) {
return a.config.Exchange(a.context, code)
}
// NewClient creates a Client that will use the specified access token for its API requests.
func (a Authenticator) NewClient(token *oauth2.Token) Client {
client := a.config.Client(a.context, token)
return Client{
http: client,
baseURL: baseAddress,
}
}
// Token gets the client's current token.
func (c *Client) Token() (*oauth2.Token, error) {
transport, ok := c.http.Transport.(*oauth2.Transport)
if !ok {
return nil, errors.New("spotify: oauth2 transport type not correct")
}
t, err := transport.Source.Token()
if err != nil {
return nil, err
}
return t, nil
}

140
vendor/github.com/zmb3/spotify/category.go generated vendored Normal file
View File

@ -0,0 +1,140 @@
package spotify
import (
"fmt"
"net/url"
"strconv"
)
// Category is used by Spotify to tag items in. For example, on the Spotify
// player's "Browse" tab.
type Category struct {
// A link to the Web API endpoint returning full details of the category
Endpoint string `json:"href"`
// The category icon, in various sizes
Icons []Image `json:"icons"`
// The Spotify category ID. This isn't a base-62 Spotify ID, its just
// a short string that describes and identifies the category (ie "party").
ID string `json:"id"`
// The name of the category
Name string `json:"name"`
}
// GetCategoryOpt is like GetCategory, but it accepts optional arguments.
// The country parameter is an ISO 3166-1 alpha-2 country code. It can be
// used to ensure that the category exists for a particular country. The
// locale argument is an ISO 639 language code and an ISO 3166-1 alpha-2
// country code, separated by an underscore. It can be used to get the
// category strings in a particular language (for example: "es_MX" means
// get categories in Mexico, returned in Spanish).
//
// This call requries authorization.
func (c *Client) GetCategoryOpt(id, country, locale string) (Category, error) {
cat := Category{}
spotifyURL := fmt.Sprintf("%sbrowse/categories/%s", c.baseURL, id)
values := url.Values{}
if country != "" {
values.Set("country", country)
}
if locale != "" {
values.Set("locale", locale)
}
if query := values.Encode(); query != "" {
spotifyURL += "?" + query
}
err := c.get(spotifyURL, &cat)
if err != nil {
return cat, err
}
return cat, err
}
// GetCategory gets a single category used to tag items in Spotify
// (on, for example, the Spotify player's Browse tab).
func (c *Client) GetCategory(id string) (Category, error) {
return c.GetCategoryOpt(id, "", "")
}
// GetCategoryPlaylists gets a list of Spotify playlists tagged with a paricular category.
func (c *Client) GetCategoryPlaylists(catID string) (*SimplePlaylistPage, error) {
return c.GetCategoryPlaylistsOpt(catID, nil)
}
// GetCategoryPlaylistsOpt is like GetCategoryPlaylists, but it accepts optional
// arguments.
func (c *Client) GetCategoryPlaylistsOpt(catID string, opt *Options) (*SimplePlaylistPage, error) {
spotifyURL := fmt.Sprintf("%sbrowse/categories/%s/playlists", c.baseURL, catID)
if opt != nil {
values := url.Values{}
if opt.Country != nil {
values.Set("country", *opt.Country)
}
if opt.Limit != nil {
values.Set("limit", strconv.Itoa(*opt.Limit))
}
if opt.Offset != nil {
values.Set("offset", strconv.Itoa(*opt.Offset))
}
if query := values.Encode(); query != "" {
spotifyURL += "?" + query
}
}
wrapper := struct {
Playlists SimplePlaylistPage `json:"playlists"`
}{}
err := c.get(spotifyURL, &wrapper)
if err != nil {
return nil, err
}
return &wrapper.Playlists, nil
}
// GetCategories gets a list of categories used to tag items in Spotify
// (on, for example, the Spotify player's "Browse" tab).
func (c *Client) GetCategories() (*CategoryPage, error) {
return c.GetCategoriesOpt(nil, "")
}
// GetCategoriesOpt is like GetCategories, but it accepts optional parameters.
//
// The locale option can be used to get the results in a particular language.
// It consists of an ISO 639 language code and an ISO 3166-1 alpha-2 country
// code, separated by an underscore. Specify the empty string to have results
// returned in the Spotify default language (American English).
func (c *Client) GetCategoriesOpt(opt *Options, locale string) (*CategoryPage, error) {
spotifyURL := c.baseURL + "browse/categories"
values := url.Values{}
if locale != "" {
values.Set("locale", locale)
}
if opt != nil {
if opt.Country != nil {
values.Set("country", *opt.Country)
}
if opt.Limit != nil {
values.Set("limit", strconv.Itoa(*opt.Limit))
}
if opt.Offset != nil {
values.Set("offset", strconv.Itoa(*opt.Offset))
}
}
if query := values.Encode(); query != "" {
spotifyURL += "?" + query
}
wrapper := struct {
Categories CategoryPage `json:"categories"`
}{}
err := c.get(spotifyURL, &wrapper)
if err != nil {
return nil, err
}
return &wrapper.Categories, nil
}

32
vendor/github.com/zmb3/spotify/countries.go generated vendored Normal file
View File

@ -0,0 +1,32 @@
package spotify
// ISO 3166-1 alpha 2 country codes.
//
// see: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
const (
CountryArgentina = "AR"
CountryAustralia = "AU"
CountryAustria = "AT"
CountryBelarus = "BY"
CountryBelgium = "BE"
CountryBrazil = "BR"
CountryCanada = "CA"
CountryChile = "CL"
CountryChina = "CN"
CountryGermany = "DE"
CountryHongKong = "HK"
CountryIreland = "IE"
CountryIndia = "IN"
CountryItaly = "IT"
CountryJapan = "JP"
CountrySpain = "ES"
CountryFinland = "FI"
CountryFrance = "FR"
CountryMexico = "MX"
CountryNewZealand = "NZ"
CountryRussia = "RU"
CountrySwitzerland = "CH"
CountryUnitedArabEmirates = "AE"
CountryUnitedKingdom = "GB"
CountryUSA = "US"
)

37
vendor/github.com/zmb3/spotify/cursor.go generated vendored Normal file
View File

@ -0,0 +1,37 @@
package spotify
// This file contains the types that implement Spotify's cursor-based
// paging object. Like the standard paging object, this object is a
// container for a set of items. Unlike the standard paging object, a
// cursor-based paging object does not provide random access to the results.
// Cursor contains a key that can be used to find the next set
// of items.
type Cursor struct {
After string `json:"after"`
}
// cursorPage contains all of the fields in a Spotify cursor-based
// paging object, except for the actual items. This type is meant
// to be embedded in other types that add the Items field.
type cursorPage struct {
// A link to the Web API endpoint returning the full
// result of this request.
Endpoint string `json:"href"`
// The maximum number of items returned, as set in the query
// (or default value if unset).
Limit int `json:"limit"`
// The URL to the next set of items.
Next string `json:"next"`
// The total number of items available to return.
Total int `json:"total"`
// The cursor used to find the next set of items.
Cursor Cursor `json:"cursors"`
}
// FullArtistCursorPage is a cursor-based paging object containing
// a set of FullArtist objects.
type FullArtistCursorPage struct {
cursorPage
Artists []FullArtist `json:"items"`
}

5
vendor/github.com/zmb3/spotify/full_tests.bat generated vendored Normal file
View File

@ -0,0 +1,5 @@
@echo off
REM - The tests that actually hit the Spotify Web API don't run by default.
REM - Use this script to run them in addition to the standard unit tests.
cmd /C "set FULLTEST=y && go test %*"

61
vendor/github.com/zmb3/spotify/library.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
package spotify
import (
"errors"
"fmt"
"net/http"
"strings"
)
// UserHasTracks checks if one or more tracks are saved to the current user's
// "Your Music" library.
func (c *Client) UserHasTracks(ids ...ID) ([]bool, error) {
if l := len(ids); l == 0 || l > 50 {
return nil, errors.New("spotify: UserHasTracks supports 1 to 50 IDs per call")
}
spotifyURL := fmt.Sprintf("%sme/tracks/contains?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
var result []bool
err := c.get(spotifyURL, &result)
if err != nil {
return nil, err
}
return result, err
}
// AddTracksToLibrary saves one or more tracks to the current user's
// "Your Music" library. This call requires the ScopeUserLibraryModify scope.
// A track can only be saved once; duplicate IDs are ignored.
func (c *Client) AddTracksToLibrary(ids ...ID) error {
return c.modifyLibraryTracks(true, ids...)
}
// RemoveTracksFromLibrary removes one or more tracks from the current user's
// "Your Music" library. This call requires the ScopeUserModifyLibrary scope.
// Trying to remove a track when you do not have the user's authorization
// results in a `spotify.Error` with the status code set to http.StatusUnauthorized.
func (c *Client) RemoveTracksFromLibrary(ids ...ID) error {
return c.modifyLibraryTracks(false, ids...)
}
func (c *Client) modifyLibraryTracks(add bool, ids ...ID) error {
if l := len(ids); l == 0 || l > 50 {
return errors.New("spotify: this call supports 1 to 50 IDs per call")
}
spotifyURL := fmt.Sprintf("%sme/tracks?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
method := "DELETE"
if add {
method = "PUT"
}
req, err := http.NewRequest(method, spotifyURL, nil)
if err != nil {
return err
}
err = c.execute(req, nil)
if err != nil {
return err
}
return nil
}

87
vendor/github.com/zmb3/spotify/page.go generated vendored Normal file
View File

@ -0,0 +1,87 @@
package spotify
import (
"errors"
)
// ErrNoMorePages is the error returned when you attempt to get the next
// (or previous) set of data but you've reached the end of the data set.
var ErrNoMorePages = errors.New("spotify: no more pages")
// This file contains the types that implement Spotify's paging object.
// See: https://developer.spotify.com/web-api/object-model/#paging-object
// basePage contains all of the fields in a Spotify paging object, except
// for the actual items. This type is meant to be embedded in other types
// that add the Items field.
type basePage struct {
// A link to the Web API Endpoint returning the full
// result of this request.
Endpoint string `json:"href"`
// The maximum number of items in the response, as set
// in the query (or default value if unset).
Limit int `json:"limit"`
// The offset of the items returned, as set in the query
// (or default value if unset).
Offset int `json:"offset"`
// The total number of items available to return.
Total int `json:"total"`
// The URL to the next page of items (if available).
Next string `json:"next"`
// The URL to the previous page of items (if available).
Previous string `json:"previous"`
}
// FullArtistPage contains FullArtists returned by the Web API.
type FullArtistPage struct {
basePage
Artists []FullArtist `json:"items"`
}
// SimpleAlbumPage contains SimpleAlbums returned by the Web API.
type SimpleAlbumPage struct {
basePage
Albums []SimpleAlbum `json:"items"`
}
// SavedAlbumPage contains SavedAlbums returned by the Web API.
type SavedAlbumPage struct {
basePage
Albums []SavedAlbum `json:"items"`
}
// SimplePlaylistPage contains SimplePlaylists returned by the Web API.
type SimplePlaylistPage struct {
basePage
Playlists []SimplePlaylist `json:"items"`
}
// SimpleTrackPage contains SimpleTracks returned by the Web API.
type SimpleTrackPage struct {
basePage
Tracks []SimpleTrack `json:"items"`
}
// FullTrackPage contains FullTracks returned by the Web API.
type FullTrackPage struct {
basePage
Tracks []FullTrack `json:"items"`
}
// SavedTrackPage contains SavedTracks return by the Web API.
type SavedTrackPage struct {
basePage
Tracks []SavedTrack `json:"items"`
}
// PlaylistTrackPage contains information about tracks in a playlist.
type PlaylistTrackPage struct {
basePage
Tracks []PlaylistTrack `json:"items"`
}
// CategoryPage contains Category objects returned by the Web API.
type CategoryPage struct {
basePage
Categories []Category `json:"items"`
}

528
vendor/github.com/zmb3/spotify/player.go generated vendored Executable file
View File

@ -0,0 +1,528 @@
package spotify
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"strconv"
"time"
)
// PlayerDevice contains information about a device that a user can play music on
type PlayerDevice struct {
// ID of the device. This may be empty.
ID ID `json:"id"`
// Active If this device is the currently active device.
Active bool `json:"is_active"`
// Restricted Whether controlling this device is restricted. At present if
// this is "true" then no Web API commands will be accepted by this device.
Restricted bool `json:"is_restricted"`
// Name The name of the device.
Name string `json:"name"`
// Type of device, such as "Computer", "Smartphone" or "Speaker".
Type string `json:"type"`
// Volume The current volume in percent.
Volume int `json:"volume_percent"`
}
// PlayerState contains information about the current playback.
type PlayerState struct {
CurrentlyPlaying
// Device The device that is currently active
Device PlayerDevice `json:"device"`
// ShuffleState Shuffle is on or off
ShuffleState bool `json:"shuffle_state"`
// RepeatState off, track, context
RepeatState string `json:"repeat_state"`
}
// PlaybackContext is the playback context
type PlaybackContext struct {
// ExternalURLs of the context, or null if not available.
ExternalURLs map[string]string `json:"external_urls"`
// Endpoint of the context, or null if not available.
Endpoint string `json:"href"`
// Type of the item's context. Can be one of album, artist or playlist.
Type string `json:"type"`
// URI is the Spotify URI for the context.
URI URI `json:"uri"`
}
// CurrentlyPlaying contains the information about currently playing items
type CurrentlyPlaying struct {
// Timestamp when data was fetched
Timestamp int64 `json:"timestamp"`
// PlaybackContext current context
PlaybackContext PlaybackContext `json:"context"`
// Progress into the currently playing track.
Progress int `json:"progress_ms"`
// Playing If something is currently playing.
Playing bool `json:"is_playing"`
// The currently playing track. Can be null.
Item *FullTrack `json:"Item"`
}
type RecentlyPlayedItem struct {
// Track is the track information
Track SimpleTrack `json:"track"`
// PlayedAt is the time that this song was played
PlayedAt time.Time `json:"played_at"`
// PlaybackContext is the current playback context
PlaybackContext PlaybackContext `json:"context"`
}
type RecentlyPlayedResult struct {
Items []RecentlyPlayedItem `json:"items"`
}
// PlaybackOffset can be specified either by track URI OR Position. If both are present the
// request will return 400 BAD REQUEST. If incorrect values are provided for position or uri,
// the request may be accepted but with an unpredictable resulting action on playback.
type PlaybackOffset struct {
// Position is zero based and cant be negative.
Position int `json:"position,omitempty"`
// URI is a string representing the uri of the item to start at.
URI URI `json:"uri,omitempty"`
}
type PlayOptions struct {
// DeviceID The id of the device this command is targeting. If not
// supplied, the user's currently active device is the target.
DeviceID *ID `json:"-"`
// PlaybackContext Spotify URI of the context to play.
// Valid contexts are albums, artists & playlists.
PlaybackContext *URI `json:"context_uri,omitempty"`
// URIs Array of the Spotify track URIs to play
URIs []URI `json:"uris,omitempty"`
// PlaybackOffset Indicates from where in the context playback should start.
// Only available when context corresponds to an album or playlist
// object, or when the URIs parameter is used.
PlaybackOffset *PlaybackOffset `json:"offset,omitempty"`
// PositionMs Indicates from what position to start playback.
// Must be a positive number. Passing in a position that is greater
// than the length of the track will cause the player to start playing the next song.
// Defaults to 0, starting a track from the beginning.
PositionMs int `json:"position_ms,omitempty"`
}
// RecentlyPlayedOptions describes options for the recently-played request. All
// fields are optional. Only one of `AfterEpochMs` and `BeforeEpochMs` may be
// given. Note that it seems as if Spotify only remembers the fifty most-recent
// tracks as of right now.
type RecentlyPlayedOptions struct {
// Limit is the maximum number of items to return. Must be no greater than
// fifty.
Limit int
// AfterEpochMs is a Unix epoch in milliseconds that describes a time after
// which to return songs.
AfterEpochMs int64
// BeforeEpochMs is a Unix epoch in milliseconds that describes a time
// before which to return songs.
BeforeEpochMs int64
}
// PlayerDevices information about available devices for the current user.
//
// Requires the ScopeUserReadPlaybackState scope in order to read information
func (c *Client) PlayerDevices() ([]PlayerDevice, error) {
var result struct {
PlayerDevices []PlayerDevice `json:"devices"`
}
err := c.get(c.baseURL+"me/player/devices", &result)
if err != nil {
return nil, err
}
return result.PlayerDevices, nil
}
// PlayerState gets information about the playing state for the current user
//
// Requires the ScopeUserReadPlaybackState scope in order to read information
func (c *Client) PlayerState() (*PlayerState, error) {
return c.PlayerStateOpt(nil)
}
// PlayerStateOpt is like PlayerState, but it accepts additional
// options for sorting and filtering the results.
func (c *Client) PlayerStateOpt(opt *Options) (*PlayerState, error) {
spotifyURL := c.baseURL + "me/player"
if opt != nil {
v := url.Values{}
if opt.Country != nil {
v.Set("market", *opt.Country)
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
}
var result PlayerState
err := c.get(spotifyURL, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// PlayerCurrentlyPlaying gets information about the currently playing status
// for the current user.
//
// Requires the ScopeUserReadCurrentlyPlaying scope or the ScopeUserReadPlaybackState
// scope in order to read information
func (c *Client) PlayerCurrentlyPlaying() (*CurrentlyPlaying, error) {
return c.PlayerCurrentlyPlayingOpt(nil)
}
// PlayerCurrentlyPlayingOpt is like PlayerCurrentlyPlaying, but it accepts
// additional options for sorting and filtering the results.
func (c *Client) PlayerCurrentlyPlayingOpt(opt *Options) (*CurrentlyPlaying, error) {
spotifyURL := c.baseURL + "me/player/currently-playing"
if opt != nil {
v := url.Values{}
if opt.Country != nil {
v.Set("market", *opt.Country)
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
}
req, err := http.NewRequest("GET", spotifyURL, nil)
if err != nil {
return nil, err
}
var result CurrentlyPlaying
err = c.execute(req, &result, http.StatusNoContent)
if err != nil {
return nil, err
}
return &result, nil
}
// PlayerRecentlyPlayed gets a list of recently-played tracks for the current
// user. This call requires ScopeUserReadRecentlyPlayed.
func (c *Client) PlayerRecentlyPlayed() ([]RecentlyPlayedItem, error) {
return c.PlayerRecentlyPlayedOpt(nil)
}
// PlayerRecentlyPlayedOpt is like PlayerRecentlyPlayed, but it accepts
// additional options for sorting and filtering the results.
func (c *Client) PlayerRecentlyPlayedOpt(opt *RecentlyPlayedOptions) ([]RecentlyPlayedItem, error) {
spotifyURL := c.baseURL + "me/player/recently-played"
if opt != nil {
v := url.Values{}
if opt.Limit != 0 {
v.Set("limit", strconv.FormatInt(int64(opt.Limit), 10))
}
if opt.BeforeEpochMs != 0 {
v.Set("before", strconv.FormatInt(int64(opt.BeforeEpochMs), 10))
}
if opt.AfterEpochMs != 0 {
v.Set("after", strconv.FormatInt(int64(opt.AfterEpochMs), 10))
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
}
result := RecentlyPlayedResult{}
err := c.get(spotifyURL, &result)
if err != nil {
return nil, err
}
return result.Items, nil
}
// TransferPlayback transfers playback to a new device and determine if
// it should start playing.
//
// Note that a value of false for the play parameter when also transferring
// to another device_id will not pause playback. To ensure that playback is
// paused on the new device you should send a pause command to the currently
// active device before transferring to the new device_id.
//
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
func (c *Client) TransferPlayback(deviceID ID, play bool) error {
reqData := struct {
DeviceID []ID `json:"device_ids"`
Play bool `json:"play"`
}{
DeviceID: []ID{deviceID},
Play: play,
}
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(reqData)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPut, c.baseURL+"me/player", buf)
if err != nil {
return err
}
err = c.execute(req, nil, http.StatusNoContent)
if err != nil {
return err
}
return nil
}
// Play Start a new context or resume current playback on the user's active
// device. This call requires ScopeUserModifyPlaybackState in order to modify the player state.
func (c *Client) Play() error {
return c.PlayOpt(nil)
}
// PlayOpt is like Play but with more options
func (c *Client) PlayOpt(opt *PlayOptions) error {
spotifyURL := c.baseURL + "me/player/play"
buf := new(bytes.Buffer)
if opt != nil {
v := url.Values{}
if opt.DeviceID != nil {
v.Set("device_id", opt.DeviceID.String())
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
err := json.NewEncoder(buf).Encode(opt)
if err != nil {
return err
}
}
req, err := http.NewRequest(http.MethodPut, spotifyURL, buf)
if err != nil {
return err
}
err = c.execute(req, nil, http.StatusNoContent)
if err != nil {
return err
}
return nil
}
// Pause Playback on the user's currently active device.
//
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
func (c *Client) Pause() error {
return c.PauseOpt(nil)
}
// PauseOpt is like Pause but with more options
//
// Only expects PlayOptions.DeviceID, all other options will be ignored
func (c *Client) PauseOpt(opt *PlayOptions) error {
spotifyURL := c.baseURL + "me/player/pause"
if opt != nil {
v := url.Values{}
if opt.DeviceID != nil {
v.Set("device_id", opt.DeviceID.String())
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
}
req, err := http.NewRequest(http.MethodPut, spotifyURL, nil)
if err != nil {
return err
}
err = c.execute(req, nil, http.StatusNoContent)
if err != nil {
return err
}
return nil
}
// Next skips to the next track in the user's queue in the user's
// currently active device. This call requires ScopeUserModifyPlaybackState
// in order to modify the player state
func (c *Client) Next() error {
return c.NextOpt(nil)
}
// NextOpt is like Next but with more options
//
// Only expects PlayOptions.DeviceID, all other options will be ignored
func (c *Client) NextOpt(opt *PlayOptions) error {
spotifyURL := c.baseURL + "me/player/next"
if opt != nil {
v := url.Values{}
if opt.DeviceID != nil {
v.Set("device_id", opt.DeviceID.String())
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
}
req, err := http.NewRequest(http.MethodPost, spotifyURL, nil)
if err != nil {
return err
}
err = c.execute(req, nil, http.StatusNoContent)
if err != nil {
return err
}
return nil
}
// Previous skips to the Previous track in the user's queue in the user's
// currently active device. This call requires ScopeUserModifyPlaybackState
// in order to modify the player state
func (c *Client) Previous() error {
return c.PreviousOpt(nil)
}
// PreviousOpt is like Previous but with more options
//
// Only expects PlayOptions.DeviceID, all other options will be ignored
func (c *Client) PreviousOpt(opt *PlayOptions) error {
spotifyURL := c.baseURL + "me/player/previous"
if opt != nil {
v := url.Values{}
if opt.DeviceID != nil {
v.Set("device_id", opt.DeviceID.String())
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
}
req, err := http.NewRequest(http.MethodPost, spotifyURL, nil)
if err != nil {
return err
}
err = c.execute(req, nil, http.StatusNoContent)
if err != nil {
return err
}
return nil
}
// Seek to the given position in the users currently playing track.
//
// The position in milliseconds to seek to. Must be a positive number.
// Passing in a position that is greater than the length of the track
// will cause the player to start playing the next song.
//
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
func (c *Client) Seek(position int) error {
return c.SeekOpt(position, nil)
}
// SeekOpt is like Seek but with more options
//
// Only expects PlayOptions.DeviceID, all other options will be ignored
func (c *Client) SeekOpt(position int, opt *PlayOptions) error {
return c.playerFuncWithOpt(
"me/player/seek",
url.Values{
"position_ms": []string{strconv.FormatInt(int64(position), 10)},
},
opt,
)
}
// Repeat Set the repeat mode for the user's playback.
//
// Options are repeat-track, repeat-context, and off.
//
// Requires the ScopeUserModifyPlaybackState in order to modify the player state.
func (c *Client) Repeat(state string) error {
return c.RepeatOpt(state, nil)
}
// RepeatOpt is like Repeat but with more options
//
// Only expects PlayOptions.DeviceID, all other options will be ignored.
func (c *Client) RepeatOpt(state string, opt *PlayOptions) error {
return c.playerFuncWithOpt(
"me/player/repeat",
url.Values{
"state": []string{state},
},
opt,
)
}
// Volume set the volume for the user's current playback device.
//
// Percent is must be a value from 0 to 100 inclusive.
//
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
func (c *Client) Volume(percent int) error {
return c.VolumeOpt(percent, nil)
}
// VolumeOpt is like Volume but with more options
//
// Only expects PlayOptions.DeviceID, all other options will be ignored
func (c *Client) VolumeOpt(percent int, opt *PlayOptions) error {
return c.playerFuncWithOpt(
"me/player/volume",
url.Values{
"volume_percent": []string{strconv.FormatInt(int64(percent), 10)},
},
opt,
)
}
// Shuffle switches shuffle on or off for user's playback.
//
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
func (c *Client) Shuffle(shuffle bool) error {
return c.ShuffleOpt(shuffle, nil)
}
// ShuffleOpt is like Shuffle but with more options
//
// Only expects PlayOptions.DeviceID, all other options will be ignored
func (c *Client) ShuffleOpt(shuffle bool, opt *PlayOptions) error {
return c.playerFuncWithOpt(
"me/player/shuffle",
url.Values{
"state": []string{strconv.FormatBool(shuffle)},
},
opt,
)
}
func (c *Client) playerFuncWithOpt(urlSuffix string, values url.Values, opt *PlayOptions) error {
spotifyURL := c.baseURL + urlSuffix
if opt != nil {
if opt.DeviceID != nil {
values.Set("device_id", opt.DeviceID.String())
}
}
if params := values.Encode(); params != "" {
spotifyURL += "?" + params
}
req, err := http.NewRequest(http.MethodPut, spotifyURL, nil)
if err != nil {
return err
}
err = c.execute(req, nil, http.StatusNoContent)
if err != nil {
return err
}
return nil
}

662
vendor/github.com/zmb3/spotify/playlist.go generated vendored Normal file
View File

@ -0,0 +1,662 @@
package spotify
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
// PlaylistTracks contains details about the tracks in a playlist.
type PlaylistTracks struct {
// A link to the Web API endpoint where full details of
// the playlist's tracks can be retrieved.
Endpoint string `json:"href"`
// The total number of tracks in the playlist.
Total uint `json:"total"`
}
// SimplePlaylist contains basic info about a Spotify playlist.
type SimplePlaylist struct {
// Indicates whether the playlist owner allows others to modify the playlist.
// Note: only non-collaborative playlists are currently returned by Spotify's Web API.
Collaborative bool `json:"collaborative"`
ExternalURLs map[string]string `json:"external_urls"`
// A link to the Web API endpoint providing full details of the playlist.
Endpoint string `json:"href"`
ID ID `json:"id"`
// The playlist image. Note: this field is only returned for modified,
// verified playlists. Otherwise the slice is empty. If returned, the source
// URL for the image is temporary and will expire in less than a day.
Images []Image `json:"images"`
Name string `json:"name"`
Owner User `json:"owner"`
IsPublic bool `json:"public"`
// The version identifier for the current playlist. Can be supplied in other
// requests to target a specific playlist version.
SnapshotID string `json:"snapshot_id"`
// A collection to the Web API endpoint where full details of the playlist's
// tracks can be retrieved, along with the total number of tracks in the playlist.
Tracks PlaylistTracks `json:"tracks"`
URI URI `json:"uri"`
}
// FullPlaylist provides extra playlist data in addition to the data provided by SimplePlaylist.
type FullPlaylist struct {
SimplePlaylist
// The playlist description. Only returned for modified, verified playlists.
Description string `json:"description"`
// Information about the followers of this playlist.
Followers Followers `json:"followers"`
Tracks PlaylistTrackPage `json:"tracks"`
}
// PlaylistOptions contains optional parameters that can be used when querying
// for featured playlists. Only the non-nil fields are used in the request.
type PlaylistOptions struct {
Options
// The desired language, consisting of a lowercase IO 639
// language code and an uppercase ISO 3166-1 alpha-2
// country code, joined by an underscore. Provide this
// parameter if you want the results returned in a particular
// language. If not specified, the result will be returned
// in the Spotify default language (American English).
Locale *string
// A timestamp in ISO 8601 format (yyyy-MM-ddTHH:mm:ss).
// use this paramter to specify the user's local time to
// get results tailored for that specific date and time
// in the day. If not provided, the response defaults to
// the current UTC time.
Timestamp *string
}
// FeaturedPlaylistsOpt gets a list of playlists featured by Spotify.
// It accepts a number of optional parameters via the opt argument.
func (c *Client) FeaturedPlaylistsOpt(opt *PlaylistOptions) (message string, playlists *SimplePlaylistPage, e error) {
spotifyURL := c.baseURL + "browse/featured-playlists"
if opt != nil {
v := url.Values{}
if opt.Locale != nil {
v.Set("locale", *opt.Locale)
}
if opt.Country != nil {
v.Set("country", *opt.Country)
}
if opt.Timestamp != nil {
v.Set("timestamp", *opt.Timestamp)
}
if opt.Limit != nil {
v.Set("limit", strconv.Itoa(*opt.Limit))
}
if opt.Offset != nil {
v.Set("offset", strconv.Itoa(*opt.Offset))
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
}
var result struct {
Playlists SimplePlaylistPage `json:"playlists"`
Message string `json:"message"`
}
err := c.get(spotifyURL, &result)
if err != nil {
return "", nil, err
}
return result.Message, &result.Playlists, nil
}
// FeaturedPlaylists gets a list of playlists featured by Spotify.
// It is equivalent to c.FeaturedPlaylistsOpt(nil).
func (c *Client) FeaturedPlaylists() (message string, playlists *SimplePlaylistPage, e error) {
return c.FeaturedPlaylistsOpt(nil)
}
// FollowPlaylist adds the current user as a follower of the specified
// playlist. Any playlist can be followed, regardless of its private/public
// status, as long as you know the owner and playlist ID.
//
// If the public argument is true, then the playlist will be included in the
// user's public playlists. To be able to follow playlists privately, the user
// must have granted the ScopePlaylistModifyPrivate scope. The
// ScopePlaylistModifyPublic scope is required to follow playlists publicly.
func (c *Client) FollowPlaylist(owner ID, playlist ID, public bool) error {
spotifyURL := buildFollowURI(c.baseURL, owner, playlist)
body := strings.NewReader(strconv.FormatBool(public))
req, err := http.NewRequest("PUT", spotifyURL, body)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
err = c.execute(req, nil)
if err != nil {
return err
}
return nil
}
// UnfollowPlaylist removes the current user as a follower of a playlist.
// Unfollowing a publicly followed playlist requires ScopePlaylistModifyPublic.
// Unfolowing a privately followed playlist requies ScopePlaylistModifyPrivate.
func (c *Client) UnfollowPlaylist(owner, playlist ID) error {
spotifyURL := buildFollowURI(c.baseURL, owner, playlist)
req, err := http.NewRequest("DELETE", spotifyURL, nil)
if err != nil {
return err
}
err = c.execute(req, nil)
if err != nil {
return err
}
return nil
}
func buildFollowURI(url string, owner, playlist ID) string {
return fmt.Sprintf("%susers/%s/playlists/%s/followers",
url, string(owner), string(playlist))
}
// GetPlaylistsForUser gets a list of the playlists owned or followed by a
// particular Spotify user.
//
// Private playlists and collaborative playlists are only retrievable for the
// current user. In order to read private playlists, the user must have granted
// the ScopePlaylistReadPrivate scope. Note that this scope alone will not
// return collaborative playlists, even though they are always private. In
// order to read collaborative playlists, the user must have granted the
// ScopePlaylistReadCollaborative scope.
func (c *Client) GetPlaylistsForUser(userID string) (*SimplePlaylistPage, error) {
return c.GetPlaylistsForUserOpt(userID, nil)
}
// GetPlaylistsForUserOpt is like PlaylistsForUser, but it accepts optional paramters
// for filtering the results.
func (c *Client) GetPlaylistsForUserOpt(userID string, opt *Options) (*SimplePlaylistPage, error) {
spotifyURL := c.baseURL + "users/" + userID + "/playlists"
if opt != nil {
v := url.Values{}
if opt.Limit != nil {
v.Set("limit", strconv.Itoa(*opt.Limit))
}
if opt.Offset != nil {
v.Set("offset", strconv.Itoa(*opt.Offset))
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
}
var result SimplePlaylistPage
err := c.get(spotifyURL, &result)
if err != nil {
return nil, err
}
return &result, err
}
// GetPlaylist gets a playlist
func (c *Client) GetPlaylist(playlistID ID) (*FullPlaylist, error) {
return c.GetPlaylistOpt(playlistID, "")
}
// GetPlaylistOpt is like GetPlaylist, but it accepts an optional fields parameter
// that can be used to filter the query.
//
// fields is a comma-separated list of the fields to return.
// See the JSON tags on the FullPlaylist struct for valid field options.
// For example, to get just the playlist's description and URI:
// fields = "description,uri"
//
// A dot separator can be used to specify non-reoccurring fields, while
// parentheses can be used to specify reoccurring fields within objects.
// For example, to get just the added date and the user ID of the adder:
// fields = "tracks.items(added_at,added_by.id)"
//
// Use multiple parentheses to drill down into nested objects, for example:
// fields = "tracks.items(track(name,href,album(name,href)))"
//
// Fields can be excluded by prefixing them with an exclamation mark, for example;
// fields = "tracks.items(track(name,href,album(!name,href)))"
func (c *Client) GetPlaylistOpt(playlistID ID, fields string) (*FullPlaylist, error) {
spotifyURL := fmt.Sprintf("%splaylists/%s", c.baseURL, playlistID)
if fields != "" {
spotifyURL += "?fields=" + url.QueryEscape(fields)
}
var playlist FullPlaylist
err := c.get(spotifyURL, &playlist)
if err != nil {
return nil, err
}
return &playlist, err
}
// GetPlaylistTracks gets full details of the tracks in a playlist, given the
// playlist's Spotify ID.
func (c *Client) GetPlaylistTracks(playlistID ID) (*PlaylistTrackPage, error) {
return c.GetPlaylistTracksOpt(playlistID, nil, "")
}
// GetPlaylistTracksOpt is like GetPlaylistTracks, but it accepts optional parameters
// for sorting and filtering the results.
//
// The field parameter is a comma-separated list of the fields to return. See the
// JSON struct tags for the PlaylistTrackPage type for valid field names.
// For example, to get just the total number of tracks and the request limit:
// fields = "total,limit"
//
// A dot separator can be used to specify non-reoccurring fields, while parentheses
// can be used to specify reoccurring fields within objects. For example, to get
// just the added date and user ID of the adder:
// fields = "items(added_at,added_by.id
//
// Use multiple parentheses to drill down into nested objects. For example:
// fields = "items(track(name,href,album(name,href)))"
//
// Fields can be excluded by prefixing them with an exclamation mark. For example:
// fields = "items.track.album(!external_urls,images)"
func (c *Client) GetPlaylistTracksOpt(playlistID ID,
opt *Options, fields string) (*PlaylistTrackPage, error) {
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks", c.baseURL, playlistID)
v := url.Values{}
if fields != "" {
v.Set("fields", fields)
}
if opt != nil {
if opt.Limit != nil {
v.Set("limit", strconv.Itoa(*opt.Limit))
}
if opt.Offset != nil {
v.Set("offset", strconv.Itoa(*opt.Offset))
}
}
if params := v.Encode(); params != "" {
spotifyURL += "?" + params
}
var result PlaylistTrackPage
err := c.get(spotifyURL, &result)
if err != nil {
return nil, err
}
return &result, err
}
// CreatePlaylistForUser creates a playlist for a Spotify user.
// The playlist will be empty until you add tracks to it.
// The playlistName does not need to be unique - a user can have
// several playlists with the same name.
//
// Creating a public playlist for a user requires ScopePlaylistModifyPublic;
// creating a private playlist requires ScopePlaylistModifyPrivate.
//
// On success, the newly created playlist is returned.
func (c *Client) CreatePlaylistForUser(userID, playlistName, description string, public bool) (*FullPlaylist, error) {
spotifyURL := fmt.Sprintf("%susers/%s/playlists", c.baseURL, userID)
body := struct {
Name string `json:"name"`
Public bool `json:"public"`
Description string `json:"description"`
}{
playlistName,
public,
description,
}
bodyJSON, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", spotifyURL, bytes.NewReader(bodyJSON))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
var p FullPlaylist
err = c.execute(req, &p, http.StatusCreated)
if err != nil {
return nil, err
}
return &p, err
}
// ChangePlaylistName changes the name of a playlist. This call requires that the
// user has authorized the ScopePlaylistModifyPublic or ScopePlaylistModifyPrivate
// scopes (depending on whether the playlist is public or private).
// The current user must own the playlist in order to modify it.
func (c *Client) ChangePlaylistName(playlistID ID, newName string) error {
return c.modifyPlaylist(playlistID, newName, "", nil)
}
// ChangePlaylistAccess modifies the public/private status of a playlist. This call
// requires that the user has authorized the ScopePlaylistModifyPublic or
// ScopePlaylistModifyPrivate scopes (depending on whether the playlist is
// currently public or private). The current user must own the playlist in order to modify it.
func (c *Client) ChangePlaylistAccess(playlistID ID, public bool) error {
return c.modifyPlaylist(playlistID, "", "", &public)
}
// ChangePlaylistDescription modifies the description of a playlist. This call
// requires that the user has authorized the ScopePlaylistModifyPublic or
// ScopePlaylistModifyPrivate scopes (depending on whether the playlist is
// currently public or private). The current user must own the playlist in order to modify it.
func (c *Client) ChangePlaylistDescription(playlistID ID, newDescription string) error {
return c.modifyPlaylist(playlistID, "", newDescription, nil)
}
// ChangePlaylistNameAndAccess combines ChangePlaylistName and ChangePlaylistAccess into
// a single Web API call. It requires that the user has authorized the ScopePlaylistModifyPublic
// or ScopePlaylistModifyPrivate scopes (depending on whether the playlist is currently
// public or private). The current user must own the playlist in order to modify it.
func (c *Client) ChangePlaylistNameAndAccess(playlistID ID, newName string, public bool) error {
return c.modifyPlaylist(playlistID, newName, "", &public)
}
// ChangePlaylistNameAccessAndDescription combines ChangePlaylistName, ChangePlaylistAccess, and
// ChangePlaylistDescription into a single Web API call. It requires that the user has authorized
// the ScopePlaylistModifyPublic or ScopePlaylistModifyPrivate scopes (depending on whether the
// playlist is currently public or private). The current user must own the playlist in order to modify it.
func (c *Client) ChangePlaylistNameAccessAndDescription(playlistID ID, newName, newDescription string, public bool) error {
return c.modifyPlaylist(playlistID, newName, newDescription, &public)
}
func (c *Client) modifyPlaylist(playlistID ID, newName, newDescription string, public *bool) error {
body := struct {
Name string `json:"name,omitempty"`
Public *bool `json:"public,omitempty"`
Description string `json:"description,omitempty"`
}{
newName,
public,
newDescription,
}
bodyJSON, err := json.Marshal(body)
if err != nil {
return err
}
spotifyURL := fmt.Sprintf("%splaylists/%s", c.baseURL, string(playlistID))
req, err := http.NewRequest("PUT", spotifyURL, bytes.NewReader(bodyJSON))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
err = c.execute(req, nil, http.StatusCreated)
if err != nil {
return err
}
return nil
}
// AddTracksToPlaylist adds one or more tracks to a user's playlist.
// This call requires ScopePlaylistModifyPublic or ScopePlaylistModifyPrivate.
// A maximum of 100 tracks can be added per call. It returns a snapshot ID that
// can be used to identify this version (the new version) of the playlist in
// future requests.
func (c *Client) AddTracksToPlaylist(playlistID ID, trackIDs ...ID) (snapshotID string, err error) {
uris := make([]string, len(trackIDs))
for i, id := range trackIDs {
uris[i] = fmt.Sprintf("spotify:track:%s", id)
}
m := make(map[string]interface{})
m["uris"] = uris
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks",
c.baseURL, string(playlistID))
body, err := json.Marshal(m)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", spotifyURL, bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
result := struct {
SnapshotID string `json:"snapshot_id"`
}{}
err = c.execute(req, &result, http.StatusCreated)
if err != nil {
return "", err
}
return result.SnapshotID, nil
}
// RemoveTracksFromPlaylist removes one or more tracks from a user's playlist.
// This call requrles that the user has authorized the ScopePlaylistModifyPublic
// or ScopePlaylistModifyPrivate scopes.
//
// If the track(s) occur multiple times in the specified playlist, then all occurrences
// of the track will be removed. If successful, the snapshot ID returned can be used to
// identify the playlist version in future requests.
func (c *Client) RemoveTracksFromPlaylist(playlistID ID, trackIDs ...ID) (newSnapshotID string, err error) {
tracks := make([]struct {
URI string `json:"uri"`
}, len(trackIDs))
for i, u := range trackIDs {
tracks[i].URI = fmt.Sprintf("spotify:track:%s", u)
}
return c.removeTracksFromPlaylist(playlistID, tracks, "")
}
// TrackToRemove specifies a track to be removed from a playlist.
// Positions is a slice of 0-based track indices.
// TrackToRemove is used with RemoveTracksFromPlaylistOpt.
type TrackToRemove struct {
URI string `json:"uri"`
Positions []int `json:"positions"`
}
// NewTrackToRemove creates a new TrackToRemove object with the specified
// track ID and playlist locations.
func NewTrackToRemove(trackID string, positions []int) TrackToRemove {
return TrackToRemove{
URI: fmt.Sprintf("spotify:track:%s", trackID),
Positions: positions,
}
}
// RemoveTracksFromPlaylistOpt is like RemoveTracksFromPlaylist, but it supports
// optional parameters that offer more fine-grained control. Instead of deleting
// all occurrences of a track, this function takes an index with each track URI
// that indicates the position of the track in the playlist.
//
// In addition, the snapshotID parameter allows you to specify the snapshot ID
// against which you want to make the changes. Spotify will validate that the
// specified tracks exist in the specified positions and make the changes, even
// if more recent changes have been made to the playlist. If a track in the
// specified position is not found, the entire request will fail and no edits
// will take place. (Note: the snapshot is optional, pass the empty string if
// you don't care about it.)
func (c *Client) RemoveTracksFromPlaylistOpt(playlistID ID,
tracks []TrackToRemove, snapshotID string) (newSnapshotID string, err error) {
return c.removeTracksFromPlaylist(playlistID, tracks, snapshotID)
}
func (c *Client) removeTracksFromPlaylist(playlistID ID,
tracks interface{}, snapshotID string) (newSnapshotID string, err error) {
m := make(map[string]interface{})
m["tracks"] = tracks
if snapshotID != "" {
m["snapshot_id"] = snapshotID
}
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks",
c.baseURL, string(playlistID))
body, err := json.Marshal(m)
if err != nil {
return "", err
}
req, err := http.NewRequest("DELETE", spotifyURL, bytes.NewReader(body))
if err != nil {
return "", nil
}
req.Header.Set("Content-Type", "application/json")
result := struct {
SnapshotID string `json:"snapshot_id"`
}{}
err = c.execute(req, &result)
if err != nil {
return "", nil
}
return result.SnapshotID, err
}
// ReplacePlaylistTracks replaces all of the tracks in a playlist, overwriting its
// exising tracks This can be useful for replacing or reordering tracks, or for
// clearing a playlist.
//
// Modifying a public playlist requires that the user has authorized the
// ScopePlaylistModifyPublic scope. Modifying a private playlist requires the
// ScopePlaylistModifyPrivate scope.
//
// A maximum of 100 tracks is permited in this call. Additional tracks must be
// added via AddTracksToPlaylist.
func (c *Client) ReplacePlaylistTracks(playlistID ID, trackIDs ...ID) error {
trackURIs := make([]string, len(trackIDs))
for i, u := range trackIDs {
trackURIs[i] = fmt.Sprintf("spotify:track:%s", u)
}
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks?uris=%s",
c.baseURL, playlistID, strings.Join(trackURIs, ","))
req, err := http.NewRequest("PUT", spotifyURL, nil)
if err != nil {
return err
}
err = c.execute(req, nil, http.StatusCreated)
if err != nil {
return err
}
return nil
}
// UserFollowsPlaylist checks if one or more (up to 5) Spotify users are following
// a Spotify playlist, given the playlist's owner and ID.
//
// Checking if a user follows a playlist publicly doesn't require any scopes.
// Checking if the user is privately following a playlist is only possible for the
// current user when that user has granted access to the ScopePlaylistReadPrivate scope.
func (c *Client) UserFollowsPlaylist(playlistID ID, userIDs ...string) ([]bool, error) {
spotifyURL := fmt.Sprintf("%splaylists/%s/followers/contains?ids=%s",
c.baseURL, playlistID, strings.Join(userIDs, ","))
follows := make([]bool, len(userIDs))
err := c.get(spotifyURL, &follows)
if err != nil {
return nil, err
}
return follows, err
}
// PlaylistReorderOptions is used with ReorderPlaylistTracks to reorder
// a track or group of tracks in a playlist.
//
// For example, in a playlist with 10 tracks, you can:
//
// - move the first track to the end of the playlist by setting
// RangeStart to 0 and InsertBefore to 10
// - move the last track to the beginning of the playlist by setting
// RangeStart to 9 and InsertBefore to 0
// - Move the last 2 tracks to the beginning of the playlist by setting
// RangeStart to 8 and RangeLength to 2.
type PlaylistReorderOptions struct {
// The position of the first track to be reordered.
// This field is required.
RangeStart int `json:"range_start"`
// The amount of tracks to be reordered. This field is optional. If
// you don't set it, the value 1 will be used.
RangeLength int `json:"range_length,omitempty"`
// The position where the tracks should be inserted. To reorder the
// tracks to the end of the playlist, simply set this to the position
// after the last track. This field is required.
InsertBefore int `json:"insert_before"`
// The playlist's snapshot ID against which you wish to make the changes.
// This field is optional.
SnapshotID string `json:"snapshot_id,omitempty"`
}
// ReorderPlaylistTracks reorders a track or group of tracks in a playlist. It
// returns a snapshot ID that can be used to identify the [newly modified] playlist
// version in future requests.
//
// See the docs for PlaylistReorderOptions for information on how the reordering
// works.
//
// Reordering tracks in the current user's public playlist requires ScopePlaylistModifyPublic.
// Reordering tracks in the user's private playlists (including collaborative playlists) requires
// ScopePlaylistModifyPrivate.
func (c *Client) ReorderPlaylistTracks(playlistID ID, opt PlaylistReorderOptions) (snapshotID string, err error) {
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks", c.baseURL, playlistID)
j, err := json.Marshal(opt)
if err != nil {
return "", err
}
req, err := http.NewRequest("PUT", spotifyURL, bytes.NewReader(j))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
result := struct {
SnapshotID string `json:"snapshot_id"`
}{}
err = c.execute(req, &result)
if err != nil {
return "", err
}
return result.SnapshotID, err
}
// SetPlaylistImage replaces the image used to represent a playlist.
// This action can only be performed by the owner of the playlist,
// and requires ScopeImageUpload as well as ScopeModifyPlaylist{Public|Private}..
func (c *Client) SetPlaylistImage(playlistID ID, img io.Reader) error {
spotifyURL := fmt.Sprintf("%splaylists/%s/images", c.baseURL, playlistID)
// data flow:
// img (reader) -> copy into base64 encoder (writer) -> pipe (write end)
// pipe (read end) -> request body
r, w := io.Pipe()
go func() {
enc := base64.NewEncoder(base64.StdEncoding, w)
_, err := io.Copy(enc, img)
enc.Close()
w.CloseWithError(err)
}()
req, err := http.NewRequest("PUT", spotifyURL, r)
if err != nil {
return err
}
req.Header.Set("Content-Type", "image/jpeg")
return c.execute(req, nil, http.StatusAccepted)
}

Some files were not shown because too many files have changed in this diff Show More