41 Commits
0.5 ... 0.9

Author SHA1 Message Date
d23e31c0e0 Bump version 2021-06-04 10:42:26 +09:30
9cb79a846e Umm, how did this ever work? 😫 2021-06-04 10:41:23 +09:30
a5ce0c7f63 Documenation update 2021-06-03 19:42:12 +09:30
bcc4e145a2 Limit number of log entries stored 2021-06-03 19:40:53 +09:30
d8c0b7d0ea Fix error popup 2021-06-03 19:40:43 +09:30
fdf70daba7 Improve log display, use a <pre> so it can be easily cut and pasted. 2021-06-03 19:36:48 +09:30
b69cdebf3b Send logs to web server for display there 2021-06-02 23:42:29 +09:30
9e22490fe2 Merge pull request #10 from NoahBohme/master
Open discord link in a new tab
2021-03-15 23:30:56 +10:30
NoahBohme
ae24f16631 Delete .github/workflows directory 2021-03-15 11:51:07 +01:00
NoahBohme
b69acac0d0 Create go.yml 2021-02-23 09:46:47 +01:00
noah
c833f185cc add link for discord webhook 2021-02-23 08:23:10 +01:00
noah
b3ee0d9d1d Open discord link in a new tab 2021-02-23 08:16:59 +01:00
23c0aa2a34 Add Exclude to the config as well. 2021-02-09 22:07:40 +10:30
2d1ac3c803 Clean up logging and improve web interface 2021-02-07 22:06:19 +10:30
942f81a378 Bump version 2021-02-07 22:05:49 +10:30
287efab257 We no longer create a .bat file. 2021-02-07 22:05:16 +10:30
2b159e5532 Update README for the new version 2021-02-07 22:05:01 +10:30
851f073e99 Read/Write config to $HOME 2021-02-07 11:42:13 +10:30
ec658520b7 go fmt 2021-02-07 08:10:27 +10:30
6b1867f35f Support noWatermark config 2021-02-07 08:10:06 +10:30
6e493522c8 Add username and watch period web configuration 2021-02-06 12:50:44 +10:30
4d09901fb3 Fix up the javascript for loading config data. 2021-02-03 21:43:23 +10:30
9c9d4e492a Wrapper for HTML pages and start of the config web interface 2021-02-02 22:09:18 +10:30
e1f5afa788 go fmt 2021-01-31 18:48:48 +10:30
46a0f5a187 Rework configuration to it's own package and make it available to web server. Start a template driven web interface. 2021-01-31 17:53:32 +10:30
55bb5a8bae Fix build instructions 2021-01-31 09:56:09 +10:30
c2b9bf410d Bring into 2021 2021-01-30 21:23:58 +10:30
a4f958f846 Continue web server integration 2020-03-26 11:40:35 +10:30
14f8fe1933 Fix go generate call 2020-03-20 05:51:25 +10:30
752ff42a19 Add asset generation. 2020-03-20 05:49:12 +10:30
1ef062d19c Generate assets when releasing 2017-07-27 22:11:15 +09:30
6f09841209 Ignore generated asset directory 2017-07-27 22:11:03 +09:30
4619bb5383 Automatically open web browser on startup 2017-07-27 22:04:47 +09:30
e240f5dbd0 Embedded assets 2017-07-27 22:04:32 +09:30
Justin Hawkins
f976777f40 Start fleshing out web server 2017-07-27 12:18:02 +09:30
80a905b7d6 Improve update check error and make it non-fatal 2017-07-26 22:44:56 +09:30
497d2e3e27 Basic web server startup 2017-07-26 22:40:21 +09:30
ab54ace0d2 Add empty web class for the upcoming build-in web server 2017-07-26 14:24:02 +09:30
450765145b Add note about existing file handling 2017-07-26 13:24:07 +09:30
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
14 changed files with 936 additions and 110 deletions

3
.gitignore vendored
View File

