diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc09a4e..4dea9db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,26 @@ -name: CI +name: Test + on: push: - branches: [ master ] pull_request: - branches: [ master ] + workflow_dispatch: + jobs: test: name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: + max-parallel: 1 fail-fast: false matrix: os: [ubuntu-18.04, ubuntu-20.04, ubuntu-22.04] steps: - name: Set up Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v3 with: go-version: 1.13 - name: Check out code into the Go module directory - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Get dependencies run: go get -v -t -d ./... - name: Run Installer @@ -27,3 +29,5 @@ jobs: sudo ./startup - name: Run Tests run: go test -v startup_test.go + env: + DISCORD_API_KEY: ${{ secrets.SINUSBOT_DISCORD_API_KEY }} diff --git a/README.md b/README.md index 0786834..4663d1c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # SinusBot Installer for Linux -![Build Status](https://github.com/sinusbot/installer-linux/workflows/CI/badge.svg) +![Build Status](https://github.com/sinusbot/installer-linux/workflows/ci.yml/badge.svg) ## Officially supported Linux distributions -- Debian 9+ -- Ubuntu 16.10+ +- Debian 10+ +- Ubuntu 18.04+ - CentOS 7+ ## Features @@ -20,7 +20,7 @@ The following tasks will be done: - Checks if the linux distribution is supported -- Installs the latest supported version of the teamspeak client +- Installs the latest supported version of the TeamSpeak client - Installs all the necessary dependencies - Creates a separated user - Installs the latest SinusBot version diff --git a/sinusbot_installer.sh b/sinusbot_installer.sh index a454b70..c281af9 100644 --- a/sinusbot_installer.sh +++ b/sinusbot_installer.sh @@ -4,7 +4,7 @@ # Vars MACHINE=$(uname -m) -Instversion="1.5" +Instversion="2.0" USE_SYSTEMD=true @@ -538,19 +538,6 @@ if [ "$INSTALL" == "Rem" ]; then exit 0 fi -# Private usage only! - -redMessage "This SinusBot version is only for private use! Accept?" - -OPTIONS=("No" "Yes") -select OPTION in "${OPTIONS[@]}"; do - case "$REPLY" in - 1) errorQuit ;; - 2) break ;; - *) errorContinue ;; - esac -done - # Ask for YT-DL redMessage "Should YT-DL be installed/updated?" @@ -621,28 +608,27 @@ fi magentaMessage "Installing necessary packages. Please wait..." if [[ -f /etc/centos-release ]]; then - yum -y -q install screen xvfb libxcursor1 ca-certificates bzip2 psmisc libglib2.0-0 less ntp python iproute which dbus libnss3 libegl1-mesa x11-xkb-utils libasound2 libxcomposite-dev libxi6 libpci3 libxslt1.1 libxkbcommon0 libxss1 >/dev/null + yum -y -q install screen xvfb libxcursor1 ca-certificates bzip2 psmisc libglib2.0-0 less ntp python3 iproute which dbus libnss3 libegl1-mesa x11-xkb-utils libasound2 libxcomposite-dev libxi6 libpci3 libxslt1.1 libxkbcommon0 libxss1 >/dev/null update-ca-trust extract >/dev/null else - # Detect if systemctl is available then use systemd as start script. Otherwise use init.d - if [ "$OSRELEASE" == "18.04" ] && [ "$OS" == "ubuntu" ]; then - apt-get -y install chrony - else - apt-get -y install ntp - fi - apt-get install -y -qq --no-install-recommends libfontconfig libxtst6 screen xvfb libxcursor1 ca-certificates bzip2 psmisc libglib2.0-0 less python iproute2 dbus libnss3 libegl1-mesa x11-xkb-utils libasound2 libxcomposite-dev libxi6 libpci3 libxslt1.1 libxkbcommon0 libxss1 + apt-get install -y -qq --no-install-recommends libfontconfig libxtst6 screen xvfb libxcursor1 ca-certificates bzip2 psmisc libglib2.0-0 less python3 iproute2 dbus libnss3 libegl1-mesa x11-xkb-utils libasound2 libxcomposite-dev libxi6 libpci3 libxslt1.1 libxkbcommon0 libxss1 libxdamage1 update-ca-certificates >/dev/null fi - +if [[ $(cat /etc/*release | grep "PRETTY_NAME=" | sed 's/PRETTY_NAME=//g') =~ "Debian" ]]; then + apt-get install -y -qq --no-install-recommends python-is-python3 +fi +if [[ $(cat /etc/*release | grep "PRETTY_NAME=" | sed 's/PRETTY_NAME=//g') =~ "Ubuntu" ]]; then + apt-get install -y -qq --no-install-recommends python-is-python3 +fi else magentaMessage "Installing necessary packages. Please wait..." if [[ -f /etc/centos-release ]]; then - yum -y -q install ca-certificates bzip2 python wget >/dev/null + yum -y -q install ca-certificates bzip2 python3 wget >/dev/null update-ca-trust extract >/dev/null else - apt-get -qq install ca-certificates bzip2 python wget -y >/dev/null + apt-get -qq install ca-certificates bzip2 python3 wget -y >/dev/null update-ca-certificates >/dev/null fi @@ -650,47 +636,6 @@ fi greenMessage "Packages installed"! -# Setting server time - -if [[ $VIRTUALIZATION_TYPE == "openvz" ]]; then - redMessage "You're using OpenVZ virtualization. You can't set your time, maybe it works but there is no guarantee. Skipping this part..." -else - if [[ -f /etc/centos-release ]] || [ $(cat /etc/*release | grep "DISTRIB_ID=" | sed 's/DISTRIB_ID=//g') ]; then - if [ "$OSRELEASE" == "18.04" ] && [ "$OS" == "ubuntu" ]; then - systemctl start chronyd - if [[ $(chronyc -a 'burst 4/4') == "200 OK" ]]; then - TIME=$(date) - else - errorExit "Error while setting time via chrony"! - fi - else - if [[ -f /etc/centos-release ]]; then - service ntpd stop - else - service ntp stop - fi - ntpd -s 0.pool.ntp.org - if [[ -f /etc/centos-release ]]; then - service ntpd start - else - service ntp start - fi - TIME=$(date) - fi - greenMessage "Automatically set time to" $TIME! - else - if [[ $(command -v timedatectl) != "" ]]; then - service ntp restart - timedatectl set-ntp yes - timedatectl - TIME=$(date) - greenMessage "Automatically set time to" $TIME! - else - redMessage "Unable to configure your date automatically, the installation will still be attempted." - fi - fi -fi - USERADD=$(which useradd) GROUPADD=$(which groupadd) ipaddress=$(ip route get 8.8.8.8 | awk {'print $7'} | tr -d '\n') diff --git a/startup.go b/startup.go index a1f4946..f652d90 100644 --- a/startup.go +++ b/startup.go @@ -43,18 +43,10 @@ func main() { Value: "1", Detect: "Should I install TeamSpeak or only Discord Mode?", }, - parameter{ - Value: "2", - Detect: "This SinusBot version is only for private use! Accept?", - }, parameter{ Value: "1", Detect: "Should YT-DL be installed/updated?", }, - parameter{ - Value: "1", - Detect: "Check your time below:", - }, parameter{ Value: "2", Detect: "Update the system packages to the latest version?", diff --git a/startup_test.go b/startup_test.go index 9ebef2e..6e4fed0 100644 --- a/startup_test.go +++ b/startup_test.go @@ -4,28 +4,126 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/multiplay/go-ts3" - "github.com/pkg/errors" "io/ioutil" "net/http" + "os" "strings" "testing" "time" + + "github.com/multiplay/go-ts3" + "github.com/pkg/errors" ) -const teamspeakCheckNickname = "SinusBot via GitHub Actions" +/* Constants and structs */ -type instance struct { +const checkNickname = "SinusBot via GitHub Actions" + +type SinusBotInstance struct { UUID string `json:"uuid"` } +func getDiscordToken() string { + return os.Getenv("DISCORD_API_KEY") +} + +type DiscordInstanceResponse struct { + Success bool `json:"success"` + UUID string `json:"uuid"` +} + +type DefaultBotIdResponse struct { + DefaultBotID string `json:"defaultBotId"` +} + +type LoginResponse struct { + Token string `json:"token"` +} + +type UserNotConnectedError struct { + Message string `json:"message"` + Code int `json:"code"` +} + +/* Automated test functions */ + func TestIsBotRunning(t *testing.T) { if _, err := getBotID(); err != nil { t.Fatalf("could not get botId: %v", err) } } -func TestConnectToTeamspeak(t *testing.T) { +func TestDiscord(t *testing.T) { + botId, err := getBotID() + if err != nil { + t.Fatalf("could not get botId: %v", err) + } + pw, err := ioutil.ReadFile(".password") + if err != nil { + t.Fatalf("could not read password file") + } + discordApiToken := getDiscordToken() + if discordApiToken == "" { + t.Fatalf("could not read discord token env") + } + token, err := login("admin", string(pw), *botId) + if err != nil { + t.Fatalf("could not get token: %v", err) + } + uuid, err := createDiscordInstance(discordApiToken, *token) + if err != nil { + t.Fatalf("could not create instance: %v", err) + } + fmt.Printf("Created instance %v\n", *uuid) + + if err := updateInstance(*uuid, *token, true, "152947849393471488/454634325556854796"); err != nil { + t.Fatalf("could not change instance settings: %v", err) + } + + if err := spawnInstance(*uuid, *token); err != nil { + t.Fatalf("could not spawn discord instance: %v", err) + } + /* Workaround for SinusBot bug */ + if err := updateInstance(*uuid, *token, true, "152947849393471488/452453323891671041"); err != nil { + t.Fatalf("could not change instance settings: %v", err) + } + + if err := killInstance(*uuid, *token); err != nil { + t.Fatalf("could not kill discord instance: %v", err) + } + + if err := spawnInstance(*uuid, *token); err != nil { + t.Fatalf("could not spawn discord instance: %v", err) + } + + time.Sleep(5 * time.Second) + /* End workaround */ + + var to string + to = "454634325556854796" + success, err := moveBot(&discordApiToken, &to) + if err != nil { + t.Fatalf("Something went wrong at discord: %v", err) + } + + if success { + fmt.Printf("Bot is connected\n") + _, err = moveBot(&discordApiToken, nil) + if err != nil { + t.Fatalf("Something went wrong at discord: %v", err) + } + if err := killInstance(*uuid, *token); err != nil { + t.Fatalf("could not kill discord instance: %v", err) + } + } else { + if err := killInstance(*uuid, *token); err != nil { + t.Fatalf("could not kill discord instance: %v", err) + } + fmt.Printf("Bot couldn't be found\n") + } +} + +func TestConnectToTeamSpeak(t *testing.T) { botId, err := getBotID() if err != nil { t.Fatalf("could not get botId: %v", err) @@ -42,14 +140,17 @@ func TestConnectToTeamspeak(t *testing.T) { if err != nil { t.Fatalf("could not get instances: %v", err) } - if err := changeSettings(bots[0].UUID, *token); err != nil { + if err := updateInstance(bots[0].UUID, *token, false, "sinusbot.com"); err != nil { t.Fatalf("could not change instance settings: %v", err) } + if err := spawnInstance(bots[0].UUID, *token); err != nil { + t.Fatalf("could not spawn teamspeak instance: %v", err) + } fmt.Println("Sleeping so that the bot will connect in this time to the server") time.Sleep(5 * time.Second) } -func TestIsBotOnTeamspeak(t *testing.T) { +func TestIsBotOnTeamSpeak(t *testing.T) { c, err := ts3.NewClient("julia.ts3index.com:10011") if err != nil { t.Fatalf("could not create new ts3 client: %v", err) @@ -64,7 +165,7 @@ func TestIsBotOnTeamspeak(t *testing.T) { } found := false for _, client := range clientList { - if strings.Contains(client.Nickname, teamspeakCheckNickname) { + if strings.Contains(client.Nickname, checkNickname) { found = true break } @@ -72,78 +173,114 @@ func TestIsBotOnTeamspeak(t *testing.T) { if !found { t.Fatal("no client found") } + botId, err := getBotID() + if err != nil { + t.Fatalf("could not get botId: %v", err) + } + pw, err := ioutil.ReadFile(".password") + if err != nil { + t.Fatalf("could not read password file") + } + token, err := login("admin", string(pw), *botId) + if err != nil { + t.Fatalf("could not get token: %v", err) + } + bots, err := getInstances(*token) + if err != nil { + t.Fatalf("could not get instances: %v", err) + } + if err := killInstance(bots[0].UUID, *token); err != nil { + t.Fatalf("could not kill teamspeak instance: %v", err) + } } -func getInstances(token string) ([]instance, error) { - req, err := http.NewRequest("GET", "http://127.0.0.1:8087/api/v1/bot/instances", nil) +/* Logical functions used by tests */ + +func createDiscordInstance(discordApiToken string, token string) (*string, error) { + postData, err := json.Marshal(map[string]string{ + "backend": "discord", + "nick": checkNickname, + "token": discordApiToken, + }) + resp, err := executePostRequest("/bot/instances", http.StatusCreated, &token, bytes.NewBuffer(postData)) if err != nil { - return nil, errors.Wrap(err, "could not create request") + return nil, errors.Wrap(err, "could not create instance") } - req.Header.Add("Authorization", "Bearer "+token) - resp, err := http.DefaultClient.Do(req) + var instance DiscordInstanceResponse + if err := json.NewDecoder(resp.Body).Decode(&instance); err != nil { + return nil, errors.Wrap(err, "could not decode data") + } + + return &instance.UUID, nil +} + +func getInstances(token string) ([]SinusBotInstance, error) { + resp, err := executeGetRequest("/bot/instances", &token) if err != nil { - return nil, errors.Wrap(err, "could not do request") + return nil, errors.Wrap(err, "could not get instances") } - var data []instance + var data []SinusBotInstance if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return nil, errors.Wrap(err, "could not decode json") } return data, nil } -func changeSettings(uuid, token string) error { - data, err := json.Marshal(map[string]string{ - "instanceId": uuid, - "nick": teamspeakCheckNickname, - "serverHost": "sinusbot.com", - }) - req, err := http.NewRequest("POST", "http://127.0.0.1:8087/api/v1/bot/i/"+uuid+"/settings", bytes.NewBuffer(data)) - if err != nil { - return errors.Wrap(err, "could not create request") +func updateInstance(uuid string, token string, isDiscord bool, arg string) error { + var data []byte + var jsonErr error + if isDiscord { + data, jsonErr = json.Marshal(map[string]string{ + "instanceId": uuid, + "nick": checkNickname, + "channelName": arg, + }) + } else { + data, jsonErr = json.Marshal(map[string]string{ + "instanceId": uuid, + "nick": checkNickname, + "serverHost": arg, + }) } - req.Header.Add("Authorization", "Bearer "+token) - req.Header.Add("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - return errors.Wrap(err, "could not do request") - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("invalid status code received by setting instance settings: %d", resp.StatusCode) + if jsonErr != nil { + return errors.Wrap(jsonErr, "Could not create json") } - req, err = http.NewRequest("POST", "http://127.0.0.1:8087/api/v1/bot/i/"+uuid+"/spawn", nil) + _, err := executePostRequest("/bot/i/"+uuid+"/settings", http.StatusOK, &token, bytes.NewBuffer(data)) if err != nil { - return errors.Wrap(err, "could not create request") + return errors.Wrap(err, "could not change instance settings") } - req.Header.Add("Authorization", "Bearer "+token) - resp, err = http.DefaultClient.Do(req) + return nil +} + +func killInstance(uuid string, token string) error { + _, err := executePostRequest("/bot/i/"+uuid+"/kill", http.StatusOK, &token, nil) if err != nil { - return errors.Wrap(err, "could not do request") + return errors.Wrap(err, "could not kill instance") } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("invalid status code received by spawning instance settings") + return nil +} + +func spawnInstance(uuid string, token string) error { + _, err := executePostRequest("/bot/i/"+uuid+"/spawn", http.StatusOK, &token, nil) + if err != nil { + return errors.Wrap(err, "could not spawn instance") } return nil } func getBotID() (*string, error) { - resp, err := http.Get("http://127.0.0.1:8087/api/v1/botId") + resp, err := executeGetRequest("/botId", nil) if err != nil { return nil, errors.Wrap(err, "could not get") } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("status is not expected: %d; got: %d", http.StatusOK, resp.StatusCode) - } - var data struct { - DefaultBotID string `json:"defaultBotId"` - } - - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + var dbr DefaultBotIdResponse + if err := json.NewDecoder(resp.Body).Decode(&dbr); err != nil { return nil, errors.Wrap(err, "could not decode data") } - return &data.DefaultBotID, nil + return &dbr.DefaultBotID, nil } -func login(username, password, botId string) (*string, error) { +func login(username string, password string, botId string) (*string, error) { data, err := json.Marshal(map[string]string{ "username": username, "password": password, @@ -152,15 +289,90 @@ func login(username, password, botId string) (*string, error) { if err != nil { return nil, errors.Wrap(err, "could not marshal json") } - resp, err := http.Post("http://127.0.0.1:8087/api/v1/bot/login", "application/json", bytes.NewBuffer(data)) + + resp, err := executePostRequest("/bot/login", http.StatusOK, nil, bytes.NewBuffer(data)) if err != nil { return nil, errors.Wrap(err, "could not post") } - var res struct { - Token string `json:"token"` - } - if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + var lr LoginResponse + if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil { return nil, errors.Wrap(err, "could not decode json") } - return &res.Token, nil + return &lr.Token, nil +} + +func moveBot(discordApiToken *string, toChannel *string) (bool, error) { + data, err := json.Marshal(map[string]*string{ + "channel_id": toChannel, + }) + if err != nil { + return false, errors.Wrap(err, "could not marshal json") + } + req, err := http.NewRequest("PATCH", "https://discord.com/api/v10/guilds/152947849393471488/members/1027147952696873011", bytes.NewBuffer(data)) + if err != nil { + return false, errors.Wrap(err, "could not create request") + } + req.Header.Add("Authorization", "Bot "+*discordApiToken) + req.Header.Add("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, errors.Wrap(err, "could not do request") + } + if resp.StatusCode == http.StatusOK { + return true, nil + } + var errorResponse UserNotConnectedError + if err := json.NewDecoder(resp.Body).Decode(&errorResponse); err != nil { + return false, errors.Wrap(err, "could not decode data") + } + if errorResponse.Code == 40032 { + return false, nil // This is a known bug in sinusbot rn + } else { + return false, fmt.Errorf("Unknown error occured: %d (%v)", errorResponse.Code, errorResponse.Message) + } +} + +/* SinusBot api wrapper */ + +func executePostRequest(endpoint string, expectedStatusCode int, token *string, data *bytes.Buffer) (*http.Response, error) { + var req *http.Request + var err error + if data != nil { + req, err = http.NewRequest("POST", "http://127.0.0.1:8087/api/v1"+endpoint, data) + } else { + req, err = http.NewRequest("POST", "http://127.0.0.1:8087/api/v1"+endpoint, nil) + } + if err != nil { + return nil, errors.Wrap(err, "could not create request") + } + if token != nil { + req.Header.Add("Authorization", "Bearer "+*token) + } + req.Header.Add("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "could not do request") + } + if resp.StatusCode != expectedStatusCode { + return nil, fmt.Errorf("invalid status code received while executing call") + } + return resp, nil +} + +func executeGetRequest(endpoint string, token *string) (*http.Response, error) { + req, err := http.NewRequest("GET", "http://127.0.0.1:8087/api/v1"+endpoint, nil) + if err != nil { + return nil, errors.Wrap(err, "could not create request") + } + if token != nil { + req.Header.Add("Authorization", "Bearer "+*token) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "could not do request") + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("invalid status code received while executing call") + } + return resp, nil }