├── .gitignore ├── pkg ├── interface.go ├── login.go ├── base.go ├── debug.go ├── share.go ├── instagram.go └── twitter.go ├── go.mod ├── go.sum ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | account_data 2 | sobot 3 | *png 4 | screenshots 5 | .rod 6 | comm 7 | main.go 8 | -------------------------------------------------------------------------------- /pkg/interface.go: -------------------------------------------------------------------------------- 1 | package sobot 2 | 3 | // type PlatformInfo interface { 4 | // Login() string 5 | // } 6 | 7 | // type ShareInterface interface { 8 | // Share() 9 | // } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/canack/sobot 2 | 3 | go 1.17 4 | 5 | require github.com/go-rod/rod v0.101.8 6 | 7 | require ( 8 | github.com/ysmood/goob v0.3.0 // indirect 9 | github.com/ysmood/gson v0.6.4 // indirect 10 | github.com/ysmood/leakless v0.7.0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/login.go: -------------------------------------------------------------------------------- 1 | package sobot 2 | 3 | func Instagram(user, pass string) *AccountInfo { 4 | //&AccountInfo{Username: user.Username, Password: user.Password} 5 | return &AccountInfo{Username: user, Password: pass, Pname: "instagram", FilePath: "", Caption: ""} 6 | } 7 | 8 | func Twitter(user, pass string) *AccountInfo { 9 | //&AccountInfo{Username: user.Username, Password: user.Password} 10 | return &AccountInfo{Username: user, Password: pass, Pname: "twitter", FilePath: "", Caption: ""} 11 | 12 | } 13 | -------------------------------------------------------------------------------- /pkg/base.go: -------------------------------------------------------------------------------- 1 | package sobot 2 | 3 | import ( 4 | device "github.com/go-rod/rod/lib/devices" 5 | ) 6 | 7 | type AccountInfo struct { 8 | Username, Password, FilePath, Pname, Caption string 9 | } 10 | 11 | var ( 12 | rod_device = device.Device{ 13 | Title: "Laptop with touch", 14 | Capabilities: []string{}, 15 | UserAgent: "", 16 | Screen: device.Screen{ 17 | DevicePixelRatio: 1, 18 | Horizontal: device.ScreenSize{ 19 | Width: 800, 20 | Height: 760, 21 | }, 22 | Vertical: device.ScreenSize{ 23 | Width: 800, 24 | Height: 760, 25 | }, 26 | }, 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /pkg/debug.go: -------------------------------------------------------------------------------- 1 | package sobot 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/go-rod/rod" 10 | ) 11 | 12 | func (user *AccountInfo) watchBrowser(page *rod.Page, end chan bool) { 13 | dt := time.Now() 14 | date := dt.Format("02-01-2006_15.04.05") 15 | 16 | directory := "screenshots/" + user.Username + "/" + user.Pname + "/" + date 17 | err := os.MkdirAll(directory, os.ModePerm) 18 | 19 | if err != nil { 20 | log.Println("We can't create a document for debugging") 21 | } 22 | 23 | final := false 24 | 25 | go func() { 26 | 27 | if <-end { 28 | final = true 29 | } 30 | }() 31 | 32 | for i := 0; ; i++ { 33 | if final { 34 | break 35 | } 36 | 37 | num := strconv.Itoa(i) 38 | name := directory + "/debug_" + num + ".png" 39 | page.MustScreenshot(name) 40 | log.Println("Captured:", name) 41 | time.Sleep(time.Second) 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/share.go: -------------------------------------------------------------------------------- 1 | package sobot 2 | 3 | import "log" 4 | 5 | func (user *AccountInfo) SetFile(data string) *AccountInfo { 6 | return &AccountInfo{ 7 | Username: user.Username, 8 | Password: user.Password, 9 | FilePath: data, 10 | Pname: user.Pname, 11 | Caption: user.Caption, 12 | } 13 | } 14 | 15 | func (user *AccountInfo) SetCaption(caption string) *AccountInfo { 16 | return &AccountInfo{ 17 | Username: user.Username, 18 | Password: user.Password, 19 | FilePath: user.FilePath, 20 | Pname: user.Pname, 21 | Caption: caption, 22 | } 23 | } 24 | 25 | func (user *AccountInfo) Share(debug bool) { 26 | if user.Pname == "instagram" { 27 | // call shareInstagram function 28 | log.Println(user.Username, "******", user.Pname) 29 | user.shareInstagram(debug) 30 | 31 | } else if user.Pname == "twitter" { 32 | // call shareTwitter function 33 | log.Println(user.Username, "******", user.Pname) 34 | user.shareTwitter(debug) 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-rod/rod v0.101.8 h1:oV0O97uwjkCVyAP0hD6K6bBE8FUMIjs0dtF7l6kEBsU= 2 | github.com/go-rod/rod v0.101.8/go.mod h1:N/zlT53CfSpq74nb6rOR0K8UF0SPUPBmzBnArrms+mY= 3 | github.com/ysmood/goob v0.3.0 h1:XZ51cZJ4W3WCoCiUktixzMIQF86W7G5VFL4QQ/Q2uS0= 4 | github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= 5 | github.com/ysmood/got v0.15.1 h1:X5jAbMyBf5yeezuFMp9HaMGXZWMSqIQcUlAHI+kJmUs= 6 | github.com/ysmood/got v0.15.1/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= 7 | github.com/ysmood/gotrace v0.2.2 h1:006KHGRThSRf8lwh4EyhNmuuq/l+Ygs+JqojkhEG1/E= 8 | github.com/ysmood/gotrace v0.2.2/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= 9 | github.com/ysmood/gson v0.6.4 h1:Yb6tosv6bk59HqjZu2/7o4BFherpYEMkDkXmlhgryZ4= 10 | github.com/ysmood/gson v0.6.4/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= 11 | github.com/ysmood/leakless v0.7.0 h1:XCGdaPExyoreoQd+H5qgxM3ReNbSPFsEXpSKwbXbwQw= 12 | github.com/ysmood/leakless v0.7.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Can Açıkgöz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # - SOBOT - Automated Social Media Sharing Tool 2 | 3 | Social media post sharing tool 4 | 5 | ## Features 6 | 7 | - The application has a stable beta version. Errors that will occur will be on the selector codes. 8 | - For now it only has instagram and twitter support. 9 | - Saves screenshots for bug tracking 10 | - Username and password are used for first login only. Then it continues over the cookie. 11 | 12 | ## Usage/Examples 13 | 14 | ```go 15 | package main 16 | 17 | import ( 18 | "github.com/canack/sobot/pkg" 19 | ) 20 | 21 | func main(){ 22 | // User creds 23 | username := "nickname" 24 | password := "secretpass123" 25 | 26 | // Post parameters 27 | file := "/home/user/Desktop/photo.jpg" 28 | comment := "Have a nice day!" 29 | 30 | // you can activate debugging with Share(true) 31 | // Sharing on Instagram 32 | sobot.Instagram(username, password).SetFile(file).SetCaption(comment).Share(true) 33 | 34 | // Sharing on Twitter 35 | sobot.Twitter(username, password).SetFile(file).SetCaption(comment).Share(true) 36 | } 37 | ``` 38 | 39 | --- 40 | ### By default, Rod will disable the browser's UI to maximize the performance. But when developing an automation task we usually care more about the ease of debugging. Rod provides a lot of solutions to help you debug the code. 41 | 42 | Let's create a ".rod" config file under the current working directory. The content is: 43 | 44 | ```show``` 45 | 46 | --- 47 | 48 | The automated operations are too fast for human eyes to catch, to debug them we usually enable the visual trace config, let's update the ".rod" file: 49 | 50 | ``` 51 | show 52 | trace 53 | ``` 54 | --- 55 | 56 | ## Routemap 57 | 58 | - [+] Instagram support (currently stable-beta) 59 | 60 | - [+] Twitter support (currently stable-beta) 61 | 62 | 63 | ## Contributors 64 | 65 | - [@canack](https://www.github.com/canack) -------------------------------------------------------------------------------- /pkg/instagram.go: -------------------------------------------------------------------------------- 1 | package sobot 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-rod/rod" 11 | "github.com/go-rod/rod/lib/proto" 12 | ) 13 | 14 | var ( 15 | watch_instagram bool 16 | ) 17 | 18 | func (user *AccountInfo) firstInstagramRun() { 19 | 20 | os.MkdirAll("account_data/instagram/"+user.Username, os.ModePerm) 21 | log.Println("First run for ", user.Username, " on ", user.Pname) 22 | 23 | // first section 24 | page := rod.New().SlowMotion(time.Second * 2).MustConnect().MustPage("about:blank") 25 | 26 | page.MustEmulate(rod_device) 27 | 28 | page.MustNavigate("https://instagram.com").MustWaitLoad() 29 | 30 | // For debugging 31 | end_debug := make(chan bool) 32 | 33 | if watch_instagram { 34 | go user.watchBrowser(page, end_debug) 35 | } 36 | time.Sleep(time.Second * 3) 37 | 38 | // username input 39 | page.MustElementX("/html/body/div[1]/section/main/article/div[2]/div[1]/div/form/div/div[1]/div/label/input").MustInput(user.Username) 40 | 41 | // password input 42 | page.MustElementX("/html/body/div[1]/section/main/article/div[2]/div[1]/div/form/div/div[2]/div/label/input").MustInput(user.Password) 43 | 44 | // login button 45 | page.MustElementX("/html/body/div[1]/section/main/article/div[2]/div[1]/div/form/div/div[3]/button").MustClick() 46 | 47 | // This section makes the first login to the account and reports if the password is wrong 48 | time.Sleep(time.Second * 7) 49 | _, err := page.Timeout(time.Second * 5).ElementX("/html/body/div[1]/section/main/article/div[2]/div[1]/div/form/div[1]/div[1]/div/label/input") 50 | 51 | if err == nil { 52 | page.Close() 53 | log.Println("Username or password incorrect") 54 | return 55 | } 56 | // This section makes the first login to the account and reports if the password is wrong 57 | 58 | page.MustWaitLoad() 59 | log.Println("Wait 3 second for get cookies successful.") 60 | time.Sleep(time.Second * 3) 61 | 62 | f, err := os.Create("account_data/instagram/" + user.Username + "/cookie.dat") 63 | 64 | if err != nil { 65 | log.Fatal("We got an error at creating cookie data file\n", err) 66 | } 67 | 68 | // Write cookies into file 69 | for _, v := range page.MustCookies() { 70 | _, err2 := f.WriteString(v.Name + "#" + v.Value + "#" + v.Domain + "\n") 71 | 72 | if err2 != nil { 73 | log.Fatal("Cookies doesn't writed on file\n", err2) 74 | } 75 | } 76 | log.Println("Cookies saved.") 77 | f.Close() 78 | 79 | if watch_instagram { 80 | end_debug <- true // for stop saving screenshots 81 | } 82 | 83 | page.Close() 84 | 85 | log.Println("Login successful and cookies registered") 86 | user.cookieInstagramRun() 87 | 88 | } 89 | 90 | func (user *AccountInfo) cookieInstagramRun() { 91 | log.Println("Found cookie for ", user.Username) 92 | 93 | // first section 94 | page := rod.New().SlowMotion(time.Second * 2).MustConnect().MustPage("about:blank") 95 | 96 | page.MustEmulate(rod_device) 97 | 98 | file, err := os.Open("account_data/instagram/" + user.Username + "/cookie.dat") 99 | 100 | if err != nil { 101 | log.Fatalf(user.Username, user.Pname, " cookie read error: %s", err) 102 | } 103 | 104 | scanner := bufio.NewScanner(file) 105 | scanner.Split(bufio.ScanLines) 106 | 107 | for scanner.Scan() { 108 | saved_dat := strings.Split(scanner.Text(), "#") 109 | page.MustSetCookies([]*proto.NetworkCookieParam{{ 110 | Name: saved_dat[0], 111 | Value: saved_dat[1], 112 | Domain: saved_dat[2], 113 | }}...) 114 | 115 | } 116 | 117 | file.Close() 118 | log.Println("Cookies loaded") 119 | 120 | page.MustNavigate("https://instagram.com").MustWaitLoad() 121 | // For debugging 122 | end_debug := make(chan bool) 123 | 124 | if watch_instagram { 125 | go user.watchBrowser(page, end_debug) 126 | } 127 | time.Sleep(time.Second * 5) 128 | 129 | // Pop-up control 130 | go func(page *rod.Page) { 131 | page.MustElementX("/html/body/div[5]/div/div/div/div[3]/button[2]").MustClick() 132 | }(page) 133 | 134 | // Add post button 135 | page.MustElementX("/html/body/div[1]/section/nav/div[2]/div/div/div[3]/div/div[3]/div/button").MustClick() 136 | 137 | // Set files 138 | page.MustElementX("/html/body/div[8]/div[2]/div/div/div/div[2]/div[1]/form/input").MustSetFiles(user.FilePath) 139 | 140 | // Next button 141 | page.MustElementX("/html/body/div[6]/div[2]/div/div/div/div[1]/div/div/div[2]/div/button").MustClick() 142 | 143 | // Next button 144 | page.MustElementX("/html/body/div[6]/div[2]/div/div/div/div[1]/div/div/div[2]/div/button").MustClick() 145 | 146 | // Write a caption 147 | page.MustElementX("/html/body/div[6]/div[2]/div/div/div/div[2]/div[2]/div/div/div/div[2]/div[1]/textarea").MustInput(user.Caption) 148 | 149 | // Share button 150 | page.MustElementX("/html/body/div[6]/div[2]/div/div/div/div[1]/div/div/div[2]/div/button").MustClick() 151 | 152 | log.Println("All process are done, waiting 15 seconds") 153 | time.Sleep(time.Second * 15) 154 | 155 | if watch_instagram { 156 | end_debug <- true // for stop saving screenshots 157 | } 158 | log.Println("Everything ok, shutting down browser.") 159 | page.Close() 160 | 161 | log.Println("Login with Cookie and share successfully") 162 | 163 | } 164 | 165 | func (user *AccountInfo) shareInstagram(debug bool) { 166 | 167 | _, err := os.ReadFile("account_data/instagram/" + user.Username + "/cookie.dat") 168 | watch_instagram = debug 169 | if err != nil { 170 | 171 | user.firstInstagramRun() 172 | 173 | } else { 174 | 175 | user.cookieInstagramRun() 176 | 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /pkg/twitter.go: -------------------------------------------------------------------------------- 1 | package sobot 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-rod/rod" 11 | "github.com/go-rod/rod/lib/proto" 12 | ) 13 | 14 | var watch_twitter bool 15 | 16 | func (user *AccountInfo) firstTwitterRun() { 17 | 18 | os.MkdirAll("account_data/twitter/"+user.Username, os.ModePerm) 19 | log.Println("First run for ", user.Username, " on ", user.Pname) 20 | 21 | page := rod.New().SlowMotion(time.Second * 2).MustConnect().MustPage("about:blank") 22 | 23 | page.MustEmulate(rod_device) 24 | 25 | page.MustNavigate("https://twitter.com/i/flow/login").MustWaitLoad() 26 | // For debugging 27 | end_debug := make(chan bool) 28 | 29 | if watch_twitter { 30 | go user.watchBrowser(page, end_debug) 31 | } 32 | 33 | time.Sleep(time.Second * 3) 34 | 35 | page.MustElementX("/html/body/div/div/div/div[1]/div/div/div/div/div/div/div[2]/div[2]/div/div/div[2]/div[2]/div[1]/div/div[5]/label/div/div[2]/div/input").MustInput(user.Username) 36 | page.MustElementX("/html/body/div/div/div/div[1]/div/div/div/div/div/div/div[2]/div[2]/div/div/div[2]/div[2]/div[1]/div/div[6]/div/span").MustClick() 37 | page.MustElementX("/html/body/div/div/div/div[1]/div/div/div/div/div/div/div[2]/div[2]/div/div/div[2]/div[2]/div[1]/div/div[3]/div/label/div/div[2]/div[1]/input").MustInput(user.Password) 38 | page.MustElementX("/html/body/div/div/div/div[1]/div/div/div/div/div/div/div[2]/div[2]/div/div/div[2]/div[2]/div[2]/div/div/div/span").MustClick() 39 | 40 | // This section makes the first login to the account and reports if the password is wrong 41 | time.Sleep(time.Second * 7) 42 | 43 | _, err := page.Timeout(time.Second * 5).ElementX("/html/body/div/div/div/div[1]/div/div/div/div/div/div/div[2]/div[2]/div/div/div[2]/div[2]/div[1]/div/div[1]/div[1]/span") 44 | 45 | if err == nil { 46 | page.Close() 47 | log.Println("Username or password incorrect") 48 | return 49 | } 50 | // This section makes the first login to the account and reports if the password is wrong 51 | 52 | page.MustWaitLoad() 53 | log.Println("Wait 3 second for get cookies successful.") 54 | time.Sleep(time.Second * 3) 55 | 56 | f, err := os.Create("account_data/twitter/" + user.Username + "/cookie.dat") 57 | 58 | if err != nil { 59 | log.Fatal("We got an error at creating cookie data file\n", err) 60 | } 61 | 62 | for _, v := range page.MustCookies() { 63 | _, err2 := f.WriteString(v.Name + "#" + v.Value + "#" + v.Domain + "\n") 64 | 65 | if err2 != nil { 66 | log.Fatal("Cookies doesn't writed on file\n", err2) 67 | } 68 | } 69 | log.Println("Cookies saved.") 70 | f.Close() 71 | 72 | if watch_instagram { 73 | end_debug <- true // for stop saving screenshots 74 | } 75 | 76 | page.Close() 77 | 78 | log.Println("Login successful and cookies registered") 79 | user.cookieTwitterRun() 80 | 81 | } 82 | 83 | func (user *AccountInfo) cookieTwitterRun() { 84 | log.Println("Found cookie for ", user.Username) 85 | 86 | // first section 87 | page := rod.New().SlowMotion(time.Second * 2).MustConnect().MustPage("about:blank") 88 | 89 | page.MustEmulate(rod_device) 90 | 91 | file, err := os.Open("account_data/twitter/" + user.Username + "/cookie.dat") 92 | 93 | if err != nil { 94 | log.Fatalf(user.Username, user.Pname, " cookie read error: %s", err) 95 | } 96 | 97 | scanner := bufio.NewScanner(file) 98 | scanner.Split(bufio.ScanLines) 99 | 100 | for scanner.Scan() { 101 | saved_dat := strings.Split(scanner.Text(), "#") 102 | page.MustSetCookies([]*proto.NetworkCookieParam{{ 103 | Name: saved_dat[0], 104 | Value: saved_dat[1], 105 | Domain: saved_dat[2], 106 | }}...) 107 | 108 | } 109 | 110 | file.Close() 111 | log.Println("Cookies loaded") 112 | 113 | page.MustNavigate("https://twitter.com").MustWaitLoad() 114 | 115 | // For debugging 116 | end_debug := make(chan bool) 117 | 118 | if watch_twitter { 119 | go user.watchBrowser(page, end_debug) 120 | } 121 | 122 | time.Sleep(time.Second * 5) 123 | 124 | page.MustElementX("/html/body/div/div/div/div[2]/main/div/div/div/div/div/div[2]/div/div[2]/div[1]/div/div/div/div[2]/div[1]/div/div/div/div/div/div/div/div/div/label/div[1]/div/div/div/div/div[2]/div/div/div/div").MustClick() 125 | 126 | page.MustElementX("/html/body/div/div/div/div[2]/main/div/div/div/div/div/div[2]/div/div[2]/div[1]/div/div/div/div[2]/div[1]/div/div/div/div/div/div/div/div/div/label/div[1]/div/div/div/div/div[2]/div/div/div/div").MustInput(user.Caption) 127 | time.Sleep(time.Second * 3) 128 | page.MustElementX("/html/body/div/div/div/div[2]/main/div/div/div/div/div/div[2]/div/div[2]/div[1]/div/div/div/div[2]/div[3]/div/div/div[1]/input").MustSetFiles(user.FilePath) 129 | time.Sleep(time.Second * 3) 130 | page.MustElementX("/html/body/div/div/div/div[2]/main/div/div/div/div/div/div[2]/div/div[2]/div[1]/div/div/div/div[2]/div[3]/div/div/div[2]/div[3]/div").MustClick() 131 | 132 | log.Println("All process are done, waiting 15 seconds") 133 | time.Sleep(time.Second * 15) 134 | 135 | if watch_instagram { 136 | end_debug <- true // for stop saving screenshots 137 | } 138 | 139 | log.Println("Everything ok, shutting down browser.") 140 | page.Close() 141 | 142 | log.Println("Login with Cookie and share successfully") 143 | } 144 | 145 | func (user *AccountInfo) shareTwitter(debug bool) { 146 | 147 | _, err := os.ReadFile("account_data/twitter/" + user.Username + "/cookie.dat") 148 | 149 | watch_twitter = debug 150 | 151 | if err != nil { 152 | user.firstTwitterRun() 153 | } else { 154 | user.cookieTwitterRun() 155 | } 156 | 157 | } 158 | --------------------------------------------------------------------------------