20 Commits
0.2 ... 0.6

Author SHA1 Message Date
3970c611a4 Add --exclude flag (to avoid uploading thumbnails) 2017-02-28 22:50:03 +10:30
d8dc3e4ea8 Fix stupid text 2017-02-28 22:21:22 +10:30
3693d94297 Fix for new version variable name 2017-02-28 22:14:41 +10:30
8ded2b2e2d Document the --no-watermark feature 2017-02-28 22:10:53 +10:30
e3e712d073 Add retries, with backoff 2017-02-28 22:07:57 +10:30
1ecac568f7 Mark watermarked images be uploaded, and make it an optional option. 2017-02-28 21:32:18 +10:30
82ba3be742 Hacky image watermarking (not yet complete) 2017-02-26 21:06:48 +10:30
Justin Hawkins
4825dc56e6 Refactor away some globals. This is probably still not very idiomatic. 2017-02-23 12:55:10 +10:30
65b9241492 Refactor according to lint 2017-02-22 21:13:07 +10:30
Justin Hawkins
73b33f5872 Improve doc 2017-02-22 16:48:29 +10:30
Justin Hawkins
cc0fee57c2 Sub directories are scanned 2017-02-22 16:47:50 +10:30
Justin Hawkins
05a3a0d09a Update README 2017-02-22 16:46:48 +10:30
Justin Hawkins
72588642b6 Fix .gitignore 2017-02-22 15:52:28 +10:30
Justin Hawkins
7ff4685a70 Simple release build script 2017-02-22 15:47:51 +10:30
Justin Hawkins
f6b92ee8bd Show github link in --version 2017-02-22 15:47:26 +10:30
Justin Hawkins
68d9ab7859 Check path before starting to prevent crash. Show id of upload. 2017-02-21 17:10:00 +10:30
Justin Hawkins
d2d7843b6f Show upload rate and speed 2017-02-21 16:22:34 +10:30
Justin Hawkins
13589535a8 Add timeouts for uploads and version check. 2017-02-21 14:57:10 +10:30
Justin Hawkins
cb1f1d1a05 Version and help commands. 2017-02-21 12:28:26 +10:30
Justin Hawkins
699ca9fcfc Add username support, clean up command line parsing, help and output 2017-02-21 12:24:14 +10:30
4 changed files with 388 additions and 185 deletions

24
.gitignore vendored
View File

@@ -1,20 +1,4 @@
/blib/ dist
/.build/ release
_build/ discord-auto-upload
cover_db/ discord-auto-upload.exe
inc/
Build
!Build/
Build.bat
.last_cover_stats
/Makefile
/Makefile.old
/MANIFEST.bak
/META.yml
/META.json
/MYMETA.*
nytprof.out
/pm_to_blib
*.o
*.bs
/_eumm/

View File

