Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MI-1931] Add API to get projects and get tasks #3

Merged
merged 30 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3a82837
[MI-1846]: Added nvmrc file
avas27JTG Jun 1, 2022
fe5b450
Modular folder structure
avas27JTG Jun 5, 2022
a87e1b8
Modular folder structure
avas27JTG Jun 5, 2022
7a74d92
[MI-1846]: Base setup
avas27JTG Jun 6, 2022
80a125d
[MI-1846]: Refactored server base setup
avas27JTG Jun 29, 2022
cf35f6f
[MI-1854]: Implement OAuth to access Azure DevOps services
avas27JTG Jul 12, 2022
b5d216b
[MI-1931] Add API to get projects and get tasks
ayusht2810 Jul 13, 2022
5f9a075
[MI-1931] Self review fixes
ayusht2810 Jul 14, 2022
d5682c3
[MI-1931] Review fixes 1
ayusht2810 Jul 15, 2022
d167803
[MI-1931] Self review fix
ayusht2810 Jul 15, 2022
971e0a5
[MI-1846]: Review fixes
avas27JTG Jul 15, 2022
4ffffbd
[MI-1931] Review fixes 2
ayusht2810 Jul 15, 2022
30cc3f1
[MI-1931] Add json error handling
ayusht2810 Jul 15, 2022
49aaa96
[MI-1931] Add comment to the code
ayusht2810 Jul 18, 2022
61e99db
[MI-1931] Add error check
ayusht2810 Jul 20, 2022
7db4e3c
[MI-1846]: Review fixes
avas27JTG Jul 22, 2022
21ff45b
Merge branch 'MI-1846' of github.com:Brightscout/mattermost-plugin-az…
avas27JTG Jul 22, 2022
1313fcc
[MI-1854]: Review fixes
avas27JTG Jul 22, 2022
7fd2e97
[MI-1854]: Removed unused config
avas27JTG Jul 22, 2022
c5ef16e
[MI-1854]: Added logic to verify state
avas27JTG Jul 25, 2022
1f7fc44
[MI-1931] Pull from parent [MI-1854_1]
ayusht2810 Jul 25, 2022
c6b2e7c
[MI-1931] Run fmt
ayusht2810 Jul 25, 2022
a0f77a6
[MI-1854]: Removed unused configs
avas27JTG Jul 26, 2022
d45ea73
Merge branch 'master' of github.com:Brightscout/mattermost-plugin-azu…
avas27JTG Jul 26, 2022
58faba5
[MI-1854]: Review fixes
avas27JTG Jul 27, 2022
d9eab3b
[MI-1931] Pull from parent [MI-1854_1]
ayusht2810 Jul 27, 2022
000e9df
[MI-1931] Pull from master
ayusht2810 Jul 28, 2022
cee16dc
[MI-1931] Remove unused code
ayusht2810 Jul 29, 2022
217f50e
[MI-1931] Correct spelling error
ayusht2810 Jul 29, 2022
39cbe06
[MI-1931] Correct formatting
ayusht2810 Aug 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions server/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,45 @@ const (
CommandTriggerName = "azuredevops"
HelpText = "###### Mattermost Azure Devops Plugin - Slash Command Help\n"
InvalidCommand = "Invalid command parameters. Please use `/azuredevops help` for more information."

// Azure API Routes
// TODO: WIP.
// GetProjects = "/%s/_apis/projects"
ayusht2810 marked this conversation as resolved.
Show resolved Hide resolved
// GetTasksID = "/%s/_apis/wit/wiql"
// GetTasks = "/%s/_apis/wit/workitems"

// Azure API versions
// TODO: WIP.
// ProjectAPIVersion = "7.1-preview.4"
// TasksIDAPIVersion = "5.1"
// TasksAPIVersion = "6.0"

// Authorization constants
Bearer = "Bearer %s"
manojmalik20 marked this conversation as resolved.
Show resolved Hide resolved
Authorization = "Authorization"

// Limits
// TODO: WIP.
// ProjectLimit = 10
// TaskLimit = 10

// TODO: WIP.
// URL filters
// Organization = "organization"
// Project = "project"
// Status = "status"
// AssignedTo = "assigned_to"
// Page = "page"

// TODO: WIP.
// Tasks status
// Doing = "doing"
// Todo = "to-do"
// Done = "done"

// TODO: WIP.
// Query params constants
// PageQueryParam = "$top"
// APIVersionQueryParam = "api-version"
// IDsQueryParam = "ids"
)
14 changes: 14 additions & 0 deletions server/constants/error_messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package constants