@@ -2,3 +2,6 @@ dist
release release
discord-auto-upload discord-auto-upload
discord-auto-upload.exe discord-auto-upload.exe
assets
*.png
*.jpg

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:
@@ -18,54 +18,75 @@ Point it at your Steam screenshot folder, or similar, and shortly after you hit
Binaries are available for Mac, Linux and Windows [here](https://github.com/tardisx/discord-auto-upload/releases/latest). 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
You'll need to [download Go](https://golang.org/dl/) check the code out somewhere, and 'go build'. You'll need to [download Go](https://golang.org/dl/) check the code out somewhere, run 'go generate' and then 'go build'.
## Using it ## Using it
`dau` is a command line driven program. When executed, it will continually scan a directory for new images, and each time it finds one it will upload it to discord, via the discord web hook. `dau` configuration is managed via its internal web interface. When the executable is run, you can visit
`http://localhost:9090` in your web browser to configure the it. Configuration persists across runs, it is
saved in a file called '.dau.json' in your home directory.
The first time you run it, you will need to configure at least the discord web hook and the watch path for
`dau` to be useful.
While running, `dau` will continually scan a directory for new images, and each time it finds one it will upload it to discord, via the discord web hook.
`dau` will only upload "new" screenshots, where "new" means a file that appears in a directory that it is watching, if it appears *after* it has started executing. `dau` will only upload "new" screenshots, where "new" means a file that appears in a directory that it is watching, if it appears *after* it has started executing.
Thus, you do not have to worry about pointing `dau` at a directory full of images, it will only upload new ones. Thus, you do not have to worry about pointing `dau` at a directory full of images, it will only upload new ones.
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. ## Configuration options
The only two mandatory command line parameters are the discord webhook URL: See the web interface at http://localhost:9090 to configure `dau`.
`--webhook URL` - the webhook URL (see [here](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) for details). ### 'Discord WebHook URL'
and the directory to watch: The webhook URL from Discord. See https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks
for more information on setting one up.
`--directory /some/path/here` - the directory that screenshots will appear in. ### 'Bot Username'
You will have to quote the path on windows, or anywhere where the directory path contains spaces. Note that This is completely optional and can be any arbitrary string. It makes the upload
subdirectories will also be scanned. appear to come from a different user (though this is visual only, and does not
actually hide the bot identity in any way). You might like to set it to your own
discord name.
Other parameters are: ### 'Directory to watch'
`--watch xx` - specify how many seconds to wait between scanning the directory. The default is 10 seconds. This is the path that `dau` will periodically inspect, looking for new images.
Note that subdirectories are also scanned. You need to enter the full filesystem
path here.
`--username <username>` - an arbitrary string to show as the bot's username in the channel. ### 'Period between filesystem checks'
`--no-watermark` - don't watermark images with a reference to this tool. This is the number of seconds between which `dau` will look for new images.
`--help` - show command line help. ### 'Do not watermark images'
`--version` - show the version. This will disable the watermarking of images. I like it when you don't set this :-)
### 'Files to exclude'
This is a string to match against the filename to check for exclusions. The common
use case is to use 'thumbnail' or similar if your image directory contains additional
thumbnail files.
## Limitations/bugs ## Limitations/bugs
* Only files ending jpg, gif or png are uploaded. * Only files ending jpg, gif or png are uploaded.
* If multiple screenshots occur quickly (<1 second apart) not all may be uploaded. * If multiple screenshots occur quickly (<1 second apart) not all may be uploaded.
* Files to upload are determined by the file modification time. If you drag and drop existing files they will
not be detected and uploaded. Only newly created files will be detected.
## Troubleshooting
Please check the "log" page on the web interface for information when things are
not working as you expect.
## TODO ## TODO
This is just a quick hack. Open to suggestions on new features and improvements. This is just a relatively quick hack. Open to suggestions on new features and improvements.
Open an [issue](https://github.com/tardisx/discord-auto-upload/issues/new) and let me know. Open an [issue](https://github.com/tardisx/discord-auto-upload/issues/new) and let me know.
Please include any relevant logs from the console when reporting bugs.

View File

@@ -3,11 +3,11 @@
use strict; use strict;
use warnings; use warnings;
open my $fh, "<", "dau.go" || die $!; open my $fh, "<", "config/config.go" || die $!;
my $version; my $version;
while (<$fh>) { while (<$fh>) {
$version = $1 if /^const\s+currentVersion.*?"([\d\.]+)"/; $version = $1 if /^const\s+CurrentVersion.*?"([\d\.]+)"/;
} }
close $fh; close $fh;
@@ -19,9 +19,9 @@ system "mkdir", "release";
system "mkdir", "dist"; system "mkdir", "dist";
my %build = ( my %build = (
win => { env => { GOOS => 'windows', GOARCH => '386' }, filename => 'dau.exe' }, win => { env => { GOOS => 'windows', GOARCH => 'amd64' }, filename => 'dau.exe' },
linux => { env => { GOOS => 'linux', GOARCH => '386' }, filename => 'dau' }, linux => { env => { GOOS => 'linux', GOARCH => 'amd64' }, filename => 'dau' },
mac => { env => { GOOS => 'darwin', GOARCH => '386' }, filename => 'dau' }, mac => { env => { GOOS => 'darwin', GOARCH => 'amd64' }, filename => 'dau' },
); );
foreach my $type (keys %build) { foreach my $type (keys %build) {
@@ -30,6 +30,7 @@ foreach my $type (keys %build) {
add_extras(); add_extras();
system(qw{go generate});
foreach my $type (keys %build) { foreach my $type (keys %build) {
local $ENV{GOOS} = $build{$type}->{env}->{GOOS}; local $ENV{GOOS} = $build{$type}->{env}->{GOOS};
local $ENV{GOARCH} = $build{$type}->{env}->{GOARCH}; local $ENV{GOARCH} = $build{$type}->{env}->{GOARCH};
@@ -38,15 +39,5 @@ foreach my $type (keys %build) {
} }
sub add_extras { sub add_extras {
# bat file for windows # we used to have a .bat file here, but no longer needed
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;
} }

78
config/config.go Normal file
View File

@@ -0,0 +1,78 @@
package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
daulog "github.com/tardisx/discord-auto-upload/log"
"github.com/mitchellh/go-homedir"
)
// Config for the application
var Config struct {
WebHookURL string
Path string
Watch int
Username string
NoWatermark bool
Exclude string
}
const CurrentVersion string = "0.9"
// Load the current config or initialise with defaults
func LoadOrInit() {
configPath := configPath()
daulog.SendLog(fmt.Sprintf("Trying to load config from %s", configPath), daulog.LogTypeDebug)
_, err := os.Stat(configPath)
if os.IsNotExist(err) {
daulog.SendLog("NOTE: No config file, writing out sample configuration", daulog.LogTypeInfo)
daulog.SendLog("You need to set the configuration via the web interface", daulog.LogTypeInfo)
Config.WebHookURL = ""
Config.Path = homeDir() + string(os.PathSeparator) + "screenshots"
Config.Watch = 10
SaveConfig()
} else {
LoadConfig()
}
}
func LoadConfig() {
path := configPath()
data, err := ioutil.ReadFile(path)
if err != nil {
log.Fatalf("cannot read config file %s: %s", path, err.Error())
}
err = json.Unmarshal([]byte(data), &Config)
if err != nil {
log.Fatalf("cannot decode config file %s: %s", path, err.Error())
}
}
func SaveConfig() {
daulog.SendLog("saving configuration", daulog.LogTypeInfo)
path := configPath()
jsonString, _ := json.Marshal(Config)
err := ioutil.WriteFile(path, jsonString, os.ModePerm)
if err != nil {
log.Fatalf("Cannot save config %s: %s", path, err.Error())
}
}
func homeDir() string {
dir, err := homedir.Dir()
if err != nil {
panic(err)
}
return dir
}
func configPath() string {
homeDir := homeDir()
return homeDir + string(os.PathSeparator) + ".dau.json"
}

156
data/config.html Normal file
View File

@@ -0,0 +1,156 @@
<main role="main" class="inner DAU">
<h1 class="DAU-heading">Config</h1>
<p class="lead">Discord-auto-upload configuration</p>
<a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><p class="lead">How to find your discord webhook</p></a>
<form class="">
<div class="form-row align-items-center config-item" data-key="webhook">
<div class="col-sm-5 my-1">
<span>Discord WebHook URL</span>
</div>
<div class="col-sm-4 my-1">
<label class="sr-only" for="inlineFormInputName">Name</label>
<input type="text" class="form-control rest-field" placeholder="https://....">
</div>
<div class="col-auto my-1">
<button type="submit" class="btn btn-primary">update</button>
</div>
</div>
</form>
<form class="">
<div class="form-row align-items-center config-item" data-key="username">
<div class="col-sm-5 my-1">
<span>Bot username (optional)</span>
</div>
<div class="col-sm-4 my-1">
<label class="sr-only" for="inlineFormInputName">Name</label>
<input type="text" class="form-control rest-field" placeholder="">
</div>
<div class="col-auto my-1">
<button type="submit" class="btn btn-primary">update</button>
</div>
</div>
</form>
<form class="">
<div class="form-row align-items-center config-item" data-key="directory">
<div class="col-sm-5 my-1">
<span>Directory to watch</span>
</div>
<div class="col-sm-4 my-1">
<label class="sr-only" for="inlineFormInputName">Name</label>
<input type="text" class="form-control rest-field" placeholder="/...">
</div>
<div class="col-auto my-1">
<button type="submit" class="btn btn-primary">update</button>
</div>
</div>
</form>
<form class="">
<div class="form-row align-items-center config-item" data-key="watch">
<div class="col-sm-5 my-1">
<span>Period between filesystem checks (seconds)</span>
</div>
<div class="col-sm-4 my-1">
<label class="sr-only" for="inlineFormInputName">Seconds</label>
<input type="text" class="form-control rest-field " placeholder="/...">
</div>
<div class="col-auto my-1">
<button type="submit" class="btn btn-primary">update</button>
</div>
</div>
</form>
<form class="">
<div class="form-row align-items-center config-item" data-key="nowatermark">
<div class="col-sm-5 my-1">
<span>Do not watermark images</span>
</div>
<div class="col-sm-4 my-1">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input rest-field rest-field-boolean" id="input-nowatermark">
<label class="custom-control-label" for="input-nowatermark">&nbsp;</label>
<span id="sadness" style="">😭</span>
</div>
</div>
<div class="col-auto my-1">
<button type="submit" class="btn btn-primary">update</button>
</div>
</div>
</form>
<form class="">
<div class="form-row align-items-center config-item" data-key="exclude">
<div class="col-sm-5 my-1">
<span>Files to exclude</span>
</div>
<div class="col-sm-4 my-1">
<label class="sr-only" for="input-exclude">Name</label>
<input type="text" id="input-exclude" class="form-control rest-field" placeholder="">
</div>
<div class="col-auto my-1">
<button type="submit" class="btn btn-primary">update</button>
</div>
</div>
</form>
</main>
<script>
function update_sadness () {
if ($('#input-nowatermark').prop('checked')) {
$('#sadness').css('visibility','');
}
else {
$('#sadness').css('visibility','hidden');
}
}
$(document).ready(function() {
$('#input-nowatermark').on('click', function() { update_sadness(); });
// populate each field
$('.config-item').each(function() {
let el = $(this);
let key = el.data('key');
$.ajax({ method: 'get', url: '/rest/config/'+key})
.done(function(data) {
var this_el = $(".config-item[data-key='"+key+"']").find('.rest-field');
if (this_el.hasClass('rest-field-boolean')) {
this_el.prop('checked', data.value);
}
else {
this_el.val(data.value);
}
update_sadness();
});
});
// respond to button clicks to update
$('.config-item button').on('click', function(e,f) {
key = $(this).parents('.config-item').data('key');
val = $(this).parents('.config-item').find('.rest-field').val();
if ($(this).parents('.config-item').find('.rest-field-boolean').length) {
val = $(this).parents('.config-item').find('.rest-field').prop('checked') ? 1 : 0;
}
$.post('/rest/config/'+key, { value: val })
.done(function(d) {
if (d.success) {
alert('Updated config');
} else {
alert("Error: " + d.error);
}
});
return false;
});
});
</script>

110
data/dau.css Normal file
View File

@@ -0,0 +1,110 @@
/*
* Globals
*/
/* Links */
a,
a:focus,
a:hover {
color: #fff;
}
/* Custom default button */
.btn-secondary,
.btn-secondary:hover,
.btn-secondary:focus {
color: #333;
text-shadow: none; /* Prevent inheritance from `body` */
background-color: #fff;
border: .05rem solid #fff;
}
/*
* Base structure
*/
html,
body {
height: 100%;
background-color: #333;
}
body {
display: -ms-flexbox;
display: flex;
color: #fff;
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
}
.DAU-container {
max-width: 42em;
}
pre {
background-color: black;
color: aliceblue;
}
/*
* Header
*/
.masthead {
margin-bottom: 2rem;
}
.masthead-brand {
margin-bottom: 0;
}
.nav-masthead .nav-link {
padding: .25rem 0;
font-weight: 700;
color: rgba(255, 255, 255, .5);
background-color: transparent;
border-bottom: .25rem solid transparent;
}
.nav-masthead .nav-link:hover,
.nav-masthead .nav-link:focus {
border-bottom-color: rgba(255, 255, 255, .25);
}
.nav-masthead .nav-link + .nav-link {
margin-left: 1rem;
}
.nav-masthead .active {
color: #fff;
border-bottom-color: #fff;
}
@media (min-width: 48em) {
.masthead-brand {
float: left;
}
.nav-masthead {
float: right;
}
}
/*
* Cover
*/
.cover {
padding: 0 1.5rem;
}
.cover .btn-lg {
padding: .75rem 1.25rem;
font-weight: 700;
}
/*
* Footer
*/
.mastfoot {
color: rgba(255, 255, 255, .5);
}

8
data/index.html Normal file
View File

@@ -0,0 +1,8 @@
<main role="main" class="inner DAU">
<h1 class="DAU-heading">Discord Auto Upload</h1>
<p class="lead">Hey look, it's DAU :-)</p>
<p class="lead">
<a href="https://github.com/tardisx/discord-auto-upload" class="btn btn-lg btn-secondary" target="_blank">Learn more</a>
</p>
</main>

18
data/logs.html Normal file
View File

@@ -0,0 +1,18 @@
<main role="main" class="inner DAU">
<h1 class="DAU-heading">Config</h1>
<p class="lead">Discord-auto-upload logs</p>
<pre id="logs" class="text-left pre-scrollable">
</pre>
</main>
<script>
$(document).ready(function() {
$.ajax({ method: 'get', url: '/rest/logs'})
.done(function(data) {
console.log(data);
$('#logs').text(data);
});
});
</script>

59
data/wrapper.tmpl Normal file
View File

@@ -0,0 +1,59 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="/dau.css" rel="stylesheet">
</head>
<body class="text-center">
<div class="DAU-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">discord-auto-upload ({{.Version}})</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link {{ if eq .Path "index.html"}} active {{ end }}" href="/">Home</a>
<a class="nav-link {{ if eq .Path "config.html"}} active {{ end }}" href="/config.html">Config</a>
<a class="nav-link {{ if eq .Path "logs.html"}} active {{ end }}" href="/logs.html">Logs</a>
</nav>
</div>
</header>
{{.Body}}
<footer class="mastfoot mt-auto">
<div class="inner">
<!-- <p>DAU template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p> -->
</div>
</footer>
</div>
</body>
</html>

143
dau.go
View File

@@ -1,5 +1,7 @@
package main package main
//go:generate go-bindata -pkg assets -o assets/static.go -prefix data/ data
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
@@ -22,67 +24,73 @@ import (
"github.com/fogleman/gg" "github.com/fogleman/gg"
"github.com/pborman/getopt" "github.com/pborman/getopt"
"golang.org/x/image/font/inconsolata"
)
const currentVersion = "0.5" // "github.com/skratchdot/open-golang/open"
"golang.org/x/image/font/inconsolata"
"github.com/tardisx/discord-auto-upload/config"
daulog "github.com/tardisx/discord-auto-upload/log"
"github.com/tardisx/discord-auto-upload/web"
)
var lastCheck = time.Now() var lastCheck = time.Now()
var newLastCheck = time.Now() var newLastCheck = time.Now()
// Config for the application
type Config struct {
webhookURL string
path string
watch int
username string
noWatermark bool
}
func main() { func main() {
config := parseOptions() parseOptions()
// log.Print("Opening web browser")
// open.Start("http://localhost:9090")
web.StartWebServer()
checkPath(config.path)
checkUpdates() checkUpdates()
log.Print("Waiting for images to appear in ", config.path) daulog.SendLog(fmt.Sprintf("Waiting for images to appear in %s", config.Config.Path), daulog.LogTypeInfo)
// wander the path, forever // wander the path, forever
for { for {
err := filepath.Walk(config.path, if checkPath(config.Config.Path) {
func(path string, f os.FileInfo, err error) error { return checkFile(path, f, err, config) }) err := filepath.Walk(config.Config.Path,
func(path string, f os.FileInfo, err error) error { return checkFile(path, f, err) })
if err != nil { if err != nil {
log.Fatal("could not watch path", err) log.Fatal("could not watch path", err)
} }
lastCheck = newLastCheck lastCheck = newLastCheck
time.Sleep(time.Duration(config.watch) * time.Second) }
daulog.SendLog(fmt.Sprintf("sleeping for %ds before next check of %s", config.Config.Watch, config.Config.Path), daulog.LogTypeDebug)
time.Sleep(time.Duration(config.Config.Watch) * time.Second)
} }
} }
func checkPath(path string) { func checkPath(path string) bool {
src, err := os.Stat(path) src, err := os.Stat(path)
if err != nil { if err != nil {
log.Fatal(err) log.Printf("Problem with path '%s': %s", path, err)
return false
} }
if !src.IsDir() { if !src.IsDir() {
log.Fatal(path, " is not a directory") log.Printf("Problem with path '%s': is not a directory", path)
os.Exit(1) return false
} }
return true
} }
func checkUpdates() { func checkUpdates() {
type GithubRelease struct { type GithubRelease struct {
HTMLURL string HTMLURL string `json:"html_url"`
TagName string TagName string `json:"tag_name"`
Name string Name string `json:"name"`
Body string Body string `json:"body"`
} }
daulog.SendLog("checking for new version", daulog.LogTypeInfo)
client := &http.Client{Timeout: time.Second * 5} client := &http.Client{Timeout: time.Second * 5}
resp, err := client.Get("https://api.github.com/repos/tardisx/discord-auto-upload/releases/latest") resp, err := client.Get("https://api.github.com/repos/tardisx/discord-auto-upload/releases/latest")
if err != nil { if err != nil {
log.Fatal("could not check for updates:", err) daulog.SendLog(fmt.Sprintf("WARNING: Update check failed: %v", err), daulog.LogTypeError)
return
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
@@ -97,24 +105,20 @@ func checkUpdates() {
log.Fatal("could not parse JSON: ", err) log.Fatal("could not parse JSON: ", err)
} }
if currentVersion < latest.TagName { if config.CurrentVersion < latest.TagName {
fmt.Printf("You are currently on version %s, but version %s is available\n", currentVersion, latest.TagName) fmt.Printf("You are currently on version %s, but version %s is available\n", config.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("Upgrade at https://github.com/tardisx/discord-auto-upload/releases/latest")
daulog.SendLog(fmt.Sprintf("New version available: %s - download at https://github.com/tardisx/discord-auto-upload/releases/latest", latest.TagName), daulog.LogTypeInfo)
} }
} }
func parseOptions() Config { func parseOptions() {
var newConfig Config
// Declare the flags to be used // Declare the flags to be used
webhookFlag := getopt.StringLong("webhook", 'w', "", "discord webhook URL")
pathFlag := getopt.StringLong("directory", 'd', "", "directory to scan, optional, defaults to current directory")
watchFlag := getopt.Int16Long("watch", 's', 10, "time between scans")
usernameFlag := getopt.StringLong("username", 'u', "", "username for the bot upload")
noWatermarkFlag := getopt.BoolLong("no-watermark", 'n', "do not put a watermark on images before uploading")
helpFlag := getopt.BoolLong("help", 'h', "help") helpFlag := getopt.BoolLong("help", 'h', "help")
versionFlag := getopt.BoolLong("version", 'v', "show version") versionFlag := getopt.BoolLong("version", 'v', "show version")
getopt.SetParameters("") getopt.SetParameters("")
@@ -128,35 +132,20 @@ func parseOptions() Config {
if *versionFlag { if *versionFlag {
fmt.Println("dau - https://github.com/tardisx/discord-auto-upload") fmt.Println("dau - https://github.com/tardisx/discord-auto-upload")
fmt.Printf("Version: %s\n", currentVersion) fmt.Printf("Version: %s\n", config.CurrentVersion)
os.Exit(0) os.Exit(0)
} }
if !getopt.IsSet("directory") { // grab the config
*pathFlag = "./" config.LoadOrInit()
log.Println("Defaulting to current directory")
} }
if !getopt.IsSet("webhook") { func checkFile(path string, f os.FileInfo, err error) error {
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
return newConfig
}
func checkFile(path string, f os.FileInfo, err error, config Config) error {
if f.ModTime().After(lastCheck) && f.Mode().IsRegular() { if f.ModTime().After(lastCheck) && f.Mode().IsRegular() {
if fileEligible(config, path) { if fileEligible(path) {
// process file // process file
processFile(config, path) processFile(path)
} }
if newLastCheck.Before(f.ModTime()) { if newLastCheck.Before(f.ModTime()) {
@@ -167,27 +156,39 @@ func checkFile(path string, f os.FileInfo, err error, config Config) error {
return nil return nil
} }
func fileEligible(config Config, file string) bool { func fileEligible(file string) bool {
if config.Config.Exclude != "" && strings.Contains(file, config.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 processFile(config Config, file string) { func processFile(file string) {
if !config.noWatermark { if !config.Config.NoWatermark {
log.Print("Copying to temp location and watermarking ", file) daulog.SendLog("Copying to temp location and watermarking ", daulog.LogTypeInfo)
file = mungeFile(file) file = mungeFile(file)
} }
log.Print("Uploading ", file) if config.Config.WebHookURL == "" {
daulog.SendLog("WebHookURL is not configured - cannot upload!", daulog.LogTypeError)
return
}
daulog.SendLog("Uploading", daulog.LogTypeInfo)
extraParams := map[string]string{} extraParams := map[string]string{}
if config.username != "" { if config.Config.Username != "" {
extraParams["username"] = config.username log.Print("Overriding username with " + config.Config.Username)
extraParams["username"] = config.Config.Username
} }
type DiscordAPIResponseAttachment struct { type DiscordAPIResponseAttachment struct {
@@ -206,7 +207,8 @@ func processFile(config Config, file string) {
var retriesRemaining = 5 var retriesRemaining = 5
for retriesRemaining > 0 { for retriesRemaining > 0 {
request, err := newfileUploadRequest(config.webhookURL, extraParams, "file", file)
request, err := newfileUploadRequest(config.Config.WebHookURL, extraParams, "file", file)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -222,6 +224,9 @@ func processFile(config Config, file string) {
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Print("Bad response from server:", resp.StatusCode) log.Print("Bad response from server:", resp.StatusCode)
if b, err := ioutil.ReadAll(resp.Body); err == nil {
log.Print("Body:", string(b))
}
retriesRemaining-- retriesRemaining--
sleepForRetries(retriesRemaining) sleepForRetries(retriesRemaining)
continue continue
@@ -262,8 +267,8 @@ func processFile(config Config, file string) {
} }
} }
if !config.noWatermark { if !config.Config.NoWatermark {
log.Print("Removing temporary file ", file) daulog.SendLog(fmt.Sprintf("Removing temporary file: %s", file), daulog.LogTypeDebug)
os.Remove(file) os.Remove(file)
} }
@@ -277,8 +282,8 @@ func sleepForRetries(retry int) {
return return
} }
retryTime := (6-retry)*(6-retry) + 6 retryTime := (6-retry)*(6-retry) + 6
log.Printf("Will retry in %d seconds (%d remaining attempts)", retryTime, retry) daulog.SendLog(fmt.Sprintf("Will retry in %d seconds (%d remaining attempts)", retryTime, retry), daulog.LogTypeError)
// time.Sleep(time.Duration(retryTime) * time.Second) 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) {

12
go.mod Normal file
View File

@@ -0,0 +1,12 @@
module github.com/tardisx/discord-auto-upload
go 1.15
require (
github.com/fogleman/gg v1.3.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/pborman/getopt v1.1.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
golang.org/x/image v0.0.0-20201208152932-35266b937fa6
)

13
go.sum Normal file
View File

@@ -0,0 +1,13 @@
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

45
log/log.go Normal file
View File

@@ -0,0 +1,45 @@
package log
import (
"time"
)
type LogEntryType string
type LogEntry struct {
Timestamp time.Time `json:"ts"`
Type LogEntryType `json:"type"`
Entry string `json:"log"`
}
const (
LogTypeInfo = "info"
LogTypeError = "error"
LogTypeDebug = "debug"
)
var LogEntries []LogEntry
var logInput chan LogEntry
func init() {
// wait for log entries
logInput = make(chan LogEntry)
go func() {
for {
aLog := <-logInput
LogEntries = append(LogEntries, aLog)
for len(LogEntries) > 100 {
LogEntries = LogEntries[1:]
}
}
}()
}
func SendLog(entry string, entryType LogEntryType) {
logInput <- LogEntry{
Timestamp: time.Now(),
Entry: entry,
Type: entryType,
}
}

307
web/server.go Normal file
View File

@@ -0,0 +1,307 @@
package web
import (
"encoding/json"
"fmt"
"log"
"mime"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"text/template"
"github.com/tardisx/discord-auto-upload/assets"
"github.com/tardisx/discord-auto-upload/config"
daulog "github.com/tardisx/discord-auto-upload/log"
)
// DAUWebServer - stuff for the web server
type DAUWebServer struct {
ConfigChange chan int
}
type valueStringResponse struct {
Success bool `json:"success"`
Value string `json:"value"`
}
type valueBooleanResponse struct {
Success bool `json:"success"`
Value bool `json:"value"`
}
type errorResponse struct {
Success bool `json:"success"`
Error string `json:"error"`
}
func getStatic(w http.ResponseWriter, r *http.Request) {
// haha this is dumb and I should change it
re := regexp.MustCompile(`[^a-zA-Z0-9\.]`)
path := r.URL.Path[1:]
sanitized_path := re.ReplaceAll([]byte(path), []byte("_"))
if string(sanitized_path) == "" {
sanitized_path = []byte("index.html")
}
data, err := assets.Asset(string(sanitized_path))
if err != nil {
// Asset was not found.
fmt.Fprintln(w, err)
}
extension := filepath.Ext(string(sanitized_path))
// is this a HTML file? if so wrap it in the template
if extension == ".html" {
wrapper, _ := assets.Asset("wrapper.tmpl")
t := template.Must(template.New("wrapper").Parse(string(wrapper)))
var b struct {
Body string
Path string
Version string
}
b.Body = string(data)
b.Path = string(sanitized_path)
b.Version = config.CurrentVersion
t.Execute(w, b)
return
}
// otherwise we are a static thing
w.Header().Set("Content-Type", mime.TypeByExtension(extension))
w.Write(data)
//
}
// TODO there should be locks around all these config accesses
func getSetWebhook(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: config.Config.WebHookURL}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
config.Config.WebHookURL = r.PostForm.Get("value")
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: config.Config.WebHookURL}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
// TODO there should be locks around all these config accesses
func getSetUsername(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: config.Config.Username}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
config.Config.Username = r.PostForm.Get("value")
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: config.Config.Username}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getSetWatch(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: strconv.Itoa(config.Config.Watch)}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
i, err := strconv.Atoi(r.PostForm.Get("value"))
if err != nil {
response := errorResponse{Success: false, Error: fmt.Sprintf("Bad value for watch: %v", err)}
js, _ := json.Marshal(response)
w.Write(js)
return
}
if i < 1 {
response := errorResponse{Success: false, Error: "must be > 0"}
js, _ := json.Marshal(response)
w.Write(js)
return
}
config.Config.Watch = i
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: strconv.Itoa(config.Config.Watch)}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getSetNoWatermark(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueBooleanResponse{Success: true, Value: config.Config.NoWatermark}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
v := r.PostForm.Get("value")
if v != "0" && v != "1" {
response := errorResponse{Success: false, Error: fmt.Sprintf("Bad value for nowatermark: %v", err)}
js, _ := json.Marshal(response)
w.Write(js)
return
}
if v == "0" {
config.Config.NoWatermark = false
} else {
config.Config.NoWatermark = true
}
config.SaveConfig()
postResponse := valueBooleanResponse{Success: true, Value: config.Config.NoWatermark}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getSetDirectory(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: config.Config.Path}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
newPath := r.PostForm.Get("value")
// sanity check this path
stat, err := os.Stat(newPath)
if os.IsNotExist(err) {
// not exist
response := errorResponse{Success: false, Error: fmt.Sprintf("Path: %s - does not exist", newPath)}
js, _ := json.Marshal(response)
w.Write(js)
return
} else if !stat.IsDir() {
// not a directory
response := errorResponse{Success: false, Error: fmt.Sprintf("Path: %s - is not a directory", newPath)}
js, _ := json.Marshal(response)
w.Write(js)
return
}
config.Config.Path = newPath
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: config.Config.Path}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getSetExclude(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: config.Config.Exclude}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
config.Config.Exclude = r.PostForm.Get("value")
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: config.Config.Exclude}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getLogs(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
text := ""
for _, log := range daulog.LogEntries {
text = text + fmt.Sprintf(
"%-6s %-19s %s\n", log.Type, log.Timestamp.Format("2006-01-02 15:04:05"), log.Entry,
)
}
// js, _ := json.Marshal(daulog.LogEntries)
w.Write([]byte(text))
}
func StartWebServer() {
http.HandleFunc("/", getStatic)
http.HandleFunc("/rest/config/webhook", getSetWebhook)
http.HandleFunc("/rest/config/username", getSetUsername)
http.HandleFunc("/rest/config/watch", getSetWatch)
http.HandleFunc("/rest/config/nowatermark", getSetNoWatermark)
http.HandleFunc("/rest/config/directory", getSetDirectory)
http.HandleFunc("/rest/config/exclude", getSetExclude)
http.HandleFunc("/rest/logs", getLogs)
go func() {
log.Print("Starting web server on http://localhost:9090")
err := http.ListenAndServe(":9090", nil) // set listen port
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}()
}