@@ -1,4 +1,4 @@
# Automatically upload screenshots from your computer into a discord channel # Automatically upload screenshots into a discord channel
This program automatically uploads new screenshots that appear in a folder on your computer to Discord and posts them in a channel: This program automatically uploads new screenshots that appear in a folder on your computer to Discord and posts them in a channel:
@@ -16,11 +16,16 @@ Point it at your Steam screenshot folder, or similar, and shortly after you hit
### Binaries ### Binaries
TBD Binaries are available for Mac, Linux and Windows [here](https://github.com/tardisx/discord-auto-upload/releases/latest).
Put them somewhere on your path and run from the command line.
The windows version comes with a .bat file to make this a little easier - edit the `dau.bat` file to include your webhook URL and
other parameters, then you can simply double click `dau.bat` to start `dau` running.
#### From source #### From source
TBD You'll need to [download Go](https://golang.org/dl/) check the code out somewhere, and 'go build'.
## Using it ## Using it
@@ -32,8 +37,6 @@ Thus, you do not have to worry about pointing `dau` at a directory full of image
If `dau` is on your path, you can run it from your screenshot folder and there is then no need to specify the path to your images. If `dau` is on your path, you can run it from your screenshot folder and there is then no need to specify the path to your images.
Note that currently `dau` does not look in subdirectories. Please submit an issue if this is a use case for you.
The only two mandatory command line parameters are the discord webhook URL: The only two mandatory command line parameters are the discord webhook URL:
`--webhook URL` - the webhook URL (see [here](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) for details). `--webhook URL` - the webhook URL (see [here](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) for details).
@@ -42,12 +45,23 @@ and the directory to watch:
`--directory /some/path/here` - the directory that screenshots will appear in. `--directory /some/path/here` - the directory that screenshots will appear in.
You will have to quote the path on windows, or anywhere where the directory path contains spaces. You will have to quote the path on windows, or anywhere where the directory path contains spaces. Note that
subdirectories will also be scanned.
Other parameters are: Other parameters are:
`--exclude <string>` - exclude any files that contain this string (commonly used to avoid uploading thumbnails).
`--watch xx` - specify how many seconds to wait between scanning the directory. The default is 10 seconds. `--watch xx` - specify how many seconds to wait between scanning the directory. The default is 10 seconds.
`--username <username>` - an arbitrary string to show as the bot's username in the channel.
`--no-watermark` - don't watermark images with a reference to this tool.
`--help` - show command line help.
`--version` - show the version.
## Limitations/bugs ## Limitations/bugs
* Only files ending jpg, gif or png are uploaded. * Only files ending jpg, gif or png are uploaded.

52
build-release.pl Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env perl
use strict;
use warnings;
open my $fh, "<", "dau.go" || die $!;
my $version;
while (<$fh>) {
$version = $1 if /^const\s+currentVersion.*?"([\d\.]+)"/;
}
close $fh;
die "no version?" unless defined $version;
# so lazy
system "rm", "-rf", "release", "dist";
system "mkdir", "release";
system "mkdir", "dist";
my %build = (
win => { env => { GOOS => 'windows', GOARCH => '386' }, filename => 'dau.exe' },
linux => { env => { GOOS => 'linux', GOARCH => '386' }, filename => 'dau' },
mac => { env => { GOOS => 'darwin', GOARCH => '386' }, filename => 'dau' },
);
foreach my $type (keys %build) {
mkdir "release/$type";
}
add_extras();
foreach my $type (keys %build) {
local $ENV{GOOS} = $build{$type}->{env}->{GOOS};
local $ENV{GOARCH} = $build{$type}->{env}->{GOARCH};
system "go", "build", "-o", "release/$type/" . $build{$type}->{filename};
system "zip", "-j", "dist/dau-$type-$version.zip", ( glob "release/$type/*" );
}
sub add_extras {
# bat file for windows
open (my $fh, ">", "release/win/dau.bat") || die $!;
print $fh 'set WEBHOOK_URL=https://yourdiscordwebhookURLhere' . "\r\n";
print $fh 'set SCREENSHOTS="C:\your\screenshot\directory\here"' ."\r\n";
print $fh 'set USERNAME="Posted by Joe Bloggs"' . "\r\n";
print $fh 'set WATCH=10' . "\r\n";
print $fh 'dau.exe --webhook %WEBHOOK_URL% --directory %SCREENSHOTS% --username %USERNAME% --watch %WATCH%' . "\r\n";
print $fh 'pause' . "\r\n";
close $fh;
}

319
dau.go
View File

@@ -1,137 +1,208 @@
package main package main
import ( import (
"fmt"
"strings"
"github.com/pborman/getopt"
"path/filepath"
"os"
"time"
"net/http"
"log"
"io"
"bytes" "bytes"
"mime/multipart"
"encoding/json" "encoding/json"
"fmt"
"io"
"io/ioutil" "io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"github.com/fogleman/gg"
"github.com/pborman/getopt"
"golang.org/x/image/font/inconsolata"
) )
var current_version = "0.2" const currentVersion = "0.6"
var last_check = time.Now()
var new_last_check = time.Now()
var webhook_url string
type webhook_response struct { var lastCheck = time.Now()
Test string var newLastCheck = time.Now()
}
func keepLines(s string, n int) string { // Config for the application
result := strings.Join(strings.Split(s, "\n")[:n], "\n") type Config struct {
return strings.Replace(result, "\r", "", -1) webhookURL string
path string
watch int
username string
noWatermark bool
exclude string
} }
func main() { func main() {
webhook, path, watch := parse_options()
webhook_url = webhook
check_updates() config := parseOptions()
checkPath(config.path)
checkUpdates()
log.Print("Waiting for images to appear in ", config.path)
// wander the path, forever // wander the path, forever
for { for {
err := filepath.Walk(path, check_file) err := filepath.Walk(config.path,
if err != nil { log.Fatal("oh dear") } func(path string, f os.FileInfo, err error) error { return checkFile(path, f, err, config) })
//fmt.Printf("filepath.Walk() returned %v\n", err) if err != nil {
last_check = new_last_check log.Fatal("could not watch path", err)
time.Sleep(time.Duration(watch)*time.Second) }
lastCheck = newLastCheck
time.Sleep(time.Duration(config.watch) * time.Second)
} }
} }
func check_updates() { func checkPath(path string) {
src, err := os.Stat(path)
if err != nil {
log.Fatal(err)
}
if !src.IsDir() {
log.Fatal(path, " is not a directory")
os.Exit(1)
}
}
func checkUpdates() {
type GithubRelease struct { type GithubRelease struct {
Html_url string HTMLURL string
Tag_name string TagName string
Name string Name string
Body string Body string
} }
resp, err := http.Get("https://api.github.com/repos/tardisx/discord-auto-upload/releases/latest") client := &http.Client{Timeout: time.Second * 5}
if (err != nil) { resp, err := client.Get("https://api.github.com/repos/tardisx/discord-auto-upload/releases/latest")
log.Fatal("could not check for updates") if err != nil {
log.Fatal("could not check for updates:", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if (err != nil) { if err != nil {
log.Fatal("could not check read update response") log.Fatal("could not check read update response")
} }
var latest GithubRelease var latest GithubRelease
err = json.Unmarshal(body, &latest) err = json.Unmarshal(body, &latest)
if (err != nil) { if err != nil {
log.Fatal("could not parse JSON", err) log.Fatal("could not parse JSON: ", err)
} }
if (current_version != latest.Tag_name) { if currentVersion < latest.TagName {
fmt.Println("A new version is available:", latest.Tag_name) fmt.Printf("You are currently on version %s, but version %s is available\n", currentVersion, latest.TagName)
fmt.Println("----------- Release Info -----------") fmt.Println("----------- Release Info -----------")
fmt.Println(latest.Body) fmt.Println(latest.Body)
fmt.Println("------------------------------------") fmt.Println("------------------------------------")
fmt.Println("( You are currently on version:", current_version, ")") fmt.Println("Upgrade at https://github.com/tardisx/discord-auto-upload/releases/latest")
} }
} }
func parseOptions() Config {
func parse_options() (webhook_url string, path string, watch int) { var newConfig Config
// Declare the flags to be used // Declare the flags to be used
// helpFlag := getopt.Bool('h', "display help") webhookFlag := getopt.StringLong("webhook", 'w', "", "discord webhook URL")
webhookFlag := getopt.StringLong("webhook", 'w', "", "webhook URL") pathFlag := getopt.StringLong("directory", 'd', "", "directory to scan, optional, defaults to current directory")
pathFlag := getopt.StringLong("directory", 'd', "", "directory")
watchFlag := getopt.Int16Long("watch", 's', 10, "time between scans") watchFlag := getopt.Int16Long("watch", 's', 10, "time between scans")
usernameFlag := getopt.StringLong("username", 'u', "", "username for the bot upload")
excludeFlag := getopt.StringLong("exclude", 'x', "", "exclude files containing this string")
noWatermarkFlag := getopt.BoolLong("no-watermark", 'n', "do not put a watermark on images before uploading")
helpFlag := getopt.BoolLong("help", 'h', "help")
versionFlag := getopt.BoolLong("version", 'v', "show version")
getopt.SetParameters("")
getopt.Parse() getopt.Parse()
return *webhookFlag, *pathFlag, int(*watchFlag) if *helpFlag {
} getopt.PrintUsage(os.Stderr)
os.Exit(0)
func check_file(path string, f os.FileInfo, err error) error {
// fmt.Println("Comparing", f.ModTime(), "to", last_check, "for", path)
if f.ModTime().After(last_check) && f.Mode().IsRegular() {
if file_eligible(path) {
// process file
process_file(path)
} }
if new_last_check.Before(f.ModTime()) { if *versionFlag {
new_last_check = f.ModTime() fmt.Println("dau - https://github.com/tardisx/discord-auto-upload")
fmt.Printf("Version: %s\n", currentVersion)
os.Exit(0)
}
if !getopt.IsSet("directory") {
*pathFlag = "./"
log.Println("Defaulting to current directory")
}
if !getopt.IsSet("webhook") {
log.Fatal("ERROR: You must specify a --webhook URL")
}
newConfig.path = *pathFlag
newConfig.webhookURL = *webhookFlag
newConfig.watch = int(*watchFlag)
newConfig.username = *usernameFlag
newConfig.noWatermark = *noWatermarkFlag
newConfig.exclude = *excludeFlag
return newConfig
}
func checkFile(path string, f os.FileInfo, err error, config Config) error {
if f.ModTime().After(lastCheck) && f.Mode().IsRegular() {
if fileEligible(config, path) {
// process file
processFile(config, path)
}
if newLastCheck.Before(f.ModTime()) {
newLastCheck = f.ModTime()
} }
} }
return nil return nil
} }
func file_eligible(file string) (bool) { func fileEligible(config Config, file string) bool {
if config.exclude != "" && strings.Contains(file, config.exclude) {
return false
}
extension := strings.ToLower(filepath.Ext(file)) extension := strings.ToLower(filepath.Ext(file))
if extension == ".png" || extension == ".jpg" || extension == ".gif" { if extension == ".png" || extension == ".jpg" || extension == ".gif" {
return true return true
} }
return false return false
} }
func process_file(file string) { func processFile(config Config, file string) {
if !config.noWatermark {
log.Print("Copying to temp location and watermarking ", file)
file = mungeFile(file)
}
log.Print("Uploading ", file) log.Print("Uploading ", file)
extraParams := map[string]string{ extraParams := map[string]string{}
// "username": "Some username",
if config.username != "" {
extraParams["username"] = config.username
} }
type DiscordAPIResponseAttachment struct { type DiscordAPIResponseAttachment struct {
Url string URL string
Proxy_url string ProxyURL string
Size int Size int
Width int Width int
Height int Height int
@@ -140,48 +211,84 @@ func process_file(file string) {
type DiscordAPIResponse struct { type DiscordAPIResponse struct {
Attachments []DiscordAPIResponseAttachment Attachments []DiscordAPIResponseAttachment
id int64 ID int64 `json:",string"`
} }
request, err := newfileUploadRequest(webhook_url, extraParams, "file", file) var retriesRemaining = 5
for retriesRemaining > 0 {
request, err := newfileUploadRequest(config.webhookURL, extraParams, "file", file)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
client := &http.Client{} start := time.Now()
client := &http.Client{Timeout: time.Second * 30}
resp, err := client.Do(request) resp, err := client.Do(request)
if err != nil { if err != nil {
log.Print("Error performing request:", err)
log.Fatal("Error performing request:", err) retriesRemaining--
sleepForRetries(retriesRemaining)
continue
} else { } else {
if (resp.StatusCode != 200) { if resp.StatusCode != 200 {
log.Print("Bad response from server:", resp.StatusCode) log.Print("Bad response from server:", resp.StatusCode)
return retriesRemaining--
sleepForRetries(retriesRemaining)
continue
} }
res_body, err := ioutil.ReadAll(resp.Body) resBody, err := ioutil.ReadAll(resp.Body)
if (err != nil) { if err != nil {
log.Fatal("could not deal with body", err) log.Print("could not deal with body: ", err)
retriesRemaining--
sleepForRetries(retriesRemaining)
continue
} }
resp.Body.Close() resp.Body.Close()
var res DiscordAPIResponse var res DiscordAPIResponse
err = json.Unmarshal(res_body, &res) err = json.Unmarshal(resBody, &res)
if (err != nil) { if err != nil {
log.Fatal("could not parse JSON", err) log.Print("could not parse JSON: ", err)
fmt.Println("Response was:", res_body) fmt.Println("Response was:", string(resBody[:]))
return retriesRemaining--
sleepForRetries(retriesRemaining)
continue
} }
if (len(res.Attachments) < 1) { if len(res.Attachments) < 1 {
log.Print("bad response - no attachments?") log.Print("bad response - no attachments?")
return retriesRemaining--
sleepForRetries(retriesRemaining)
continue
} }
var a = res.Attachments[0] var a = res.Attachments[0]
log.Printf("Uploaded to %s %dx%d, %d bytes\n", a.Url, a.Width, a.Height, a.Size) elapsed := time.Since(start)
rate := float64(a.Size) / elapsed.Seconds() / 1024.0
log.Printf("Uploaded to %s %dx%d", a.URL, a.Width, a.Height)
log.Printf("id: %d, %d bytes transferred in %.2f seconds (%.2f KiB/s)", res.ID, a.Size, elapsed.Seconds(), rate)
break
}
} }
if !config.noWatermark {
log.Print("Removing temporary file ", file)
os.Remove(file)
}
if retriesRemaining == 0 {
log.Fatal("Failed to upload, even after retries")
}
}
func sleepForRetries(retry int) {
if retry == 0 {
return
}
retryTime := (6-retry)*(6-retry) + 6
log.Printf("Will retry in %d seconds (%d remaining attempts)", retryTime, retry)
// time.Sleep(time.Duration(retryTime) * time.Second)
} }
func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) { func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) {
@@ -198,6 +305,9 @@ func newfileUploadRequest(uri string, params map[string]string, paramName, path
return nil, err return nil, err
} }
_, err = io.Copy(part, file) _, err = io.Copy(part, file)
if err != nil {
log.Fatal("Could not copy: ", err)
}
for key, val := range params { for key, val := range params {
_ = writer.WriteField(key, val) _ = writer.WriteField(key, val)
@@ -211,3 +321,46 @@ func newfileUploadRequest(uri string, params map[string]string, paramName, path
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
return req, err return req, err
} }
func mungeFile(path string) string {
reader, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
defer reader.Close()
im, _, err := image.Decode(reader)
if err != nil {
log.Fatal(err)
}
bounds := im.Bounds()
// var S float64 = float64(bounds.Max.X)
dc := gg.NewContext(bounds.Max.X, bounds.Max.Y)
dc.Clear()
dc.SetRGB(0, 0, 0)
dc.SetFontFace(inconsolata.Regular8x16)
dc.DrawImage(im, 0, 0)
dc.DrawRoundedRectangle(0, float64(bounds.Max.Y-18.0), 320, float64(bounds.Max.Y), 0)
dc.SetRGB(0, 0, 0)
dc.Fill()
dc.SetRGB(1, 1, 1)
dc.DrawString("github.com/tardisx/discord-auto-upload", 5.0, float64(bounds.Max.Y)-5.0)
tempfile, err := ioutil.TempFile("", "dau")
if err != nil {
log.Fatal(err)
}
tempfile.Close()
os.Remove(tempfile.Name())
actualName := tempfile.Name() + ".png"
dc.SavePNG(actualName)
return actualName
}