Skip to content

Commit

Permalink
Add basic support for installing Go bridges
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Jul 29, 2023
1 parent 7ffce6a commit 003b27a
Show file tree
Hide file tree
Showing 8 changed files with 480 additions and 26 deletions.
190 changes: 190 additions & 0 deletions api/gitlab/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package gitlab

import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"

"github.com/fatih/color"
"github.com/schollz/progressbar/v3"
"github.com/tidwall/gjson"

"github.com/beeper/bridge-manager/cli/hyper"
"github.com/beeper/bridge-manager/log"
)

// language=graphql
const getLastSuccessfulJobQuery = `
query($repo: ID!, $ref: String!, $job: String!) {
project(fullPath: $repo) {
pipelines(status: SUCCESS, ref: $ref, first: 1) {
nodes {
sha
job(name: $job) {
webPath
}
}
}
}
}
`

type lastSuccessfulJobQueryVariables struct {
Repo string `json:"repo"`
Ref string `json:"ref"`
Job string `json:"job"`
}

type LastBuild struct {
Commit string
JobURL string
}

func GetLastBuild(domain, repo, mainBranch, job string) (*LastBuild, error) {
resp, err := graphqlQuery(domain, getLastSuccessfulJobQuery, lastSuccessfulJobQueryVariables{
Repo: repo,
Ref: mainBranch,
Job: job,
})
if err != nil {
return nil, err
}
res := gjson.GetBytes(resp, "project.pipelines.nodes.0")
if !res.Exists() {
return nil, fmt.Errorf("didn't get pipeline info in response")
}
return &LastBuild{
Commit: gjson.Get(res.Raw, "sha").Str,
JobURL: gjson.Get(res.Raw, "job.webPath").Str,
}, nil
}

func getRefFromBridge(bridge string) (string, error) {
switch bridge {
case "imessage", "whatsapp":
return "master", nil
case "discord", "slack", "gmessages":
return "main", nil
default:
return "", fmt.Errorf("unknown bridge %s", bridge)
}
}

func getJobFromBridge(bridge string) (string, error) {
switch bridge {
case "imessage":
if runtime.GOOS != "darwin" {
return "", fmt.Errorf("mautrix-imessage can only run on Macs")
}
return "build universal", nil
default:
osAndArch := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
switch osAndArch {
case "linux/amd64":
return "build amd64", nil
case "linux/arm64":
return "build arm64", nil
case "linux/arm":
if bridge == "signal" {
return "", fmt.Errorf("mautrix-signal does not support 32-bit arm")
}
return "build arm", nil
default:
return "", fmt.Errorf("binaries for %s are not yet built in the CI", osAndArch)
}
}
}

func linkifyCommit(repo, commit string) string {
return hyper.Link(commit[:8], fmt.Sprintf("https://github.com/%s/commit/%s", repo, commit), false)
}

func linkifyDiff(repo, fromCommit, toCommit string) string {
formattedDiff := fmt.Sprintf("%s...%s", fromCommit[:8], toCommit[:8])
return hyper.Link(formattedDiff, fmt.Sprintf("https://github.com/%s/compare/%s...%s", repo, fromCommit, toCommit), false)
}