const (
ayusht2810 marked this conversation as resolved.
Show resolved Hide resolved
// Error messages
Error = "error"
NotAuthorized = "not authorized"
// TODO: WIP.
// InvalidPageNumber = "invalid page number"
// OrganizationRequired = "organization is required"
// ProjectRequired = "project is required"
// InvalidStatus = "invalid status"
// InvalidAssignedTo = "you can only see tasks assigned to yourself"
// NoResultPresent = "no results are present"
)
File renamed without changes.
9 changes: 9 additions & 0 deletions server/constants/taskQuery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package constants

// TODO: WIP.
ayusht2810 marked this conversation as resolved.
Show resolved Hide resolved
// const (
// // TaskQuery
// TaskQuery = "Select [System.Id] From WorkItems Where [System.TeamProject] = '%s'"
// TaskQueryStatusFilter = " and [System.State] = '%s'"
// TaskQueryAssignedToFilter = " and [System.AssignedTo] = @me"
// )
140 changes: 140 additions & 0 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/gorilla/mux"

"github.com/Brightscout/mattermost-plugin-azure-devops/server/constants"
"github.com/Brightscout/mattermost-plugin-azure-devops/server/serializers"
)

// Initializes the plugin REST API
Expand All @@ -30,11 +31,150 @@ func (p *Plugin) InitRoutes() {
// OAuth
s.HandleFunc(constants.PathOAuthConnect, p.OAuthConnect).Methods(http.MethodGet)
s.HandleFunc(constants.PathOAuthCallback, p.OAuthComplete).Methods(http.MethodGet)
// TODO: WIP.
// s.HandleFunc("/projects", p.handleGetProjects).Methods(http.MethodGet)
// s.HandleFunc("/tasks", p.handleGetTasks).Methods(http.MethodGet)

// TODO: for testing purpose, remove later
s.HandleFunc("/test", p.testAPI).Methods(http.MethodGet)
}

func (p *Plugin) handleError(w http.ResponseWriter, r *http.Request, error *serializers.Error) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(error.Code)
message := map[string]string{constants.Error: error.Message}
response, err := json.Marshal(message)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
if _, err := w.Write(response); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

// TODO: WIP.
// API to get projects in an organization.
// func (p *Plugin) handleGetProjects(w http.ResponseWriter, r *http.Request) {
// mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
// if mattermostUserID == "" {
// http.Error(w, constants.NotAuthorized, http.StatusUnauthorized)
// return
// }

// organization := r.URL.Query().Get("organization")
// if organization == "" {
// http.Error(w, constants.OrganizationRequired, http.StatusBadRequest)
// return
// }

// page := StringToInt(r.URL.Query().Get("page"))
// if page <= 0 {
// http.Error(w, constants.InvalidPageNumber, http.StatusBadRequest)
// return
// }

// // Wrap all query params.
// queryParams := map[string]interface{}{
// "organization": organization,
// "page": page,
// }

// boards, err := p.Client.GetProjectList(queryParams, mattermostUserID)
// if err != nil {
// w.WriteHeader(http.StatusInternalServerError)
// if _, err = w.Write([]byte(err.Error())); err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// }
// return
// }

// response, err := json.Marshal(boards)
// if err != nil {
// w.WriteHeader(http.StatusInternalServerError)
// if _, err = w.Write([]byte(err.Error())); err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// }
// return
// }
// w.Header().Add("Content-Type", "application/json")
// if _, err := w.Write(response); err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// }
// }

