commit ec4f54cffcaf8b96d82fc24e396872e81d5207ad Author: Brian Buller Date: Wed Apr 1 14:41:58 2020 -0500 Initial Commit. Also, probably, done. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..029c065 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Binary +twitstch +# Configuration +twitstch.conf diff --git a/app_config.go b/app_config.go new file mode 100644 index 0000000..3475bda --- /dev/null +++ b/app_config.go @@ -0,0 +1,150 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "strconv" + "strings" + + "github.com/BurntSushi/toml" +) + +type AppConfig struct { + StorageDir string `toml:"storage_dir"` + ApiToken string `toml:"api_token"` + ApiSecret string `toml:"api_secret"` + AppToken string `toml:"app_token"` + AppSecret string `toml:"app_secret"` + + PullCount int + ForceDownload bool + Verbose bool +} + +func NewAppConfig(args []string) (*AppConfig, error) { + fmt.Println("Initializing App...") + c := &AppConfig{} + if err := c.ProcessArgs(args); err != nil { + return nil, err + } + if err := c.Load(); err != nil { + return nil, err + } + return c, c.VerifyConfig() +} + +func (c *AppConfig) ProcessArgs(args []string) error { + var err error + for _, arg := range args { + var k, v string + if strings.ContainsRune(arg, '=') { + k = arg[:strings.Index(arg, "=")] + v = arg[strings.Index(arg, "=")+1:] + } else { + k = arg + } + switch k { + case "-count", "-c": + c.PullCount, err = strconv.Atoi(v) + if err != nil { + return fmt.Errorf("Invalid Count (%s): %w", v, err) + } + fmt.Printf("Pulling %d tweets", c.PullCount) + case "-force", "-f": + c.ForceDownload = true + fmt.Println("Forcing Downloads") + case "-help", "-h": + PrintUsageAndExit() + case "-verbose", "-v": + c.Verbose = true + } + } + return nil +} + +func (c *AppConfig) Load() error { + cfgPath := AppName + ".conf" + tomlData, err := ioutil.ReadFile(cfgPath) + if err != nil { + if err = c.Save(); err != nil { + return err + } + } + if _, err := toml.Decode(string(tomlData), &c); err != nil { + return err + } + return nil +} + +func (c *AppConfig) Save() error { + buf := new(bytes.Buffer) + cfgPath := AppName + ".conf" + if err := toml.NewEncoder(buf).Encode(c); err != nil { + return err + } + return ioutil.WriteFile(cfgPath, buf.Bytes(), 0644) +} + +func (c *AppConfig) VerifyConfig() error { + configChanged := false + if c.ApiToken == "" { + c.ApiToken = GetDataFromUser("API Token") + configChanged = true + } + if c.ApiSecret == "" { + c.ApiSecret = GetDataFromUser("API Secret") + configChanged = true + } + if c.AppToken == "" { + c.AppToken = GetDataFromUser("App Token") + configChanged = true + } + if c.AppSecret == "" { + c.AppSecret = GetDataFromUser("App Secret") + configChanged = true + } + exist, err := FileExists(c.StorageDir) + for c.StorageDir == "" || err != nil || !exist { + // Check if the storage directory exists + c.StorageDir = GetDataFromUser("Image Download Directory") + configChanged = true + exist, err = FileExists(c.StorageDir) + } + if configChanged { + return c.Save() + } + fmt.Println("Screenshot Directory:", c.StorageDir) + return nil +} + +func (c *AppConfig) GetFilePath(filename string) string { + return c.StorageDir + string(os.PathSeparator) + filename +} + +func GetDataFromUser(label string) string { + reader := bufio.NewReader(os.Stdin) + var res string + for res == "" { + fmt.Println(label + ": ") + res, _ = reader.ReadString('\n') + res = strings.TrimSpace(res) + if res == "" { + fmt.Println("Non-empty response is required") + } + } + return res +} + +func FileExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return true, err +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..14103cc --- /dev/null +++ b/main.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/dghubble/go-twitter/twitter" + "github.com/dghubble/oauth1" +) + +const AppName = "twitstch" + +var appConfig *AppConfig + +func main() { + var err error + appConfig, err = NewAppConfig(os.Args) + if err != nil { + log.Fatal(err) + } + + config := oauth1.NewConfig(appConfig.ApiToken, appConfig.ApiSecret) + token := oauth1.NewToken(appConfig.AppToken, appConfig.AppSecret) + httpClient := config.Client(oauth1.NoContext, token) + + sleepTime := time.Minute + for { + PrintIfVerbose(time.Now().Format("20060102T150405"), ": Start\n") + err = ProcessTimeline(httpClient) + if err != nil { + // Backoff + sleepTime = sleepTime * 2 + } + PrintIfVerbose(time.Now().Format("20060102T150405"), ": Done\n") + time.Sleep(sleepTime) + if appConfig.ForceDownload { + PrintIfVerbose("'Force' flag set. Exiting.\n") + break + } + } +} + +// Doesn't work yet... +func WatchStream(httpClient *http.Client) { + // Twitter client + client := twitter.NewClient(httpClient) + params := &twitter.StreamUserParams{ + With: "followings", + StallWarnings: twitter.Bool(false), + } + stream, err := client.Streams.User(params) + if err != nil { + log.Fatal(err) + } + for message := range stream.Messages { + fmt.Println(message) + } + fmt.Println("Message Channel Closed") +} + +func ProcessTimeline(httpClient *http.Client) error { + // Twitter client + client := twitter.NewClient(httpClient) + + // Home Timeline (last 5 entries) + tweets, _, err := client.Timelines.HomeTimeline(&twitter.HomeTimelineParams{Count: 5}) + if err != nil { + return err + } + + for _, t := range tweets { + for _, m := range t.Entities.Media { + filename := t.Text[:strings.LastIndex(t.Text, " ")] + filename = strings.ReplaceAll(filename, " ", "_") + ".jpg" + create, err := t.CreatedAtTime() + if err != nil { + create = time.Now() + } + filename = create.Format("20060102T150405") + "_" + filename + if ImageNeedsDownload(filename) || appConfig.ForceDownload { + err = DownloadImage(httpClient, m.MediaURLHttps, filename) + if err != nil { + PrintIfVerbose("Error downloading image (", m.MediaURLHttps, ")", err.Error(), "\n") + + } + } + } + } + return nil +} + +func ImageNeedsDownload(filename string) bool { + exist, err := FileExists(appConfig.GetFilePath(filename)) + if err != nil { + return false + } + return !exist +} + +func DownloadImage(httpClient *http.Client, url, filename string) error { + fmt.Println("Downloading ", url, " -> ", filename, "...") + imgResp, err := httpClient.Get(url) + if err != nil { + PrintIfVerbose(err.Error(), "\n") + return err + } + defer imgResp.Body.Close() + file, err := os.Create(appConfig.GetFilePath(filename)) + if err != nil { + PrintIfVerbose(err.Error(), "\n") + return err + } + defer file.Close() + _, err = io.Copy(file, imgResp.Body) + if err != nil { + fmt.Println(err.Error(), "\n") + } else { + fmt.Println("Done\n") + } + return err +} + +func PrintIfVerbose(val ...string) { + if appConfig.Verbose { + for _, v := range val { + fmt.Print(v) + } + } +} + +func PrintUsageAndExit() { + fmt.Println("twitstch - Download Images from a Twitter Stream") + fmt.Println(" -count,-c= Pull the last tweets") + fmt.Println(" -force,-f Download images even if the file already exists") + fmt.Println(" -help ,-h View this message") + fmt.Println(" -verbose ,-v Be chatty") + fmt.Println() + os.Exit(0) +}