├── .gitignore ├── go.mod ├── pkg └── wakatime │ ├── goals_test.go │ ├── useragents_test.go │ ├── users_test.go │ ├── projects_test.go │ ├── commits_test.go │ ├── stats_test.go │ ├── durations_test.go │ ├── query.go │ ├── useragents.go │ ├── durations.go │ ├── users.go │ ├── projects.go │ ├── goals.go │ ├── wakatime.go │ ├── commits.go │ └── stats.go ├── .github └── workflows │ └── go.yml ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/YouEclipse/wakatime-go 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /pkg/wakatime/goals_test.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestGoals(t *testing.T) { 10 | apiKey := os.Getenv("WAKATIME_API_KEY") 11 | userID := os.Getenv("WAKATIME_USER_ID") 12 | 13 | client := NewClient(apiKey, nil) 14 | ctx := context.Background() 15 | query1 := &GoalsQuery{} 16 | _, err := client.Goals.Current(ctx, query1) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | _, err = client.Goals.User(ctx, userID, query1) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /pkg/wakatime/useragents_test.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestUserAgents(t *testing.T) { 10 | apiKey := os.Getenv("WAKATIME_API_KEY") 11 | userID := os.Getenv("WAKATIME_USER_ID") 12 | 13 | client := NewClient(apiKey, nil) 14 | ctx := context.Background() 15 | query1 := &UserAgentsQuery{} 16 | _, err := client.UserAgents.Current(ctx, query1) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | _, err = client.UserAgents.User(ctx, userID, query1) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /pkg/wakatime/users_test.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestUsers(t *testing.T) { 10 | apiKey := os.Getenv("WAKATIME_API_KEY") 11 | userID := os.Getenv("WAKATIME_USER_ID") 12 | apiBase := os.Getenv("WAKATIME_API_BASE") 13 | 14 | client := NewClient(apiKey, nil, apiBase) 15 | ctx := context.Background() 16 | query1 := &UsersQuery{} 17 | _, err := client.Users.Current(ctx, query1) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | _, err = client.Users.User(ctx, userID, query1) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /pkg/wakatime/projects_test.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestProjects(t *testing.T) { 10 | apiKey := os.Getenv("WAKATIME_API_KEY") 11 | userID := os.Getenv("WAKATIME_USER_ID") 12 | apiBase := os.Getenv("WAKATIME_API_BASE") 13 | 14 | client := NewClient(apiKey, nil, apiBase) 15 | ctx := context.Background() 16 | query := &ProjectsQuery{} 17 | _, err := client.Projects.Current(ctx, query) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | _, err = client.Projects.User(ctx, userID, query) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /pkg/wakatime/commits_test.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestCommits(t *testing.T) { 10 | apiKey := os.Getenv("WAKATIME_API_KEY") 11 | userID := os.Getenv("WAKATIME_USER_ID") 12 | projectID := os.Getenv("WAKATIME_PROJECT_ID") 13 | client := NewClient(apiKey, nil) 14 | ctx := context.Background() 15 | query := &CommitsQuery{} 16 | _, err := client.Commits.Current(ctx, projectID, query) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | _, err = client.Commits.User(ctx, userID, projectID, query) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /pkg/wakatime/stats_test.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestStats(t *testing.T) { 10 | apiKey := os.Getenv("WAKATIME_API_KEY") 11 | userID := os.Getenv("WAKATIME_USER_ID") 12 | apiBase := os.Getenv("WAKATIME_API_BASE") 13 | 14 | client := NewClient(apiKey, nil, apiBase) 15 | ctx := context.Background() 16 | query := &StatsQuery{} 17 | _, err := client.Stats.Current(ctx, RangeLast7Days, query) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | 22 | _, err = client.Stats.User(ctx, userID, RangeLast7Days, query) 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /pkg/wakatime/durations_test.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestDurations(t *testing.T) { 11 | apiKey := os.Getenv("WAKATIME_API_KEY") 12 | userID := os.Getenv("WAKATIME_USER_ID") 13 | 14 | client := NewClient(apiKey, nil) 15 | ctx := context.Background() 16 | query1 := &DurationsQuery{} 17 | _, err := client.Durations.Current(ctx, query1) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | query2 := &DurationsQuery{Date: String(time.Now().UTC().Format("2006-01-02"))} 22 | _, err = client.Durations.User(ctx, userID, query2) 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 1.x 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ^1.14 18 | id: go 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v2 22 | 23 | - name: Test 24 | env: 25 | WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }} 26 | WAKATIME_USER_ID: "a5b4feda-214d-4ef2-bdc5-9a844c045006" 27 | WAKATIME_PROJECT_ID: "59f75063-a117-44cc-a744-470398d682f2" 28 | run: go test -v ./pkg/wakatime 29 | -------------------------------------------------------------------------------- /pkg/wakatime/query.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | func isNil(i interface{}) bool { 11 | vi := reflect.ValueOf(i) 12 | if vi.Kind() == reflect.Ptr { 13 | return vi.IsNil() 14 | } 15 | return false 16 | } 17 | 18 | // Query defines from the url.Values. 19 | type Query url.Values 20 | 21 | // Parse parses the given data to a Query. 22 | // Only parses the elements with query tag. 23 | func (q *Query) Parse(data interface{}) error { 24 | 25 | typ := reflect.TypeOf(data) 26 | value := reflect.ValueOf(data) 27 | if value.Kind() == reflect.Ptr { 28 | if value.IsNil() { 29 | return nil 30 | } 31 | value = value.Elem() 32 | typ = typ.Elem() 33 | } 34 | 35 | for i := 0; i < value.NumField(); i++ { 36 | tag := strings.Split(typ.Field(i).Tag.Get("query"), ",")[0] 37 | v := value.Field(i) 38 | if v.Kind() == reflect.Ptr { 39 | if v.IsNil() { 40 | continue 41 | } 42 | v = v.Elem() 43 | } 44 | 45 | (*url.Values)(q).Set(tag, fmt.Sprintf("%v", v)) 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/wakatime/useragents.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | ) 9 | 10 | // UserAgentsService defines endpoint of the useragents API. 11 | // A single user's profile information 12 | type UserAgentsService service 13 | 14 | // UserAgentsQuery defines the query of the userAgents API. 15 | type UserAgentsQuery struct { 16 | } 17 | 18 | // UserAgentsResponse defines the response of the userAgents API. 19 | // see https://wakatime.com/developers#userAgents for more information. 20 | type UserAgentsResponse struct { 21 | Data *[]UserAgentsData `json:"data,omitempty"` 22 | } 23 | 24 | // UserAgentsData defines the data of the UserAgentsResponse. 25 | type UserAgentsData struct { 26 | ID *string `json:"id,omitempty"` 27 | Value *string `json:"value,omitempty"` 28 | Editor *string `json:"editor,omitempty"` 29 | Version *string `json:"version,omitempty"` 30 | OS *string `json:"os,omitempty"` 31 | LastSeen *string `json:"last_seen,omitempty"` 32 | CreatedAt *string `json:"created_at,omitempty"` 33 | } 34 | 35 | // Current do the request of the UserAgents API with current user. 36 | func (u *UserAgentsService) Current(ctx context.Context, query *UserAgentsQuery) (*UserAgentsResponse, error) { 37 | return u.User(ctx, "current", query) 38 | } 39 | 40 | // User do the request of the UserAgents API with the given user. 41 | func (u *UserAgentsService) User(ctx context.Context, userID string, query *UserAgentsQuery) (*UserAgentsResponse, error) { 42 | q := Query{} 43 | q.Parse(query) 44 | endpoint := fmt.Sprintf("users/%s/user_agents", userID) 45 | return u.do(ctx, endpoint, (url.Values)(q)) 46 | } 47 | 48 | func (u *UserAgentsService) do(ctx context.Context, endpoint string, query url.Values) (*UserAgentsResponse, error) { 49 | respBytes, err := (*service)(u).get(ctx, endpoint, query) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | var response = &UserAgentsResponse{} 55 | err = json.Unmarshal(respBytes, &response) 56 | if err != nil { 57 | return nil, fmt.Errorf("unmarshal error :%w", err) 58 | } 59 | 60 | return response, nil 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wakatime-go 2 | 3 | [WIP] 🕘 Go library for accessing the [Wakatime](https://wakatime.com/developers#introduction) API 4 | 5 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/YouEclipse/wakatime-go/pkg) ![Go](https://github.com/YouEclipse/wakatime-go/workflows/Go/badge.svg) 6 | 7 | ## Install 8 | 9 | ### Requirements 10 | 11 | > Go version 1.13+ 12 | 13 | ### Installation 14 | 15 | ```shell 16 | go get github.com/YouEclipse/wakatime-go 17 | ``` 18 | 19 | ## Quick start 20 | 21 | ```golang 22 | 23 | import ( 24 | "github.com/YouEclipse/wakatime-go/pkg/wakatime" 25 | ) 26 | 27 | func main() { 28 | 29 | apiKey := os.Getenv("WAKATIME_API_KEY") 30 | client := wakatime.NewClient(apiKey, &http.Client{}) 31 | 32 | ctx := context.Background() 33 | query := &wakatime.StatsQuery{} 34 | 35 | stats, err := client.Stats.Current(ctx, wakatime.RangeLast7Days, query) 36 | 37 | ... 38 | } 39 | 40 | 41 | 42 | ``` 43 | 44 | ## Features v0.1.0 45 | 46 | ### TODOs 47 | 48 | - [x] [Commits](https://wakatime.com/developers#commits) 49 | - [x] [Durations](https://wakatime.com/developers#durations) 50 | - [x] [Goals](https://wakatime.com/developers#goals) 51 | - [ ] [Heartbeats](https://wakatime.com/developers#heartbeats) 52 | - [ ] [Leaders](https://wakatime.com/developers#leaders) 53 | - [ ] [Meta](https://wakatime.com/developers#meta) 54 | - [ ] [Org Dashboard Member Durations](https://wakatime.com/developers#org_dashboard_member_durations) 55 | - [ ] [Org Dashboard Member Summaries](https://wakatime.com/developers#org_dashboard_member_summaries) 56 | - [ ] [Org Dashboard Members](https://wakatime.com/developers#org_dashboard_members) 57 | - [ ] [Org Dashboards](https://wakatime.com/developers#org_dashboards) 58 | - [ ] [Orgs](https://wakatime.com/developers#orgs) 59 | - [ ] [Private Leaderboards](https://wakatime.com/developers#private_leaderboards) 60 | - [ ] [Private Leaderboards Leaders](https://wakatime.com/developers#private_leaderboards_leaders) 61 | - [x] [Projects](https://wakatime.com/developers#projects) 62 | - [x] [Stats](https://wakatime.com/developers#stats) 63 | - [ ] [Summaries](https://wakatime.com/developers#summaries) 64 | - [x] [User Agents](https://wakatime.com/developers#user_agents) 65 | - [x] [Users](https://wakatime.com/developers#users) 66 | 67 | ... 68 | 69 | ## License 70 | 71 | [Apache 2.0](./LICENSE) 72 | -------------------------------------------------------------------------------- /pkg/wakatime/durations.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // DurationService defines endpoint of the durations API. 12 | // A user's coding activity for the given day as an array of durations. 13 | type DurationService service 14 | 15 | // DurationsQuery defines the query of the durations API. 16 | type DurationsQuery struct { 17 | Date *string `query:"date"` 18 | Project *string `query:"project"` 19 | Branches *string `query:"branches"` 20 | Timeouts *string `query:"timeouts"` 21 | WritesOnly *bool `query:"writes_only"` 22 | } 23 | 24 | // DurationsResponse defines the response of the durations API. 25 | // see https://wakatime.com/developers#durations for more information. 26 | type DurationsResponse struct { 27 | Branches []*string `json:"branches,omitempty"` 28 | Data []*DurationsData `json:"data,omitempty"` 29 | End *time.Time `json:"end,omitempty"` 30 | Start *time.Time `json:"start,omitempty"` 31 | Timezone *string `json:"timezone,omitempty"` 32 | } 33 | 34 | // DurationsData defines the data of the DurationsResponse. 35 | type DurationsData struct { 36 | CreatedAt *time.Time `json:"created_at,omitempty"` 37 | Cursorpos *int64 `json:"cursorpos,omitempty"` 38 | Duration *float64 `json:"duration,omitempty"` 39 | ID *string `json:"id,omitempty"` 40 | Lineno *int64 `json:"lineno,omitempty"` 41 | MachineNameID *string `json:"machine_name_id,omitempty"` 42 | Project *string `json:"project,omitempty"` 43 | Time *float64 `json:"time,omitempty"` 44 | UserID *string `json:"user_id,omitempty"` 45 | } 46 | 47 | // Current do the request of the durations API with current user. 48 | func (d *DurationService) Current(ctx context.Context, query *DurationsQuery) (*DurationsResponse, error) { 49 | return d.User(ctx, "current", query) 50 | 51 | } 52 | 53 | // User do the request of the durations API with the given user. 54 | func (d *DurationService) User(ctx context.Context, userID string, query *DurationsQuery) (*DurationsResponse, error) { 55 | if query.Date == nil || *query.Date == "" { 56 | query.Date = String(time.Now().UTC().Format("2006-01-02")) 57 | } 58 | 59 | q := Query{} 60 | q.Parse(query) 61 | endpoint := fmt.Sprintf("users/%s/durations", userID) 62 | return d.do(ctx, endpoint, (url.Values)(q)) 63 | } 64 | 65 | func (d *DurationService) do(ctx context.Context, endpoint string, query url.Values) (*DurationsResponse, error) { 66 | respBytes, err := (*service)(d).get(ctx, endpoint, query) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | var response = &DurationsResponse{} 72 | err = json.Unmarshal(respBytes, &response) 73 | if err != nil { 74 | return nil, fmt.Errorf("unmarshal error :%w", err) 75 | } 76 | 77 | return response, nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/wakatime/users.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | ) 9 | 10 | // UsersService defines endpoint of the users API. 11 | // A single user's profile information 12 | type UsersService service 13 | 14 | // UsersQuery defines the query of the users API. 15 | type UsersQuery struct { 16 | } 17 | 18 | // UsersResponse defines the response of the users API. 19 | // see https://wakatime.com/developers#users for more information. 20 | type UsersResponse struct { 21 | Data *UsersData `json:"data,omitempty"` 22 | } 23 | 24 | // UsersData defines the data of the UsersResponse. 25 | type UsersData struct { 26 | ID *string `json:"id,omitempty"` 27 | HasPremiumFeatures *bool `json:"has_premium_features,omitempty"` 28 | DisplayName *string `json:"display_name,omitempty"` 29 | FullName *string `json:"full_name,omitempty"` 30 | Email *string `json:"email,omitempty"` 31 | Photo *string `json:"photo,omitempty"` 32 | IsEmailPublic *bool `json:"is_email_public,omitempty"` 33 | IsEmailConfirmed *bool `json:"is_email_confirmed,omitempty"` 34 | PublicEmail *string `json:"public_email,omitempty"` 35 | PhotoPublic *bool `json:"photo_public,omitempty"` 36 | Timezone *string `json:"timezone,omitempty"` 37 | LastHeartbeatAt *string `json:"last_heartbeat_at,omitempty"` 38 | LastPlugin *string `json:"last_plugin,omitempty"` 39 | LastPluginName *string `json:"last_plugin_name,omitempty"` 40 | LastProject *string `json:"last_project,omitempty"` 41 | Plan *string `json:"plan,omitempty"` 42 | Username *string `json:"username,omitempty"` 43 | Website *string `json:"website,omitempty"` 44 | HumanReadableWebsite *string `json:"human_readable_website,omitempty"` 45 | Location *string `json:"location,omitempty"` 46 | LoggedTimePublic *bool `json:"logged_time_public,omitempty"` 47 | LanguagesUsedPublic *bool `json:"languages_used_public,omitempty"` 48 | IsHireable *bool `json:"is_hireable,omitempty"` 49 | CreatedAt *string `json:"created_at,omitempty"` 50 | ModifiedAt *string `json:"modified_at,omitempty"` 51 | } 52 | 53 | // Current do the request of the Users API with current user. 54 | func (u *UsersService) Current(ctx context.Context, query *UsersQuery) (*UsersResponse, error) { 55 | return u.User(ctx, "current", query) 56 | } 57 | 58 | // User do the request of the Users API with the given user. 59 | func (u *UsersService) User(ctx context.Context, userID string, query *UsersQuery) (*UsersResponse, error) { 60 | q := Query{} 61 | q.Parse(query) 62 | endpoint := fmt.Sprintf("users/%s/", userID) 63 | return u.do(ctx, endpoint, (url.Values)(q)) 64 | } 65 | 66 | func (u *UsersService) do(ctx context.Context, endpoint string, query url.Values) (*UsersResponse, error) { 67 | respBytes, err := (*service)(u).get(ctx, endpoint, query) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | var response = &UsersResponse{} 73 | err = json.Unmarshal(respBytes, &response) 74 | if err != nil { 75 | return nil, fmt.Errorf("unmarshal error :%w", err) 76 | } 77 | 78 | return response, nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/wakatime/projects.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // ProjectsService defines endpoint of the Projects API. 12 | // List of WakaTime projects for a user. 13 | type ProjectsService service 14 | 15 | // ProjectsQuery defines the query of the Projects API. 16 | type ProjectsQuery struct{} 17 | 18 | // ProjectsResponse defines the response of the Projects API. 19 | // see https://wakatime.com/developers#Projects for more information. 20 | type ProjectsResponse struct { 21 | Data []Project `json:"data,omitempty"` 22 | } 23 | 24 | // Project is the project of the commit. 25 | type Project struct { 26 | CreatedAt *time.Time `json:"created_at,omitempty"` 27 | HasPublicURL *bool `json:"has_public_url,omitempty"` 28 | HTMLEscapedName *string `json:"html_escaped_name,omitempty"` 29 | HumanReadableLastHeartbeatAt *string `json:"human_readable_last_heartbeat_at,omitempty"` 30 | ID *string `json:"id,omitempty"` 31 | LastHeartbeatAt *time.Time `json:"last_heartbeat_at,omitempty"` 32 | Name *string `json:"name,omitempty"` 33 | //Repository *Repository `json:"repository,omitempty"` 34 | URL *string `json:"url,omitempty"` 35 | } 36 | 37 | // Repository is the repository of the project. 38 | type Repository struct { 39 | Badge *string `json:"badge,omitempty"` 40 | CreatedAt *time.Time `json:"created_at,omitempty"` 41 | DefaultBranch *string `json:"default_branch,omitempty"` 42 | Description *string `json:"description,omitempty"` 43 | ForkCount *int64 `json:"fork_count,omitempty"` 44 | FullName *string `json:"full_name,omitempty"` 45 | Homepage *string `json:"homepage,omitempty"` 46 | HTMLURL *string `json:"html_url,omitempty"` 47 | ID *string `json:"id,omitempty"` 48 | ImageIconURL *string `json:"image_icon_url,omitempty"` 49 | IsFork *bool `json:"is_fork,omitempty"` 50 | IsPrivate *bool `json:"is_private,omitempty"` 51 | LastSyncedAt *string `json:"last_synced_at,omitempty"` 52 | ModifiedAt *time.Time `json:"modified_at,omitempty"` 53 | Name *string `json:"name,omitempty"` 54 | Provider *string `json:"provider,omitempty"` 55 | StarCount *int64 `json:"star_count,omitempty"` 56 | URL *string `json:"url,omitempty"` 57 | WakatimeProjectName *string `json:"wakatime_project_name,omitempty"` 58 | WatchCount *int64 `json:"watch_count,omitempty"` 59 | } 60 | 61 | // Current do the request of the Projects API with current user. 62 | func (p *ProjectsService) Current(ctx context.Context, query *ProjectsQuery) (*ProjectsResponse, error) { 63 | return p.User(ctx, "current", query) 64 | 65 | } 66 | 67 | // User do the request of the Projects API with the given user. 68 | func (p *ProjectsService) User(ctx context.Context, userID string, query *ProjectsQuery) (*ProjectsResponse, error) { 69 | q := Query{} 70 | q.Parse(query) 71 | endpoint := fmt.Sprintf("users/%s/projects", userID) 72 | return p.do(ctx, endpoint, (url.Values)(q)) 73 | } 74 | 75 | func (p *ProjectsService) do(ctx context.Context, endpoint string, query url.Values) (*ProjectsResponse, error) { 76 | respBytes, err := (*service)(p).get(ctx, endpoint, query) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | var response = &ProjectsResponse{} 82 | err = json.Unmarshal(respBytes, &response) 83 | if err != nil { 84 | return nil, fmt.Errorf("unmarshal error :%w", err) 85 | } 86 | 87 | return response, nil 88 | } 89 | -------------------------------------------------------------------------------- /pkg/wakatime/goals.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | ) 9 | 10 | // GoalsService defines endpoint of the Goals API. 11 | // A user's coding activity for the given day as an array of Goals. 12 | type GoalsService service 13 | 14 | // GoalsQuery defines the query of the Goals API. 15 | type GoalsQuery struct{} 16 | 17 | // GoalsResponse defines the response of the Goals API. 18 | // see https://wakatime.com/developers#Goals for more information. 19 | type GoalsResponse struct { 20 | Data []*GoalsData `json:"data,omitempty"` 21 | Total *int `json:"total,omitempty"` 22 | TotalPages *int `json:"total_pages,omitempty"` 23 | } 24 | 25 | // GoalsData defines the data of the GoalsResponse. 26 | type GoalsData struct { 27 | AverageStatus *string `json:"average_status,omitempty"` 28 | ChartData *ChartData `json:"chart_data,omitempty"` 29 | CumulativeStatus *string `json:"cumulative_status,omitempty"` 30 | Delta *string `json:"delta,omitempty"` 31 | ID *string `json:"id,omitempty"` 32 | IgnoreDays []*string `json:"ignore_days,omitempty"` 33 | ImproveByPercent *float64 `json:"improve_by_percent,omitempty"` 34 | IsEnabled *bool `json:"is_enabled,omitempty"` 35 | Languages []*string `json:"languages,omitempty"` 36 | Projects []*string `json:"projects,omitempty"` 37 | RangeText *string `json:"range_text,omitempty"` 38 | Seconds *int `json:"seconds,omitempty"` 39 | Status *string `json:"status,omitempty"` 40 | Subscribers []*Subscribers `json:"subscribers,omitempty"` 41 | Title *string `json:"title,omitempty"` 42 | Type *string `json:"type,omitempty"` 43 | } 44 | 45 | // ChartData defines the data of the Chart of the GoalsData 46 | type ChartData struct { 47 | ActualSeconds *float64 `json:"actual_seconds,omitempty"` 48 | ActualSecondsText *string `json:"actual_seconds_text,omitempty"` 49 | GoalSeconds *int `json:"goal_seconds,omitempty"` 50 | GoalSecondsText *string `json:"goal_seconds_text,omitempty"` 51 | Range *RangeData `json:"range,omitempty"` 52 | } 53 | 54 | // RangeData defines the Range of the ChartData 55 | type RangeData struct { 56 | Date *string `json:"date,omitempty"` 57 | End *string `json:"end,omitempty"` 58 | Start *string `json:"start,omitempty"` 59 | Text *string `json:"text,omitempty"` 60 | Timezone *string `json:"timezone,omitempty"` 61 | } 62 | 63 | // Subscribers defines the Subscribers of the GoalsData 64 | type Subscribers struct { 65 | Email *string `json:"email,omitempty"` 66 | EmailFrequency *string `json:"email_frequency,omitempty"` 67 | FullName *string `json:"full_name,omitempty"` 68 | UserID *string `json:"user_id,omitempty"` 69 | Username *string `json:"username,omitempty"` 70 | } 71 | 72 | // Current do the request of the goals API with current user. 73 | func (g *GoalsService) Current(ctx context.Context, query *GoalsQuery) (*GoalsResponse, error) { 74 | return g.User(ctx, "current", query) 75 | 76 | } 77 | 78 | // User do the request of the Goals API with the given user. 79 | func (g *GoalsService) User(ctx context.Context, userID string, query *GoalsQuery) (*GoalsResponse, error) { 80 | q := Query{} 81 | q.Parse(query) 82 | endpoint := fmt.Sprintf("users/%s/goals", userID) 83 | return g.do(ctx, endpoint, (url.Values)(q)) 84 | } 85 | 86 | func (g *GoalsService) do(ctx context.Context, endpoint string, query url.Values) (*GoalsResponse, error) { 87 | respBytes, err := (*service)(g).get(ctx, endpoint, query) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | var response = &GoalsResponse{} 93 | err = json.Unmarshal(respBytes, &response) 94 | if err != nil { 95 | return nil, fmt.Errorf("unmarshal error :%w", err) 96 | } 97 | 98 | return response, nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/wakatime/wakatime.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "reflect" 14 | ) 15 | 16 | // Client defines the wakatime Client. 17 | type Client struct { 18 | common service // Reuse a single struct instead of allocating one for each service on the heap. 19 | 20 | Commits *CommitService 21 | Durations *DurationService 22 | Goals *GoalsService 23 | Projects *ProjectsService 24 | Stats *StatService 25 | UserAgents *UserAgentsService 26 | Users *UsersService 27 | } 28 | 29 | const ( 30 | // APIBase is the URL prefix for the wakatime API 31 | APIBase = "https://wakatime.com/api/v1/" 32 | ) 33 | 34 | // NewClient returns a new Client with the given API key. 35 | func NewClient(apikey string, httpClient *http.Client, apiBase ...string) *Client { 36 | c := &Client{} 37 | if httpClient == nil { 38 | httpClient = &http.Client{} 39 | } 40 | c.common = service{ 41 | apikey: apikey, 42 | apiBase: APIBase, 43 | client: httpClient, 44 | } 45 | if len(apiBase) > 0 && apiBase[0] != "" { 46 | c.common.apiBase = apiBase[0] 47 | } 48 | 49 | c.Commits = (*CommitService)(&c.common) 50 | c.Durations = (*DurationService)(&c.common) 51 | c.Goals = (*GoalsService)(&c.common) 52 | c.Projects = (*ProjectsService)(&c.common) 53 | c.Stats = (*StatService)(&c.common) 54 | c.UserAgents = (*UserAgentsService)(&c.common) 55 | c.Users = (*UsersService)(&c.common) 56 | 57 | return c 58 | } 59 | 60 | type service struct { 61 | apikey string 62 | apiBase string 63 | client *http.Client 64 | } 65 | 66 | func (s *service) get(ctx context.Context, endpoint string, query url.Values) ([]byte, error) { 67 | u := s.apiBase + endpoint 68 | 69 | request, err := http.NewRequest("GET", u, nil) 70 | if err != nil { 71 | return nil, fmt.Errorf("new request err :%w", err) 72 | } 73 | request.URL.RawQuery = query.Encode() 74 | 75 | request.Header.Set("Content-Type", "application/json") 76 | request.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(s.apikey))) 77 | 78 | response, err := s.client.Do(request) 79 | if err != nil { 80 | return nil, fmt.Errorf("http client do err :%w", err) 81 | } 82 | 83 | defer response.Body.Close() 84 | bodyBytes, err := ioutil.ReadAll(response.Body) 85 | if err != nil { 86 | return nil, fmt.Errorf("read body err :%w", err) 87 | } 88 | 89 | if !(response.StatusCode >= 200 && response.StatusCode <= 299) { 90 | return nil, fmt.Errorf("response code expects 2xx, but got %d, response body %s", 91 | response.StatusCode, bodyBytes) 92 | } 93 | 94 | return bodyBytes, nil 95 | } 96 | 97 | func (s *service) post(ctx context.Context, endpoint string, query url.Values, params interface{}) ([]byte, error) { 98 | u := s.apiBase + endpoint 99 | 100 | var reqBody io.Reader 101 | v := reflect.ValueOf(params) 102 | if v.Kind() == reflect.Ptr && v.IsNil() { 103 | paramsBytes, err := json.Marshal(params) 104 | if err != nil { 105 | return nil, fmt.Errorf("marshal params err :%w", err) 106 | } 107 | reqBody = bytes.NewBuffer(paramsBytes) 108 | } else { 109 | reqBody = nil 110 | } 111 | 112 | request, err := http.NewRequest("POST", u, reqBody) 113 | if err != nil { 114 | return nil, fmt.Errorf("new request err :%w", err) 115 | } 116 | 117 | request.URL.RawQuery = query.Encode() 118 | 119 | request.Header.Set("Content-Type", "application/json") 120 | request.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(s.apikey))) 121 | 122 | response, err := s.client.Do(request) 123 | if err != nil { 124 | return nil, fmt.Errorf("http client do err :%w", err) 125 | } 126 | 127 | defer response.Body.Close() 128 | bodyBytes, err := ioutil.ReadAll(response.Body) 129 | if err != nil { 130 | return nil, fmt.Errorf("read body err :%w", err) 131 | } 132 | 133 | if response.StatusCode != 200 { 134 | return nil, fmt.Errorf("response code expects 200, but got %d, response body %s", 135 | response.StatusCode, bodyBytes) 136 | } 137 | 138 | return bodyBytes, nil 139 | } 140 | 141 | // Bool allocates a new bool value to store v and returns a pointer to it. 142 | func Bool(v bool) *bool { return &v } 143 | 144 | // Int64 allocates a new int64 value to store v and returns a pointer to it. 145 | func Int64(v int64) *int64 { return &v } 146 | 147 | // String allocates a new string value to store v and returns a pointer to it. 148 | func String(v string) *string { return &v } 149 | -------------------------------------------------------------------------------- /pkg/wakatime/commits.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // CommitService defines endpoint of the commits API. 12 | // List of commits for a WakaTime project showing the time spent coding in each commit. 13 | type CommitService service 14 | 15 | // CommitsQuery defines the query of the commits API. 16 | type CommitsQuery struct { 17 | Author *string `query:"author"` 18 | Page *int64 `query:"page"` 19 | } 20 | 21 | // CommitsResponse defines the response of the commits API. 22 | // see https://wakatime.com/developers#commits for more information. 23 | type CommitsResponse struct { 24 | Author *string `json:"author,omitempty"` 25 | Commits []*Commit `json:"commits,omitempty"` 26 | NextPage *int64 `json:"next_page,omitempty"` 27 | NextPageURL *string `json:"next_page_url,omitempty"` 28 | Page int `json:"page,omitempty"` 29 | PrevPage *int64 `json:"prev_page,omitempty"` 30 | PrevPageURL *string `json:"prev_page_url,omitempty"` 31 | Project *Project `json:"project,omitempty"` 32 | Status *string `json:"status,omitempty"` 33 | TotalPages int `json:"total_pages,omitempty"` 34 | } 35 | 36 | // Commit defines the commit data of the commits API response. 37 | type Commit struct { 38 | AuthorAvatarURL *string `json:"author_avatar_url,omitempty"` 39 | AuthorDate *time.Time `json:"author_date,omitempty"` 40 | AuthorEmail *string `json:"author_email,omitempty"` 41 | AuthorHTMLURL *string `json:"author_html_url,omitempty"` 42 | AuthorID *string `json:"author_id,omitempty"` 43 | AuthorName *string `json:"author_name,omitempty"` 44 | AuthorURL *string `json:"author_url,omitempty"` 45 | AuthorUsername *string `json:"author_username,omitempty"` 46 | Branch *string `json:"branch,omitempty"` 47 | CommitterAvatarURL *string `json:"committer_avatar_url,omitempty"` 48 | CommitterDate *time.Time `json:"committer_date,omitempty"` 49 | CommitterEmail *string `json:"committer_email,omitempty"` 50 | CommitterHTMLURL *string `json:"committer_html_url,omitempty"` 51 | CommitterName *string `json:"committer_name,omitempty"` 52 | CommitterURL *string `json:"committer_url,omitempty"` 53 | CommitterUsername *string `json:"committer_username,omitempty"` 54 | CreatedAt *time.Time `json:"created_at,omitempty"` 55 | Hash *string `json:"hash,omitempty"` 56 | HTMLURL *string `json:"html_url,omitempty"` 57 | HumanReadableDate *string `json:"human_readable_date,omitempty"` 58 | HumanReadableNaturalDate *string `json:"human_readable_natural_date,omitempty"` 59 | HumanReadableTotal *string `json:"human_readable_total,omitempty"` 60 | HumanReadableTotalWithSeconds *string `json:"human_readable_total_with_seconds,omitempty"` 61 | ID *string `json:"id,omitempty"` 62 | Message *string `json:"message,omitempty"` 63 | Ref *string `json:"ref,omitempty"` 64 | TotalSeconds *int64 `json:"total_seconds,omitempty"` 65 | TruncatedHash *string `json:"truncated_hash,omitempty"` 66 | URL *string `json:"url,omitempty"` 67 | } 68 | 69 | // Current do the request of the commits API with current user. 70 | func (d *CommitService) Current(ctx context.Context, projectID string, query *CommitsQuery) (*CommitsResponse, error) { 71 | return d.User(ctx, "current", projectID, query) 72 | 73 | } 74 | 75 | // User do the request of the commits API with the given user. 76 | func (d *CommitService) User(ctx context.Context, userID, projectID string, query *CommitsQuery) (*CommitsResponse, error) { 77 | endpoint := fmt.Sprintf("users/%s/projects/%s/commits", userID, projectID) 78 | 79 | q := Query{} 80 | q.Parse(query) 81 | return d.do(ctx, endpoint, (url.Values)(q)) 82 | } 83 | 84 | func (d *CommitService) do(ctx context.Context, endpoint string, query url.Values) (*CommitsResponse, error) { 85 | respBytes, err := (*service)(d).get(ctx, endpoint, query) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | var response = &CommitsResponse{} 91 | err = json.Unmarshal(respBytes, &response) 92 | if err != nil { 93 | return nil, fmt.Errorf("unmarshal error :%w", err) 94 | } 95 | 96 | return response, nil 97 | } 98 | -------------------------------------------------------------------------------- /pkg/wakatime/stats.go: -------------------------------------------------------------------------------- 1 | package wakatime 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // StatService defines endpoint of the durations API. 12 | // A user's coding activity for the given time range. 13 | type StatService service 14 | 15 | // TimeRange defines the string type of time range. 16 | type TimeRange string 17 | 18 | const ( 19 | // RangeLast7Days is the range of last 7 days 20 | RangeLast7Days TimeRange = "last_7_days" 21 | // RangeLast30Days is the range of last 30 days 22 | RangeLast30Days TimeRange = "last_30_days" 23 | // RangeLast6Months is the range of last 6 months 24 | RangeLast6Months TimeRange = "last_6_months" 25 | // RangeLastYear is the range of last year 26 | RangeLastYear TimeRange = "last_year" 27 | ) 28 | 29 | // StatsQuery defines query of the stats API. 30 | type StatsQuery struct { 31 | Timeout *int64 `query:"timeout"` 32 | WritesOnly *bool `query:"writes_only"` 33 | Project *string `query:"project"` 34 | } 35 | 36 | // StatsResponse defines the response of the stats API. 37 | type StatsResponse struct { 38 | Data StatsData `json:"data,omitempty"` 39 | } 40 | 41 | // StatsData defines the data of stats API response. 42 | type StatsData struct { 43 | BestDay *BestDay `json:"best_day,omitempty"` 44 | Categories []*StatItem `json:"categories,omitempty"` 45 | CreatedAt *time.Time `json:"created_at,omitempty"` 46 | DailyAverage *float64 `json:"daily_average,omitempty"` 47 | DailyAverageIncludingOtherLanguage *int64 `json:"daily_average_including_other_language,omitempty"` 48 | DaysIncludingHolidays *int64 `json:"days_including_holidays,omitempty"` 49 | DaysMinusHolidays *int64 `json:"days_minus_holidays,omitempty"` 50 | Dependencies []interface{} `json:"dependencies,omitempty"` 51 | Editors []*StatItem `json:"editors,omitempty"` 52 | End *time.Time `json:"end,omitempty"` 53 | Holidays *int64 `json:"holidays,omitempty"` 54 | HumanReadableDailyAverage *string `json:"human_readable_daily_average,omitempty"` 55 | HumanReadableDailyAverageIncludingOtherLanguage *string `json:"human_readable_daily_average_including_other_language,omitempty"` 56 | HumanReadableTotal *string `json:"human_readable_total,omitempty"` 57 | HumanReadableTotalIncludingOtherLanguage *string `json:"human_readable_total_including_other_language,omitempty"` 58 | ID *string `json:"id,omitempty"` 59 | IsAlreadyUpdating *bool `json:"is_already_updating,omitempty"` 60 | IsCodingActivityVisible *bool `json:"is_coding_activity_visible,omitempty"` 61 | IsIncludingToday *bool `json:"is_including_today,omitempty"` 62 | IsOtherUsageVisible *bool `json:"is_other_usage_visible,omitempty"` 63 | IsStuck *bool `json:"is_stuck,omitempty"` 64 | IsUpToDate *bool `json:"is_up_to_date,omitempty"` 65 | Languages []StatItem `json:"languages,omitempty"` 66 | Machines []Machines `json:"machines,omitempty"` 67 | ModifiedAt *time.Time `json:"modified_at,omitempty"` 68 | OperatingSystems []*StatItem `json:"operating_systems,omitempty"` 69 | Project *interface{} `json:"project,omitempty"` 70 | Projects []*StatItem `json:"projects,omitempty"` 71 | Range *string `json:"range,omitempty"` 72 | Start *time.Time `json:"start,omitempty"` 73 | Status *string `json:"status,omitempty"` 74 | Timeout *int64 `json:"timeout,omitempty"` 75 | Timezone *string `json:"timezone,omitempty"` 76 | TotalSeconds *float64 `json:"total_seconds,omitempty"` 77 | TotalSecondsIncludingOtherLanguage *float64 `json:"total_seconds_including_other_language,omitempty"` 78 | UserID *string `json:"user_id,omitempty"` 79 | Username *string `json:"username,omitempty"` 80 | WritesOnly *bool `json:"writes_only,omitempty"` 81 | } 82 | 83 | // BestDay is the day with most coding time. 84 | type BestDay struct { 85 | CreatedAt *time.Time `json:"created_at,omitempty"` 86 | Date *string `json:"date,omitempty"` 87 | ID *string `json:"id,omitempty"` 88 | ModifiedAt *time.Time `json:"modified_at,omitempty"` 89 | Text *string `json:"text,omitempty"` 90 | TotalSeconds *float64 `json:"total_seconds,omitempty"` 91 | } 92 | 93 | // StatItem is the item for a stat. 94 | type StatItem struct { 95 | Digital *string `json:"digital,omitempty"` 96 | Hours *int64 `json:"hours,omitempty"` 97 | Minutes *int64 `json:"minutes,omitempty"` 98 | Name *string `json:"name,omitempty"` 99 | Percent *float64 `json:"percent,omitempty"` 100 | Text *string `json:"text,omitempty"` 101 | TotalSeconds *float64 `json:"total_seconds,omitempty"` 102 | } 103 | 104 | // Machine is the details of machine. 105 | type Machine struct { 106 | CreatedAt *time.Time `json:"created_at,omitempty"` 107 | ID *string `json:"id,omitempty"` 108 | IP *string `json:"ip,omitempty"` 109 | LastSeenAt *time.Time `json:"last_seen_at,omitempty"` 110 | Name *string `json:"name,omitempty"` 111 | Value *string `json:"value,omitempty"` 112 | } 113 | 114 | // Machines is the stats for machines. 115 | type Machines struct { 116 | Digital *string `json:"digital,omitempty"` 117 | Hours *int64 `json:"hours,omitempty"` 118 | Machine *Machine `json:"machine,omitempty"` 119 | Minutes *int64 `json:"minutes,omitempty"` 120 | Name *string `json:"name,omitempty"` 121 | Percent *float64 `json:"percent,omitempty"` 122 | Text *string `json:"text,omitempty"` 123 | TotalSeconds *float64 `json:"total_seconds,omitempty"` 124 | } 125 | 126 | // Current do the request of the durations API with current user. 127 | func (d *StatService) Current(ctx context.Context, timeRange TimeRange, query *StatsQuery) (*StatsResponse, error) { 128 | return d.User(ctx, "current", timeRange, query) 129 | 130 | } 131 | 132 | // User do the request of the durations API with the given user. 133 | func (d *StatService) User(ctx context.Context, userID string, timeRange TimeRange, query *StatsQuery) (*StatsResponse, error) { 134 | 135 | q := Query{} 136 | q.Parse(query) 137 | endpoint := fmt.Sprintf("users/%s/stats/%s", userID, timeRange) 138 | return d.do(ctx, endpoint, (url.Values)(q)) 139 | } 140 | 141 | func (d *StatService) do(ctx context.Context, endpoint string, query url.Values) (*StatsResponse, error) { 142 | respBytes, err := (*service)(d).get(ctx, endpoint, query) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | var response = &StatsResponse{} 148 | err = json.Unmarshal(respBytes, &response) 149 | if err != nil { 150 | return nil, fmt.Errorf("unmarshal error :%w", err) 151 | } 152 | 153 | return response, nil 154 | } 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [2020] [chuiyouwu@gmail.com] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | --------------------------------------------------------------------------------