// TODO: WIP.
// API to get tasks of a projects in an organization.
// func (p *Plugin) handleGetTasks(w http.ResponseWriter, r *http.Request) {
// mattermostUserID := r.Header.Get(constants.HeaderMattermostUserID)
// if mattermostUserID == "" {
// error := serializers.Error{Code: http.StatusUnauthorized, Message: constants.NotAuthorized}
// p.handleError(w, r, &error)
// return
// }
// statusData := map[string]string{
// constants.Doing: "doing",
// constants.Todo: "To Do",
// constants.Done: "done",
// }
// organization := r.URL.Query().Get(constants.Organization)
// if organization == "" {
// error := serializers.Error{Code: http.StatusBadRequest, Message: constants.OrganizationRequired}
// p.handleError(w, r, &error)
// return
// }
// project := r.URL.Query().Get(constants.Project)
// if project == "" {
// error := serializers.Error{Code: http.StatusBadRequest, Message: constants.ProjectRequired}
// p.handleError(w, r, &error)
// return
// }
// status := r.URL.Query().Get(constants.Status)
// if status != "" && statusData[status] == "" {
// error := serializers.Error{Code: http.StatusBadRequest, Message: constants.InvalidStatus}
// p.handleError(w, r, &error)
// return
// }
// assignedTo := r.URL.Query().Get(constants.AssignedTo)
// if assignedTo != "" && assignedTo != "me" {
// error := serializers.Error{Code: http.StatusBadRequest, Message: constants.InvalidAssignedTo}
// p.handleError(w, r, &error)
// return
// }
// page := StringToInt(r.URL.Query().Get(constants.Page))
// if page <= 0 {
// error := serializers.Error{Code: http.StatusBadRequest, Message: constants.InvalidPageNumber}
// p.handleError(w, r, &error)
// return
// }

// // Wrap all query params.
// queryParams := map[string]interface{}{
// constants.Organization: organization,
// constants.Project: project,
// constants.Status: statusData[status],
// constants.AssignedTo: assignedTo,
// constants.Page: page,
// }

// tasks, err := p.Client.GetTaskList(queryParams, mattermostUserID)
// if err != nil {
// error := serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()}
// p.handleError(w, r, &error)
// return
// }

// response, err := json.Marshal(tasks)
// if err != nil {
// error := serializers.Error{Code: http.StatusInternalServerError, Message: err.Error()}
// p.handleError(w, r, &error)
// return
// }
// w.Header().Add("Content-Type", "application/json")
// if _, err := w.Write(response); err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// }
// }

