├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── client.go ├── handlers.go ├── logger.go ├── main.go ├── payload.go ├── transcribe.go └── upload.go /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | 3 | # Created by https://www.gitignore.io/api/macos,visualstudiocode,go,vim 4 | # Edit at https://www.gitignore.io/?templates=macos,visualstudiocode,go,vim 5 | 6 | ### Go ### 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | 23 | ### Go Patch ### 24 | /vendor/ 25 | /Godeps/ 26 | 27 | ### macOS ### 28 | # General 29 | .DS_Store 30 | .AppleDouble 31 | .LSOverride 32 | 33 | # Icon must end with two \r 34 | Icon 35 | 36 | # Thumbnails 37 | ._* 38 | 39 | # Files that might appear in the root of a volume 40 | .DocumentRevisions-V100 41 | .fseventsd 42 | .Spotlight-V100 43 | .TemporaryItems 44 | .Trashes 45 | .VolumeIcon.icns 46 | .com.apple.timemachine.donotpresent 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | 55 | ### Vim ### 56 | # Swap 57 | [._]*.s[a-v][a-z] 58 | [._]*.sw[a-p] 59 | [._]s[a-rt-v][a-z] 60 | [._]ss[a-gi-z] 61 | [._]sw[a-p] 62 | 63 | # Session 64 | Session.vim 65 | Sessionx.vim 66 | 67 | # Temporary 68 | .netrwhist 69 | *~ 70 | # Auto-generated tag files 71 | tags 72 | # Persistent undo 73 | [._]*.un~ 74 | 75 | ### VisualStudioCode ### 76 | .vscode/ 77 | !.vscode/settings.json 78 | !.vscode/tasks.json 79 | !.vscode/launch.json 80 | !.vscode/extensions.json 81 | 82 | ### VisualStudioCode Patch ### 83 | # Ignore all local history of files 84 | .history 85 | 86 | # End of https://www.gitignore.io/api/macos,visualstudiocode,go,vim 87 | 88 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 89 | 90 | ./ocrbot -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:2ee1ec060a2e71d2eb24755cb3f3867c52faa9895d2287d3c3bf09b01020cf32" 6 | name = "github.com/ChimeraCoder/anaconda" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "9c68684170b980d5b4c2ed08fae9b530e659904d" 10 | version = "v2.0.0" 11 | 12 | [[projects]] 13 | branch = "master" 14 | digest = "1:088fd1efa8fcd4f85c3e3c0d135f28114115517a2b215f2e39b67146d032ea33" 15 | name = "github.com/ChimeraCoder/tokenbucket" 16 | packages = ["."] 17 | pruneopts = "UT" 18 | revision = "c5a927568de7aad8a58127d80bcd36ca4e71e454" 19 | 20 | [[projects]] 21 | branch = "master" 22 | digest = "1:863cb3d06b96794fd14a54579116b7e704cd8bc395e9d0404a5ad236d3e198b4" 23 | name = "github.com/azr/backoff" 24 | packages = ["."] 25 | pruneopts = "UT" 26 | revision = "53511d3c733003985b0b76f733df1f4d0095ee6a" 27 | 28 | [[projects]] 29 | digest = "1:af462ca573dd761a91707fbac1b073c529c4e701d98913ab501066096ae89a73" 30 | name = "github.com/dghubble/oauth1" 31 | packages = ["."] 32 | pruneopts = "UT" 33 | revision = "19d7291ba290f420ccea50e076322aca95f27fcd" 34 | version = "v0.6.0" 35 | 36 | [[projects]] 37 | branch = "master" 38 | digest = "1:efe7dc6d12119a36c1a617b574e99ac9c5ca8fbff57eb283a60462552b7c782a" 39 | name = "github.com/dustin/go-jsonpointer" 40 | packages = ["."] 41 | pruneopts = "UT" 42 | revision = "ba0abeacc3dcca5b9b20f31509c46794edbc9965" 43 | 44 | [[projects]] 45 | branch = "master" 46 | digest = "1:07263fbfa1c038e890b410f730279aa7adc217180f1c2f78c6dc80b3bbe9e819" 47 | name = "github.com/dustin/gojson" 48 | packages = ["."] 49 | pruneopts = "UT" 50 | revision = "2e71ec9dd5adce3b168cd0dbde03b5cc04951c30" 51 | 52 | [[projects]] 53 | branch = "master" 54 | digest = "1:b5cfd8d62c46082a75974f2f5e167c272f7f7d47c7a21caf49b17a45a558a601" 55 | name = "github.com/garyburd/go-oauth" 56 | packages = ["oauth"] 57 | pruneopts = "UT" 58 | revision = "bca2e7f09a178fd36b034107a00e2323bca6a82e" 59 | 60 | [[projects]] 61 | digest = "1:cbec35fe4d5a4fba369a656a8cd65e244ea2c743007d8f6c1ccb132acf9d1296" 62 | name = "github.com/gorilla/mux" 63 | packages = ["."] 64 | pruneopts = "UT" 65 | revision = "00bdffe0f3c77e27d2cf6f5c70232a2d3e4d9c15" 66 | version = "v1.7.3" 67 | 68 | [[projects]] 69 | digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de" 70 | name = "github.com/konsorten/go-windows-terminal-sequences" 71 | packages = ["."] 72 | pruneopts = "UT" 73 | revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e" 74 | version = "v1.0.2" 75 | 76 | [[projects]] 77 | digest = "1:f3b5bf575d84780832516c53d135b4917fe891ca67c472959578b7e09277e4b2" 78 | name = "github.com/otiai10/gosseract" 79 | packages = ["."] 80 | pruneopts = "UT" 81 | revision = "5bb1d6fc20fa3fafb3236d6c93c393369e4b38d9" 82 | version = "v2.2.1" 83 | 84 | [[projects]] 85 | digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976" 86 | name = "github.com/sirupsen/logrus" 87 | packages = ["."] 88 | pruneopts = "UT" 89 | revision = "839c75faf7f98a33d445d181f3018b5c3409a45e" 90 | version = "v1.4.2" 91 | 92 | [[projects]] 93 | branch = "master" 94 | digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70" 95 | name = "golang.org/x/net" 96 | packages = ["context"] 97 | pruneopts = "UT" 98 | revision = "da137c7871d730100384dbcf36e6f8fa493aef5b" 99 | 100 | [[projects]] 101 | branch = "master" 102 | digest = "1:47844666be86089349a441f5f0ece22f42a87a8cb8c9a31294c593f43209ad19" 103 | name = "golang.org/x/sys" 104 | packages = ["unix"] 105 | pruneopts = "UT" 106 | revision = "fde4db37ae7ad8191b03d30d27f258b5291ae4e3" 107 | 108 | [solve-meta] 109 | analyzer-name = "dep" 110 | analyzer-version = 1 111 | input-imports = [ 112 | "github.com/ChimeraCoder/anaconda", 113 | "github.com/dghubble/oauth1", 114 | "github.com/gorilla/mux", 115 | "github.com/otiai10/gosseract", 116 | "github.com/sirupsen/logrus", 117 | ] 118 | solver-name = "gps-cdcl" 119 | solver-version = 1 120 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[override]] 29 | name = "github.com/chimeracoder/anaconda" 30 | version = "2.0.0" 31 | 32 | [prune] 33 | go-tests = true 34 | unused-packages = true 35 | 36 | [[override]] 37 | name = "github.com/otiai10/gosseract" 38 | version = "2.2.1" 39 | 40 | [[constraint]] 41 | name = "github.com/gorilla/mux" 42 | version = "1.7.3" 43 | 44 | [[constraint]] 45 | name = "github.com/dghubble/oauth1" 46 | version = "0.6.0" 47 | 48 | [[constraint]] 49 | name = "github.com/sirupsen/logrus" 50 | version = "1.4.2" 51 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | 11 | "github.com/ChimeraCoder/anaconda" 12 | "github.com/dghubble/oauth1" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | var ( 17 | apiKey = os.Getenv("APIKEY") 18 | apiSecret = os.Getenv("APISECRET") 19 | accessToken = os.Getenv("ACCESSTOKEN") 20 | accessSecret = os.Getenv("ACCESSSECRET") 21 | webhookEndpoint = "https://api.twitter.com/1.1/account_activity/all/%s/webhooks.json" 22 | subscriptionEndpoint = "https://api.twitter.com/1.1/account_activity/all/%s/subscriptions.json" 23 | ) 24 | 25 | var api *anaconda.TwitterApi 26 | 27 | func init() { 28 | anaconda.SetConsumerKey(apiKey) 29 | anaconda.SetConsumerSecret(apiSecret) 30 | api = anaconda.NewTwitterApi(accessToken, accessSecret) 31 | } 32 | 33 | func replyTweet(tweet string, replyID string) error { 34 | v := url.Values{} 35 | v.Set("in_reply_to_status_id", replyID) 36 | _, err := api.PostTweet(tweet, v) 37 | if err != nil { 38 | return errors.Wrap(err, "reply tweet failed") 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func createClient() *http.Client { 45 | config := oauth1.NewConfig(apiKey, apiSecret) 46 | token := oauth1.NewToken(accessToken, accessSecret) 47 | return config.Client(oauth1.NoContext, token) 48 | } 49 | 50 | // registerWebhook registers the webhook path to twitter environment and returns respective webhookID 51 | func registerWebhook(envName, webhookPath string) (string, error) { 52 | httpClient := createClient() 53 | path := fmt.Sprintf(webhookEndpoint, envName) 54 | values := url.Values{} 55 | values.Set("url", webhookPath) 56 | 57 | // TODO: Check whether you can achieve the same using anaconda 58 | resp, err := httpClient.PostForm(path, values) 59 | if err != nil { 60 | return "", errors.Wrap(err, "register webhook POST failed") 61 | } 62 | defer resp.Body.Close() 63 | 64 | body, err := ioutil.ReadAll(resp.Body) 65 | if err != nil { 66 | return "", errors.Wrap(err, "read failed") 67 | } 68 | 69 | var data map[string]interface{} 70 | if err := json.Unmarshal([]byte(body), &data); err != nil { 71 | return "", errors.Wrap(err, "json unmarshall failed") 72 | } 73 | log.Infof("Registered Webhook: %s", data) 74 | webhookID, err := subscribeWebhook(envName) 75 | if err != nil { 76 | return "", errors.Wrap(err, "subscribe webhook failed") 77 | } 78 | 79 | return webhookID, nil 80 | } 81 | 82 | // subscribeWebhook subscribes this endpoint to events 83 | func subscribeWebhook(envName string) (string, error) { 84 | client := createClient() 85 | path := fmt.Sprintf(subscriptionEndpoint, envName) 86 | 87 | // TODO: Check whether you can achieve the same using anaconda 88 | resp, err := client.PostForm(path, nil) 89 | if err != nil { 90 | return "", errors.Wrap(err, "webhook post call failed") 91 | } 92 | if resp.StatusCode != 204 { 93 | return "", errors.New("204 status code not received") 94 | } 95 | 96 | webhookID, err := ioutil.ReadAll(resp.Body) 97 | if err != nil { 98 | return "", errors.Wrap(err, "webhook read failed") 99 | } 100 | 101 | return string(webhookID), nil 102 | } 103 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "math/rand" 11 | "net/http" 12 | "net/url" 13 | "regexp" 14 | "strings" 15 | "time" 16 | 17 | "github.com/gorilla/mux" 18 | ) 19 | 20 | var re = regexp.MustCompile(`(?:^|[^@#/])\b(\w+)`) 21 | var replies = []string{ 22 | "Howdy 👋\nHere is the transcribed content: ", 23 | "Hey!\nThere you go: ", 24 | "Hola 🤓\nHere it is: ", 25 | "Done 😊", 26 | "Phew, I guess I got it right 🙈", 27 | "Yee-haw! 🤠\nText coming through: ", 28 | } 29 | 30 | func startServer() { 31 | m := mux.NewRouter() 32 | m.HandleFunc("/webhook/twitter", crcCheck).Methods("GET") 33 | m.HandleFunc("/webhookdev/twitter", crcCheck).Methods("GET") // Dev environment 34 | m.HandleFunc("/webhook/twitter", webhookHandler).Methods("POST") 35 | m.HandleFunc("/webhookdev/twitter", webhookHandler).Methods("POST") // Dev environment 36 | m.HandleFunc("/register-webhook", registerNewWebhook).Methods("POST") 37 | m.HandleFunc("/ping", ping).Methods("GET") 38 | 39 | server := &http.Server{ 40 | Handler: m, 41 | } 42 | server.Addr = "0.0.0.0:8000" 43 | log.Info("Starting server...") 44 | server.ListenAndServe() 45 | } 46 | 47 | func webhookHandler(w http.ResponseWriter, r *http.Request) { 48 | body, _ := ioutil.ReadAll(r.Body) 49 | var payload Webhook 50 | err := json.Unmarshal(body, &payload) 51 | if err != nil { 52 | log.Error(err) 53 | return 54 | } 55 | 56 | //Check if it was a tweet_create_event and tweet was in the payload and it was not tweeted by the bot 57 | if len(payload.TweetCreateEvent) < 1 || payload.UserId == payload.TweetCreateEvent[0].User.IdStr { 58 | log.Warn("Tweet create event is not a mention") 59 | return 60 | } 61 | log.Infof("WH payload: %+v", payload) 62 | txt := payload.TweetCreateEvent[0].Text 63 | if !isCommand(txt) { 64 | log.Error("Not a valid command") 65 | return 66 | } 67 | 68 | replyHandle := payload.TweetCreateEvent[0].User.Handle 69 | log.Infof("Got mentioned by %s", replyHandle) 70 | 71 | parentID := payload.TweetCreateEvent[0].ParentID 72 | if parentID == 0 { 73 | log.Error("Unable to find parent tweet ID") 74 | return 75 | } 76 | v := url.Values{} 77 | parentTweet, err := api.GetTweet(parentID, v) 78 | if err != nil { 79 | log.Error("Unable to get parent tweet") 80 | return 81 | } 82 | if len(parentTweet.Entities.Media) == 0 { 83 | log.Error("Parent tweet has no media") 84 | return 85 | } 86 | 87 | // TODO: handle multiple media files 88 | media := parentTweet.Entities.Media[0] 89 | if media.Type != "photo" { 90 | return 91 | } 92 | 93 | // TODO: use dynamic file names for saving images 94 | err = downloadImage(media.Media_url_https, "pic.jpg") 95 | if err != nil { 96 | log.Errorf("Unable to download image. \n %s", err) 97 | return 98 | } 99 | 100 | text, err := read("pic.jpg") 101 | if err != nil { 102 | log.Errorf("Unable to read image. \n %s", err) 103 | return 104 | } 105 | 106 | // TODO: If pastebin API limit is reached, retry here. Or add queue for creating pastes 107 | pasteURL, err := createPaste(text) 108 | if err != nil { 109 | log.Errorf("Unable to create paste. \n %s", err) 110 | return 111 | } 112 | 113 | replyText := pickReply() 114 | err = replyTweet("@"+replyHandle+" "+replyText+"\n"+pasteURL, payload.TweetCreateEvent[0].IdStr) 115 | if err != nil { 116 | log.Errorf("Error while replying to %s \n %s", replyHandle, err) // Log tweet URL instead of just handle 117 | } else { 118 | log.Infof("Reply sent successfully to %s", replyHandle) 119 | } 120 | 121 | } 122 | 123 | func crcCheck(w http.ResponseWriter, r *http.Request) { 124 | w.Header().Set("Content-Type", "application/json") 125 | 126 | token := r.URL.Query()["crc_token"] 127 | if len(token) < 1 { 128 | log.Error("No crc token given") 129 | return 130 | } 131 | 132 | h := hmac.New(sha256.New, []byte(apiSecret)) 133 | h.Write([]byte(token[0])) 134 | encoded := base64.StdEncoding.EncodeToString(h.Sum(nil)) 135 | 136 | response := make(map[string]string) 137 | response["response_token"] = "sha256=" + encoded 138 | 139 | responseJSON, err := json.Marshal(response) 140 | if err != nil { 141 | log.Error(err) 142 | return 143 | } 144 | log.Info("CRC check done successfully") 145 | fmt.Fprintf(w, string(responseJSON)) 146 | } 147 | 148 | func ping(w http.ResponseWriter, r *http.Request) { 149 | fmt.Fprint(w, "pong") 150 | } 151 | 152 | func registerNewWebhook(w http.ResponseWriter, r *http.Request) { 153 | var payload RegisterWebHook 154 | decoder := json.NewDecoder(r.Body) 155 | decoder.Decode(&payload) 156 | webhookID, err := registerWebhook(payload.EnvName, payload.AppURL+payload.WebhookPath) 157 | if err != nil { 158 | log.Errorf("Unable to register webhook.\n %s", err) 159 | } 160 | log.Infof("subscribed to webhook: %s", webhookID) 161 | } 162 | 163 | func isCommand(s string) bool { 164 | cmds := re.FindAllString(s, -1) 165 | cmd := strings.TrimSpace(cmds[0]) 166 | if cmd != "extract" { 167 | return false 168 | } 169 | return true 170 | } 171 | 172 | func pickReply() string { 173 | rand.Seed(time.Now().UnixNano()) 174 | i := rand.Intn(len(replies)) 175 | return replies[i] 176 | } 177 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var log = *logrus.New() 12 | 13 | // newLogger creates a new logger instance that writes to both stdout and a log file 14 | func newMultiLogger() (*os.File, error) { 15 | f, err := os.OpenFile("ocrbot.log", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) 16 | if err != nil { 17 | return nil, errors.Wrap(err, "create file failed") 18 | } 19 | mw := io.MultiWriter(os.Stdout, f) 20 | log.SetOutput(mw) 21 | log.Level = logrus.InfoLevel 22 | return f, nil 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | f, err := newMultiLogger() 5 | defer f.Close() 6 | if err != nil { 7 | log.Fatalf("Unable to create log file \n %s", err) 8 | } 9 | 10 | startServer() 11 | } 12 | -------------------------------------------------------------------------------- /payload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Webhook struct { 4 | UserId string `json:"for_user_id"` 5 | TweetCreateEvent []Tweet `json:"tweet_create_events"` 6 | } 7 | 8 | type Tweet struct { 9 | Id int64 10 | IdStr string `json:"id_str"` 11 | User User 12 | Text string 13 | ParentID int64 `json:"in_reply_to_status_id"` 14 | Entities Entity `json:"entities"` 15 | } 16 | 17 | type Entity struct { 18 | Media []Media `json:"media"` 19 | } 20 | 21 | type Media struct { 22 | ID int64 `json:"id"` 23 | URL string `json:"media_url_https"` 24 | Type string `json:"type"` 25 | } 26 | 27 | type User struct { 28 | Id int64 29 | IdStr string `json:"id_str"` 30 | Name string 31 | Handle string `json:"screen_name"` 32 | } 33 | 34 | type RegisterWebHook struct { 35 | EnvName string `json:"env_name"` 36 | AppURL string `json:"app_url"` 37 | WebhookPath string `json:"webhook_path"` 38 | } 39 | -------------------------------------------------------------------------------- /transcribe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/otiai10/gosseract" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // read reads the image from given path and returns the transcribed text 13 | func read(path string) (string, error) { 14 | client := gosseract.NewClient() 15 | defer client.Close() 16 | 17 | err := client.SetImage(path) 18 | if err != nil { 19 | return "", errors.Wrap(err, "image read failed") 20 | } 21 | 22 | text, err := client.Text() 23 | if err != nil { 24 | return "", errors.Wrap(err, "text conversion failed") 25 | } 26 | 27 | return text, nil 28 | } 29 | 30 | // downaloadImage downloads the image from url to local file 31 | func downloadImage(url, path string) error { 32 | response, err := http.Get(url) 33 | if err != nil { 34 | return err 35 | } 36 | defer response.Body.Close() 37 | 38 | file, err := os.Create(path) 39 | if err != nil { 40 | return errors.Wrap(err, "create image path failed") 41 | } 42 | defer file.Close() 43 | 44 | _, err = io.Copy(file, response.Body) 45 | if err != nil { 46 | return errors.Wrap(err, "copy image to file failed") 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | var ( 14 | apiDevKey = os.Getenv("APIDEVKEY") 15 | userKey = os.Getenv("USERKEY") 16 | pasteBinURL = "https://pastebin.com/api/api_post.php" 17 | ) 18 | 19 | // createPaste uploads the text to pastebin account and returns URL of the paste 20 | func createPaste(text string) (string, error) { 21 | v := url.Values{} 22 | v.Add("api_dev_key", apiDevKey) 23 | v.Add("api_user_key", userKey) 24 | v.Add("api_option", "paste") 25 | v.Add("api_paste_code", text) 26 | 27 | buf := strings.NewReader(v.Encode()) 28 | resp, err := http.Post(pasteBinURL, "application/x-www-form-urlencoded", buf) 29 | if err != nil { 30 | return "", errors.Wrap(err, "pastebin upload failed") 31 | } 32 | 33 | pasteURL, err := ioutil.ReadAll(resp.Body) 34 | if err != nil { 35 | return "", errors.Wrap(err, "read failed") 36 | } 37 | return string(pasteURL), nil 38 | } 39 | --------------------------------------------------------------------------------