Initial Commit.

Also, probably, done.
This commit is contained in:
Brian Buller 2020-04-01 14:41:58 -05:00
commit ec4f54cffc
3 changed files with 298 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Binary
twitstch
# Configuration
twitstch.conf

150
app_config.go Normal file
View File

@ -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
}

144
main.go Normal file
View File

@ -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=<num> Pull the last <num> 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)
}