func (p *Plugin) WithRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
Expand Down
99 changes: 95 additions & 4 deletions server/plugin/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (
type Client interface {
TestApi() (string, error) // TODO: remove later
GenerateOAuthToken(encodedFormValues string) (*serializers.OAuthSuccessResponse, error)
// TODO: WIP.
// GetProjectList(queryParams map[string]interface{}, mattermostUserID string) (*serializers.ProjectList, error)
// GetTaskList(queryParams map[string]interface{}, mattermostUserID string) (*serializers.TaskList, error)
}

type client struct {
Expand All @@ -34,6 +37,88 @@ func (c *client) TestApi() (string, error) {
return "hello world", nil
}

// TODO: WIP.
manojmalik20 marked this conversation as resolved.
Show resolved Hide resolved
// Function to get the list of projects.
// func (azureDevops *client) GetProjectList(queryParams map[string]interface{}, mattermostUserID string) (*serializers.ProjectList, error) {
// var projectList *serializers.ProjectList
// page := queryParams["page"].(int)

// // Query params of URL.
// params := url.Values{}
// params.Add(constants.PageQueryParam, fmt.Sprint(page*constants.ProjectLimit))
// params.Add(constants.APIVersionQueryParam, constants.ProjectAPIVersion)

// // URL to fetch projects list.
// project := fmt.Sprintf(constants.GetProjects, queryParams["organization"])
// if _, err := azureDevops.callJSON(azureDevops.plugin.getConfiguration().AzureDevopsAPIBaseURL, project, http.MethodGet, mattermostUserID, nil, &projectList, params); err != nil {
// return nil, errors.Wrap(err, "failed to get Projects list")
// }

// // Check if new projects are present for current page.
// if page*constants.ProjectLimit >= projectList.Count+constants.ProjectLimit {
// return nil, errors.Errorf(constants.NoResultPresent)
// }
// return projectList, nil
// }

// TODO: WIP.
// Function to get the list of tasks.
// func (azureDevops *client) GetTaskList(queryParams map[string]interface{}, mattermostUserID string) (*serializers.TaskList, error) {
// page := queryParams[constants.Page].(int)

// // Query params of URL.
// params := url.Values{}
// params.Add(constants.PageQueryParam, fmt.Sprint(page*constants.TaskLimit))
// params.Add(constants.APIVersionQueryParam, constants.TasksIDAPIVersion)

// // Query to fetch the tasks IDs list.
// query := fmt.Sprintf(constants.TaskQuery, queryParams[constants.Project])

// // Add filters to the query.
// if queryParams[constants.Status] != "" {
// query += fmt.Sprintf(constants.TaskQueryStatusFilter, queryParams[constants.Status])
// }
// if queryParams[constants.AssignedTo] == "me" {
// query += constants.TaskQueryAssignedToFilter
// }

// // Query payload.
// taskQuery := map[string]string{
// "query": query,
// }
// // URL to fetch tasks IDs list.
// taskIDs := fmt.Sprintf(constants.GetTasksID, queryParams[constants.Organization])

// var taskIDList *serializers.TaskIDList
// if _, err := azureDevops.callJSON(azureDevops.plugin.getConfiguration().AzureDevopsAPIBaseURL, taskIDs, http.MethodPost, mattermostUserID, taskQuery, &taskIDList, params); err != nil {
// return nil, errors.Wrap(err, "failed to get Task ID list")
// }

// // Check if new task ID are present for current page.
// if page*constants.TaskLimit >= len(taskIDList.TaskList)+constants.TaskLimit {
// return nil, errors.Errorf(constants.NoResultPresent)
// }

// var IDs string
// for i := 0; i < len(taskIDList.TaskList); i++ {
// IDs += fmt.Sprint(strconv.Itoa(taskIDList.TaskList[i].ID), ",")
// }

// params = url.Values{}
// params.Add(constants.IDsQueryParam, strings.TrimSuffix(IDs, ","))
// params.Add(constants.APIVersionQueryParam, constants.TasksAPIVersion)

// // URL to fetch tasks list.
// task := fmt.Sprintf(constants.GetTasks, queryParams[constants.Organization])

// var taskList *serializers.TaskList
// if _, err := azureDevops.callJSON(azureDevops.plugin.getConfiguration().AzureDevopsAPIBaseURL, task, http.MethodGet, mattermostUserID, nil, &taskList, params); err != nil {
// return nil, errors.Wrap(err, "failed to get Task list")
// }

// return taskList, nil
// }

func (c *client) GenerateOAuthToken(encodedFormValues string) (*serializers.OAuthSuccessResponse, error) {
var oAuthSuccessResponse *serializers.OAuthSuccessResponse

Expand All @@ -45,23 +130,23 @@ func (c *client) GenerateOAuthToken(encodedFormValues string) (*serializers.OAut
}

// Wrapper to make REST API requests with "application/json" type content
func (c *client) callJSON(url, path, method string, in, out interface{}) (responseData []byte, err error) {
func (c *client) callJSON(url, path, method string, mattermostUserID string, in, out interface{}) (responseData []byte, err error) {
contentType := "application/json"
buf := &bytes.Buffer{}
if err = json.NewEncoder(buf).Encode(in); err != nil {
return nil, err
}
return c.call(url, method, path, contentType, buf, out, "")
return c.call(url, method, path, contentType, mattermostUserID, buf, out, "")
}

// Wrapper to make REST API requests with "application/x-www-form-urlencoded" type content
func (c *client) callFormURLEncoded(url, path, method string, out interface{}, formValues string) (responseData []byte, err error) {
contentType := "application/x-www-form-urlencoded"
return c.call(url, method, path, contentType, nil, out, formValues)
return c.call(url, method, path, contentType, "", nil, out, formValues)
}

// Makes HTTP request to REST APIs
func (c *client) call(basePath, method, path, contentType string, inBody io.Reader, out interface{}, formValues string) (responseData []byte, err error) {
func (c *client) call(basePath, method, path, contentType string, mamattermostUserID string, inBody io.Reader, out interface{}, formValues string) (responseData []byte, err error) {
errContext := fmt.Sprintf("Azure Devops: Call failed: method:%s, path:%s", method, path)
pathURL, err := url.Parse(path)
if err != nil {
Expand Down Expand Up @@ -97,6 +182,12 @@ func (c *client) call(basePath, method, path, contentType string, inBody io.Read
req.Header.Add("Content-Type", contentType)
}

if mamattermostUserID != "" {
if err = c.plugin.AddAuthorization(req, mamattermostUserID); err != nil {
return nil, err
}
}

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
Expand Down
Loading