func DownloadMautrixBridgeBinary(ctx context.Context, bridge, path string, noUpdate bool, branchOverride, currentCommit string) error {
domain := "mau.dev"
repo := fmt.Sprintf("mautrix/%s", bridge)
fileName := fmt.Sprintf("mautrix-%s", bridge)
ref, err := getRefFromBridge(bridge)
if err != nil {
return err
}
if branchOverride != "" {
ref = branchOverride
}
job, err := getJobFromBridge(bridge)
if err != nil {
return err
}

if currentCommit == "" {
log.Printf("Finding latest version of [cyan]%s[reset] from [cyan]%s[reset]", fileName, domain)
} else {
log.Printf("Checking for updates to [cyan]%s[reset] from [cyan]%s[reset]", fileName, domain)
}
build, err := GetLastBuild(domain, repo, ref, job)
if err != nil {
return fmt.Errorf("failed to get last build info: %w", err)
}
if build.Commit == currentCommit {
log.Printf("[cyan]%s[reset] is up to date (commit: %s)", fileName, linkifyCommit(repo, currentCommit))
return nil
} else if currentCommit != "" && noUpdate {
log.Printf("[cyan]%s[reset] [yellow]is out of date, latest commit is %s (diff: %s)[reset]", fileName, linkifyCommit(repo, build.Commit), linkifyDiff(repo, currentCommit, build.Commit))
return nil
}
if currentCommit == "" {
log.Printf("Installing [cyan]%s[reset] (commit: %s)", fileName, linkifyCommit(repo, build.Commit))
} else {
log.Printf("Updating [cyan]%s[reset] (diff: %s)", fileName, linkifyDiff(repo, currentCommit, build.Commit))
}
file, err := os.CreateTemp("", "bbctl-"+fileName+"-*")
if err != nil {
return fmt.Errorf("failed to open temp file: %w", err)
}
defer func() {
_ = file.Close()
_ = os.Remove(file.Name())
}()
artifactURL := (&url.URL{
Scheme: "https",
Host: domain,
Path: filepath.Join(build.JobURL, "artifacts", "raw", fileName),
}).String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, artifactURL, nil)
if err != nil {
return fmt.Errorf("failed to prepare download request: %w", err)
}
resp, err := cli.Do(req)
if err != nil {
return fmt.Errorf("failed to download artifact: %w", err)
}
defer resp.Body.Close()
bar := progressbar.DefaultBytes(
resp.ContentLength,
fmt.Sprintf("Downloading %s", color.CyanString(fileName)),
)
_, err = io.Copy(io.MultiWriter(file, bar), resp.Body)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
_ = file.Close()
err = os.Rename(file.Name(), path)
if err != nil {
return fmt.Errorf("failed to move temp file: %w", err)
}
err = os.Chmod(path, 0755)
if err != nil {
return fmt.Errorf("failed to chmod binary: %w", err)
}
log.Printf("Successfully installed [cyan]%s[reset] commit %s", fileName, linkifyCommit(domain, build.Commit))
return nil
}
87 changes: 87 additions & 0 deletions api/gitlab/graphql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package gitlab

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"

"maunium.net/go/mautrix"
)

var cli = &http.Client{Timeout: 30 * time.Second}

type queryRequestBody struct {
Query string `json:"query"`
Variables any `json:"variables"`
}

type QueryErrorLocation struct {
Line int `json:"line"`
Column int `json:"column"`
}

type QueryErrorItem struct {
Message string `json:"message"`
Locations []QueryErrorLocation `json:"locations"`
}

type QueryError []QueryErrorItem

func (qe QueryError) Error() string {
if len(qe) == 1 {
return qe[0].Message
}
plural := "s"
if len(qe) == 2 {
plural = ""
}
return fmt.Sprintf("%s (and %d other error%s)", qe[0].Message, len(qe)-1, plural)
}

type queryResponse struct {
Data json.RawMessage `json:"data"`
Errors QueryError `json:"errors"`
}

func graphqlQuery(domain, query string, args any) (json.RawMessage, error) {
req := &http.Request{
URL: &url.URL{
Scheme: "https",
Host: domain,
Path: "/api/graphql",
},
Method: http.MethodPost,
Header: http.Header{
"User-Agent": {mautrix.DefaultUserAgent},
"Content-Type": {"application/json"},
"Accept": {"application/json"},
},
}
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(queryRequestBody{
Query: query,
Variables: args,
})
if err != nil {
return nil, fmt.Errorf("failed to encode request body: %w", err)
}
req.Body = io.NopCloser(&buf)
resp, err := cli.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
var respData queryResponse
err = json.NewDecoder(resp.Body).Decode(&respData)
if err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
}
if len(respData.Errors) > 0 {
return nil, respData.Errors
}
return respData.Data, nil
}
1 change: 1 addition & 0 deletions cmd/bbctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ var app = &cli.App{
deleteCommand,
whoamiCommand,
configCommand,
runCommand,
},
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/bbctl/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func doRegisterBridge(ctx *cli.Context, bridge, bridgeType string, onlyGet bool)
}
SaveHungryURL(ctx, whoami.UserInfo.HungryURL)
bridgeInfo, ok := whoami.User.Bridges[bridge]
if !bridgeInfo.BridgeState.IsSelfHosted {
if ok && !bridgeInfo.BridgeState.IsSelfHosted {
return nil, UserError{fmt.Sprintf("Your %s bridge is not self-hosted.", color.CyanString(bridge))}
}
if ok && !onlyGet && ctx.Command.Name == "register" {
Expand Down
Loading

0 comments on commit 003b27a

Please sign in to comment.