github.com/elliott5/community@v0.14.1-0.20160709191136-823126fb026a/documize/section/github/github.go (about) 1 // Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved. 2 // 3 // This software (Documize Community Edition) is licensed under 4 // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html 5 // 6 // You can operate outside the AGPL restrictions by purchasing 7 // Documize Enterprise Edition and obtaining a commercial license 8 // by contacting <sales@documize.com>. 9 // 10 // https://documize.com 11 12 package github 13 14 import ( 15 "bytes" 16 "encoding/json" 17 "errors" 18 "fmt" 19 "html/template" 20 "io/ioutil" 21 "net/http" 22 "net/url" 23 "strconv" 24 "strings" 25 26 "github.com/documize/community/documize/api/request" 27 "github.com/documize/community/documize/section/provider" 28 "github.com/documize/community/wordsmith/log" 29 30 gogithub "github.com/google/go-github/github" 31 "golang.org/x/oauth2" 32 ) 33 34 // TODO find a smaller image than the one below 35 const githubGravatar = "https://i2.wp.com/assets-cdn.github.com/images/gravatars/gravatar-user-420.png" 36 37 var meta provider.TypeMeta 38 39 func init() { 40 meta = provider.TypeMeta{} 41 42 meta.ID = "38c0e4c5-291c-415e-8a4d-262ee80ba5df" 43 meta.Title = "GitHub" 44 meta.Description = "Link code commits and issues" 45 meta.ContentType = "github" 46 meta.Callback = Callback 47 } 48 49 // Provider represents GitHub 50 type Provider struct { 51 } 52 53 // Meta describes us. 54 func (*Provider) Meta() provider.TypeMeta { 55 return meta 56 } 57 58 func clientID() string { 59 return request.ConfigString(meta.ConfigHandle(), "clientID") 60 } 61 func clientSecret() string { 62 return request.ConfigString(meta.ConfigHandle(), "clientSecret") 63 } 64 func authorizationCallbackURL() string { 65 // NOTE: URL value must have the path and query "/api/public/validate?section=github" 66 return request.ConfigString(meta.ConfigHandle(), "authorizationCallbackURL") 67 } 68 func validateToken(ptoken string) error { 69 // Github authorization check 70 authClient := gogithub.NewClient((&gogithub.BasicAuthTransport{ 71 Username: clientID(), 72 Password: clientSecret(), 73 }).Client()) 74 _, _, err := authClient.Authorizations.Check(clientID(), ptoken) 75 return err 76 } 77 78 func secretsJSON(token string) string { 79 return `{"token":"` + strings.TrimSpace(token) + `"}` 80 } 81 82 // Command to run the various functions required... 83 func (p *Provider) Command(ctx *provider.Context, w http.ResponseWriter, r *http.Request) { 84 query := r.URL.Query() 85 method := query.Get("method") 86 87 if len(method) == 0 { 88 msg := "missing method name" 89 log.ErrorString("github: " + msg) 90 provider.WriteMessage(w, "gitub", msg) 91 return 92 } 93 94 if method == "config" { 95 var ret struct { 96 CID string `json:"clientID"` 97 URL string `json:"authorizationCallbackURL"` 98 } 99 ret.CID = clientID() 100 ret.URL = authorizationCallbackURL() 101 provider.WriteJSON(w, ret) 102 return 103 } 104 105 defer r.Body.Close() // ignore error 106 107 body, err := ioutil.ReadAll(r.Body) 108 109 if err != nil { 110 msg := "Bad body" 111 log.ErrorString("github: " + msg) 112 provider.WriteMessage(w, "gitub", msg) 113 return 114 } 115 116 // get the secret token in the database 117 ptoken := ctx.GetSecrets("token") 118 119 switch method { 120 121 case "saveSecret": // secret Token update code 122 123 // write the new one, direct from JS 124 if err = ctx.SaveSecrets(string(body)); err != nil { 125 log.Error("github settoken configuration", err) 126 provider.WriteError(w, "github", err) 127 return 128 } 129 provider.WriteEmpty(w) 130 return 131 132 } 133 134 // load the config from the client-side 135 config := githubConfig{} 136 err = json.Unmarshal(body, &config) 137 138 if err != nil { 139 log.Error("github Command Unmarshal", err) 140 provider.WriteError(w, "github", err) 141 return 142 } 143 144 config.Clean() 145 // always use DB version of the token 146 config.Token = ptoken 147 148 client := p.githubClient(config) 149 150 switch method { // the main data handling switch 151 152 case "checkAuth": 153 154 if len(ptoken) == 0 { 155 err = errors.New("empty github token") 156 } else { 157 err = validateToken(ptoken) 158 } 159 if err != nil { 160 // token now invalid, so wipe it 161 ctx.SaveSecrets("") // ignore error, already in an error state 162 log.Error("github check token validation", err) 163 provider.WriteError(w, "github", err) 164 return 165 } 166 provider.WriteEmpty(w) 167 return 168 169 case tagCommitsData: 170 171 render, err := p.getCommits(client, config) 172 if err != nil { 173 log.Error("github getCommits:", err) 174 provider.WriteError(w, "github", err) 175 return 176 } 177 178 provider.WriteJSON(w, render) 179 180 case tagIssuesData: 181 182 render, err := p.getIssues(client, config) 183 if err != nil { 184 log.Error("github getIssues:", err) 185 provider.WriteError(w, "github", err) 186 return 187 } 188 189 provider.WriteJSON(w, render) 190 191 /*case "issuenum_data": 192 193 render, err := t.getIssueNum(client, config) 194 if err != nil { 195 log.Error("github getIssueNum:", err) 196 provider.WriteError(w, "github", err) 197 return 198 } 199 200 provider.WriteJSON(w, render)*/ 201 202 case "owners": 203 204 me, _, err := client.Users.Get("") 205 if err != nil { 206 log.Error("github get user details:", err) 207 provider.WriteError(w, "github", err) 208 return 209 } 210 211 orgs, _, err := client.Organizations.List("", nil) 212 if err != nil { 213 log.Error("github get user's organisations:", err) 214 provider.WriteError(w, "github", err) 215 return 216 } 217 218 owners := make([]githubOwner, 1+len(orgs)) 219 owners[0] = githubOwner{ID: *me.Login, Name: *me.Login} 220 for ko, vo := range orgs { 221 id := 1 + ko 222 owners[id].ID = *vo.Login 223 owners[id].Name = *vo.Login 224 } 225 226 owners = sortOwners(owners) 227 228 provider.WriteJSON(w, owners) 229 230 case "repos": 231 232 var render []githubRepo 233 if config.Owner != "" { 234 235 me, _, err := client.Users.Get("") 236 if err != nil { 237 log.Error("github get user details:", err) 238 provider.WriteError(w, "github", err) 239 return 240 } 241 242 var repos []*gogithub.Repository 243 if config.Owner == *me.Login { 244 repos, _, err = client.Repositories.List(config.Owner, nil) 245 } else { 246 opt := &gogithub.RepositoryListByOrgOptions{ 247 ListOptions: gogithub.ListOptions{PerPage: 100}, 248 } 249 repos, _, err = client.Repositories.ListByOrg(config.Owner, opt) 250 } 251 if err != nil { 252 log.Error("github get user/org repositories:", err) 253 provider.WriteError(w, "github", err) 254 return 255 } 256 for _, vr := range repos { 257 private := "" 258 if *vr.Private { 259 private = " (private)" 260 } 261 render = append(render, 262 githubRepo{ 263 Name: config.Owner + "/" + *vr.Name + private, 264 ID: fmt.Sprintf("%s:%s", config.Owner, *vr.Name), 265 Owner: config.Owner, 266 Repo: *vr.Name, 267 Private: *vr.Private, 268 URL: *vr.HTMLURL, 269 }) 270 } 271 } 272 render = sortRepos(render) 273 274 provider.WriteJSON(w, render) 275 276 case "branches": 277 278 if config.Owner == "" || config.Repo == "" { 279 provider.WriteJSON(w, []githubBranch{}) // we have nothing to return 280 return 281 } 282 branches, _, err := client.Repositories.ListBranches(config.Owner, config.Repo, 283 &gogithub.ListOptions{PerPage: 100}) 284 if err != nil { 285 log.Error("github get branch details:", err) 286 provider.WriteError(w, "github", err) 287 return 288 } 289 render := make([]githubBranch, len(branches)) 290 for kc, vb := range branches { 291 render[kc] = githubBranch{ 292 Name: *vb.Name, 293 ID: fmt.Sprintf("%s:%s:%s", config.Owner, config.Repo, *vb.Name), 294 Included: false, 295 URL: "https://github.com/" + config.Owner + "/" + config.Repo + "/tree/" + *vb.Name, 296 } 297 } 298 299 provider.WriteJSON(w, render) 300 301 case "labels": 302 303 if config.Owner == "" || config.Repo == "" { 304 provider.WriteJSON(w, []githubBranch{}) // we have nothing to return 305 return 306 } 307 labels, _, err := client.Issues.ListLabels(config.Owner, config.Repo, 308 &gogithub.ListOptions{PerPage: 100}) 309 if err != nil { 310 log.Error("github get labels:", err) 311 provider.WriteError(w, "github", err) 312 return 313 } 314 render := make([]githubBranch, len(labels)) 315 for kc, vb := range labels { 316 render[kc] = githubBranch{ 317 Name: *vb.Name, 318 ID: fmt.Sprintf("%s:%s:%s", config.Owner, config.Repo, *vb.Name), 319 Included: false, 320 Color: *vb.Color, 321 } 322 } 323 324 provider.WriteJSON(w, render) 325 326 default: 327 328 log.ErrorString("Github connector unknown method: " + method) 329 provider.WriteEmpty(w) 330 } 331 } 332 333 func (*Provider) githubClient(config githubConfig) *gogithub.Client { 334 ts := oauth2.StaticTokenSource( 335 &oauth2.Token{AccessToken: config.Token}, 336 ) 337 tc := oauth2.NewClient(oauth2.NoContext, ts) 338 339 return gogithub.NewClient(tc) 340 } 341 342 /* 343 func (*Provider) getIssueNum(client *gogithub.Client, config githubConfig) ([]githubIssueActivity, error) { 344 345 ret := []githubIssueActivity{} 346 347 issue, _, err := client.Issues.Get(config.Owner, config.Repo, config.IssueNum) 348 349 if err == nil { 350 n := "" 351 a := "" 352 p := issue.User 353 if p != nil { 354 if p.Login != nil { 355 n = *p.Login 356 } 357 if p.AvatarURL != nil { 358 a = *p.AvatarURL 359 } 360 } 361 ret = append(ret, githubIssueActivity{ 362 Name: n, 363 Event: "created", 364 Message: template.HTML(*issue.Title), 365 Date: "on " + issue.UpdatedAt.Format("January 2 2006, 15:04"), 366 Avatar: a, 367 URL: template.URL(*issue.HTMLURL), 368 }) 369 ret = append(ret, githubIssueActivity{ 370 Name: n, 371 Event: "described", 372 Message: template.HTML(*issue.Body), 373 Date: "on " + issue.UpdatedAt.Format("January 2 2006, 15:04"), 374 Avatar: a, 375 URL: template.URL(*issue.HTMLURL), 376 }) 377 ret = append(ret, githubIssueActivity{ 378 Name: "", 379 Event: "Note", 380 Message: template.HTML("the issue timeline below is in reverse order"), 381 Date: "", 382 Avatar: githubGravatar, 383 URL: template.URL(*issue.HTMLURL), 384 }) 385 } else { 386 return ret, err 387 } 388 389 opts := &gogithub.ListOptions{PerPage: config.BranchLines} 390 391 guff, _, err := client.Issues.ListIssueTimeline(config.Owner, config.Repo, config.IssueNum, opts) 392 393 if err != nil { 394 return ret, err 395 } 396 397 for _, v := range guff { 398 if config.SincePtr == nil || v.CreatedAt.After(*config.SincePtr) { 399 var n, a, m, u string 400 401 p := v.Actor 402 if p != nil { 403 if p.Name != nil { 404 n = *p.Name 405 } 406 if p.AvatarURL != nil { 407 a = *p.AvatarURL 408 } 409 } 410 411 u = fmt.Sprintf("https://github.com/%s/%s/issues/%d#event-%d", 412 config.Owner, config.Repo, config.IssueNum, *v.ID) 413 414 switch *v.Event { 415 case "commented": 416 ic, _, err := client.Issues.GetComment(config.Owner, config.Repo, *v.ID) 417 if err != nil { 418 log.ErrorString("github error fetching issue event comment: " + err.Error()) 419 } else { 420 m = *ic.Body 421 u = *ic.HTMLURL 422 p := ic.User 423 if p != nil { 424 if p.Login != nil { 425 n = *p.Login 426 } 427 if p.AvatarURL != nil { 428 a = *p.AvatarURL 429 } 430 } 431 } 432 } 433 434 ret = append(ret, githubIssueActivity{ 435 Name: n, 436 Event: *v.Event, 437 Message: template.HTML(m), 438 Date: "on " + v.CreatedAt.Format("January 2 2006, 15:04"), 439 Avatar: a, 440 URL: template.URL(u), 441 }) 442 } 443 } 444 445 return ret, nil 446 447 } 448 */ 449 450 func wrapLabels(labels []gogithub.Label) string { 451 l := "" 452 for _, ll := range labels { 453 l += `<span class="github-issue-label" style="background-color:#` + *ll.Color + `">` + *ll.Name + `</span> ` 454 } 455 return l 456 } 457 458 func (*Provider) getIssues(client *gogithub.Client, config githubConfig) ([]githubIssue, error) { 459 460 ret := []githubIssue{} 461 462 isRequired := make([]int, 0, 10) 463 for _, s := range strings.Split(strings.Replace(config.IssuesText, "#", "", -1), ",") { 464 i, err := strconv.Atoi(strings.TrimSpace(s)) 465 if err == nil { 466 isRequired = append(isRequired, i) 467 } 468 } 469 if len(isRequired) > 0 { 470 471 for _, i := range isRequired { 472 473 issue, _, err := client.Issues.Get(config.Owner, config.Repo, i) 474 475 if err == nil { 476 n := "" 477 p := issue.User 478 if p != nil { 479 if p.Login != nil { 480 n = *p.Login 481 } 482 } 483 l := wrapLabels(issue.Labels) 484 ret = append(ret, githubIssue{ 485 Name: n, 486 Message: *issue.Title, 487 Date: issue.CreatedAt.Format("January 2 2006, 15:04"), 488 Updated: issue.UpdatedAt.Format("January 2 2006, 15:04"), 489 URL: template.URL(*issue.HTMLURL), 490 Labels: template.HTML(l), 491 ID: *issue.Number, 492 IsOpen: *issue.State == "open", 493 }) 494 } 495 } 496 497 } else { 498 499 opts := &gogithub.IssueListByRepoOptions{ 500 Sort: "updated", 501 State: config.IssueState.ID, 502 ListOptions: gogithub.ListOptions{PerPage: config.BranchLines}} 503 504 if config.SincePtr != nil { 505 opts.Since = *config.SincePtr 506 } 507 508 for _, lab := range config.Lists { 509 if lab.Included { 510 opts.Labels = append(opts.Labels, lab.Name) 511 } 512 } 513 514 guff, _, err := client.Issues.ListByRepo(config.Owner, config.Repo, opts) 515 516 if err != nil { 517 return ret, err 518 } 519 520 for _, v := range guff { 521 n := "" 522 ptr := v.User 523 if ptr != nil { 524 if ptr.Login != nil { 525 n = *ptr.Login 526 } 527 } 528 l := wrapLabels(v.Labels) 529 ret = append(ret, githubIssue{ 530 Name: n, 531 Message: *v.Title, 532 Date: v.CreatedAt.Format("January 2 2006, 15:04"), 533 Updated: v.UpdatedAt.Format("January 2 2006, 15:04"), 534 URL: template.URL(*v.HTMLURL), 535 Labels: template.HTML(l), 536 ID: *v.Number, 537 IsOpen: *v.State == "open", 538 }) 539 } 540 } 541 542 return ret, nil 543 544 } 545 546 func (*Provider) getCommits(client *gogithub.Client, config githubConfig) ([]githubBranchCommits, error) { 547 548 opts := &gogithub.CommitsListOptions{ 549 SHA: config.Branch, 550 ListOptions: gogithub.ListOptions{PerPage: config.BranchLines}} 551 552 if config.SincePtr != nil { 553 opts.Since = *config.SincePtr 554 } 555 556 guff, _, err := client.Repositories.ListCommits(config.Owner, config.Repo, opts) 557 558 if err != nil { 559 return nil, err 560 } 561 562 if len(guff) == 0 { 563 return []githubBranchCommits{}, nil 564 } 565 566 day := "" 567 newDay := "" 568 ret := []githubBranchCommits{} 569 570 for k, v := range guff { 571 572 if guff[k].Commit != nil { 573 if guff[k].Commit.Committer.Date != nil { 574 y, m, d := (*guff[k].Commit.Committer.Date).Date() 575 newDay = fmt.Sprintf("%s %d, %d", m.String(), d, y) 576 } 577 } 578 if day != newDay { 579 day = newDay 580 ret = append(ret, githubBranchCommits{ 581 Name: fmt.Sprintf("%s/%s:%s", config.Owner, config.Repo, config.Branch), 582 Day: day, 583 }) 584 } 585 586 var a, d, l, m, u string 587 if v.Commit != nil { 588 if v.Commit.Committer.Date != nil { 589 // d = fmt.Sprintf("%v", *v.Commit.Committer.Date) 590 d = v.Commit.Committer.Date.Format("January 2 2006, 15:04") 591 } 592 if v.Commit.Message != nil { 593 m = *v.Commit.Message 594 } 595 } 596 if v.Committer != nil { 597 if v.Committer.Login != nil { 598 l = *v.Committer.Login 599 } 600 if v.Committer.AvatarURL != nil { 601 a = *v.Committer.AvatarURL 602 } 603 } 604 if a == "" { 605 a = githubGravatar 606 } 607 if v.HTMLURL != nil { 608 u = *v.HTMLURL 609 } 610 ret[len(ret)-1].Commits = append(ret[len(ret)-1].Commits, githubCommit{ 611 Name: l, 612 Message: m, 613 Date: d, 614 Avatar: a, 615 URL: template.URL(u), 616 }) 617 } 618 619 return ret, nil 620 621 } 622 623 // Refresh ... gets the latest version 624 func (p *Provider) Refresh(ctx *provider.Context, configJSON, data string) string { 625 var c = githubConfig{} 626 627 err := json.Unmarshal([]byte(configJSON), &c) 628 629 if err != nil { 630 log.Error("unable to unmarshall github config", err) 631 return "internal configuration error '" + err.Error() + "'" 632 } 633 634 c.Clean() 635 c.Token = ctx.GetSecrets("token") 636 637 switch c.ReportInfo.ID { 638 /*case "issuenum_data": 639 refreshed, err := t.getIssueNum(t.githubClient(c), c) 640 if err != nil { 641 log.Error("unable to get github issue number activity", err) 642 return data 643 } 644 j, err := json.Marshal(refreshed) 645 if err != nil { 646 log.Error("unable to marshall github issue number activity", err) 647 return data 648 } 649 return string(j)*/ 650 651 case tagIssuesData: 652 refreshed, err := p.getIssues(p.githubClient(c), c) 653 if err != nil { 654 log.Error("unable to get github issues", err) 655 return data 656 } 657 j, err := json.Marshal(refreshed) 658 if err != nil { 659 log.Error("unable to marshall github issues", err) 660 return data 661 } 662 return string(j) 663 664 case tagCommitsData: 665 refreshed, err := p.getCommits(p.githubClient(c), c) 666 if err != nil { 667 log.Error("unable to get github commits", err) 668 return data 669 } 670 j, err := json.Marshal(refreshed) 671 if err != nil { 672 log.Error("unable to marshall github commits", err) 673 return data 674 } 675 return string(j) 676 677 default: 678 msg := "unknown data format: " + c.ReportInfo.ID 679 log.ErrorString(msg) 680 return "internal configuration error, " + msg 681 } 682 683 } 684 685 // Render ... just returns the data given, suitably formatted 686 func (p *Provider) Render(ctx *provider.Context, config, data string) string { 687 var err error 688 689 payload := githubRender{} 690 var c = githubConfig{} 691 692 err = json.Unmarshal([]byte(config), &c) 693 694 if err != nil { 695 log.Error("unable to unmarshall github config", err) 696 return "Please delete and recreate this Github section." 697 } 698 699 c.Clean() 700 c.Token = ctx.GetSecrets("token") 701 702 payload.Config = c 703 payload.Repo = c.RepoInfo 704 payload.Limit = c.BranchLines 705 if len(c.BranchSince) > 0 { 706 payload.DateMessage = "created after " + c.BranchSince 707 } 708 709 switch c.ReportInfo.ID { 710 /* case "issuenum_data": 711 payload.IssueNum = c.IssueNum 712 raw := []githubIssueActivity{} 713 714 if len(data) > 0 { 715 err = json.Unmarshal([]byte(data), &raw) 716 if err != nil { 717 log.Error("unable to unmarshall github issue activity data", err) 718 return "Documize internal github json umarshall issue activity data error: " + err.Error() 719 } 720 } 721 722 opt := &gogithub.MarkdownOptions{Mode: "gfm", Context: c.Owner + "/" + c.Repo} 723 client := p.githubClient(c) 724 for k, v := range raw { 725 if v.Event == "commented" { 726 output, _, err := client.Markdown(string(v.Message), opt) 727 if err != nil { 728 log.Error("convert commented text to markdown", err) 729 } else { 730 raw[k].Message = template.HTML(output) 731 } 732 } 733 } 734 payload.IssueNumActivity = raw */ 735 736 case tagIssuesData: 737 raw := []githubIssue{} 738 739 if len(data) > 0 { 740 err = json.Unmarshal([]byte(data), &raw) 741 if err != nil { 742 log.Error("unable to unmarshall github issue data", err) 743 return "Documize internal github json umarshall open data error: " + err.Error() + "<BR>" + data 744 } 745 } 746 payload.Issues = raw 747 if strings.TrimSpace(c.IssuesText) != "" { 748 payload.ShowIssueNumbers = true 749 payload.DateMessage = c.IssuesText 750 } else { 751 if len(c.Lists) > 0 { 752 for _, v := range c.Lists { 753 if v.Included { 754 payload.ShowList = true 755 break 756 } 757 } 758 payload.List = c.Lists 759 } 760 } 761 762 case tagCommitsData: 763 raw := []githubBranchCommits{} 764 err = json.Unmarshal([]byte(data), &raw) 765 766 if err != nil { 767 log.Error("unable to unmarshall github commit data", err) 768 return "Documize internal github json umarshall data error: " + err.Error() + "<BR>" + data 769 } 770 c.ReportInfo.ID = tagCommitsData 771 payload.BranchCommits = raw 772 for _, list := range raw { 773 payload.CommitCount += len(list.Commits) 774 } 775 776 default: 777 msg := "unknown data format: " + c.ReportInfo.ID 778 log.ErrorString(msg) 779 return "internal configuration error, " + msg 780 781 } 782 783 t := template.New("github") 784 785 tmpl, ok := renderTemplates[c.ReportInfo.ID] 786 if !ok { 787 msg := "github render template not found for: " + c.ReportInfo.ID 788 log.ErrorString(msg) 789 return "Documize internal error: " + msg 790 } 791 792 t, err = t.Parse(tmpl) 793 794 if err != nil { 795 log.Error("github render template.Parse error:", err) 796 return "Documize internal github template.Parse error: " + err.Error() 797 } 798 799 buffer := new(bytes.Buffer) 800 err = t.Execute(buffer, payload) 801 if err != nil { 802 log.Error("github render template.Execute error:", err) 803 return "Documize internal github template.Execute error: " + err.Error() 804 } 805 806 return buffer.String() 807 } 808 809 // Callback is called by a browser redirect from Github, via the validation endpoint 810 func Callback(res http.ResponseWriter, req *http.Request) error { 811 812 code := req.URL.Query().Get("code") 813 state := req.URL.Query().Get("state") 814 815 ghurl := "https://github.com/login/oauth/access_token" 816 vals := "client_id=" + clientID() 817 vals += "&client_secret=" + clientSecret() 818 vals += "&code=" + code 819 vals += "&state=" + state 820 821 req2, err := http.NewRequest("POST", ghurl+"?"+vals, strings.NewReader(vals)) 822 if err != nil { 823 return err 824 } 825 826 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 827 req2.Header.Set("Accept", "application/json") 828 829 res2, err := http.DefaultClient.Do(req2) 830 if err != nil { 831 return err 832 } 833 834 var gt githubCallbackT 835 836 err = json.NewDecoder(res2.Body).Decode(>) 837 if err != nil { 838 return err 839 } 840 841 err = res2.Body.Close() 842 if err != nil { 843 return err 844 } 845 846 returl, err := url.QueryUnescape(state) 847 if err != nil { 848 return err 849 } 850 851 up, err := url.Parse(returl) 852 if err != nil { 853 return err 854 } 855 856 target := up.Scheme + "://" + up.Host + up.Path + "?code=" + gt.AccessToken 857 858 http.Redirect(res, req, target, http.StatusTemporaryRedirect) 859 860 return nil 861 }