Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5ce0c7f63 | |||
| bcc4e145a2 | |||
| d8c0b7d0ea | |||
| fdf70daba7 | |||
| b69cdebf3b | |||
| 9e22490fe2 | |||
|
|
ae24f16631 | ||
|
|
b69acac0d0 | ||
|
|
c833f185cc | ||
|
|
b3ee0d9d1d | ||
| 23c0aa2a34 | |||
| 2d1ac3c803 | |||
| 942f81a378 | |||
| 287efab257 | |||
| 2b159e5532 | |||
| 851f073e99 | |||
| ec658520b7 | |||
| 6b1867f35f | |||
| 6e493522c8 | |||
| 4d09901fb3 | |||
| 9c9d4e492a | |||
| e1f5afa788 | |||
| 46a0f5a187 | |||
| 55bb5a8bae | |||
| c2b9bf410d | |||
| a4f958f846 | |||
| 14f8fe1933 | |||
| 752ff42a19 | |||
| 1ef062d19c | |||
| 6f09841209 | |||
| 4619bb5383 | |||
| e240f5dbd0 | |||
|
|
f976777f40 | ||
| 80a905b7d6 | |||
| 497d2e3e27 | |||
| ab54ace0d2 | |||
| 450765145b | |||
| 3970c611a4 | |||
| d8dc3e4ea8 | |||
| 3693d94297 | |||
| 8ded2b2e2d | |||
| e3e712d073 | |||
| 1ecac568f7 | |||
| 82ba3be742 | |||
|
|
4825dc56e6 | ||
| 65b9241492 | |||
|
|
73b33f5872 | ||
|
|
cc0fee57c2 | ||
|
|
05a3a0d09a | ||
|
|
72588642b6 | ||
|
|
7ff4685a70 | ||
|
|
f6b92ee8bd | ||
|
|
68d9ab7859 | ||
|
|
d2d7843b6f | ||
|
|
13589535a8 | ||
|
|
cb1f1d1a05 | ||
|
|
699ca9fcfc |
27
.gitignore
vendored
27
.gitignore
vendored
@@ -1,20 +1,7 @@
|
|||||||
/blib/
|
dist
|
||||||
/.build/
|
release
|
||||||
_build/
|
discord-auto-upload
|
||||||
cover_db/
|
discord-auto-upload.exe
|
||||||
inc/
|
assets
|
||||||
Build
|
*.png
|
||||||
!Build/
|
*.jpg
|
||||||
Build.bat
|
|
||||||
.last_cover_stats
|
|
||||||
/Makefile
|
|
||||||
/Makefile.old
|
|
||||||
/MANIFEST.bak
|
|
||||||
/META.yml
|
|
||||||
/META.json
|
|
||||||
/MYMETA.*
|
|
||||||
nytprof.out
|
|
||||||
/pm_to_blib
|
|
||||||
*.o
|
|
||||||
*.bs
|
|
||||||
/_eumm/
|
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -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,44 +16,77 @@ 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).
|
||||||
|
|
||||||
#### From source
|
#### From source
|
||||||
|
|
||||||
TBD
|
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
|
||||||
|
|
||||||
Note that currently `dau` does not look in subdirectories. Please submit an issue if this is a use case for you.
|
See the web interface at http://localhost:9090 to configure `dau`.
|
||||||
|
|
||||||
The only two mandatory command line parameters are the discord webhook URL:
|
### 'Discord WebHook URL'
|
||||||
|
|
||||||
`--webhook URL` - the webhook URL (see [here](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) for details).
|
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.
|
||||||
|
|
||||||
and the directory to watch:
|
### 'Bot Username'
|
||||||
|
|
||||||
`--directory /some/path/here` - the directory that screenshots will appear in.
|
This is completely optional and can be any arbitrary string. It makes the upload
|
||||||
|
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.
|
||||||
|
|
||||||
You will have to quote the path on windows, or anywhere where the directory path contains spaces.
|
### 'Directory to watch'
|
||||||
|
|
||||||
Other parameters are:
|
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.
|
||||||
|
|
||||||
`--watch xx` - specify how many seconds to wait between scanning the directory. The default is 10 seconds.
|
### 'Period between filesystem checks'
|
||||||
|
|
||||||
|
This is the number of seconds between which `dau` will look for new images.
|
||||||
|
|
||||||
|
### 'Do not watermark images'
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
43
build-release.pl
Executable file
43
build-release.pl
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
open my $fh, "<", "config/config.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 => 'amd64' }, filename => 'dau.exe' },
|
||||||
|
linux => { env => { GOOS => 'linux', GOARCH => 'amd64' }, filename => 'dau' },
|
||||||
|
mac => { env => { GOOS => 'darwin', GOARCH => 'amd64' }, filename => 'dau' },
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach my $type (keys %build) {
|
||||||
|
mkdir "release/$type";
|
||||||
|
}
|
||||||
|
|
||||||
|
add_extras();
|
||||||
|
|
||||||
|
system(qw{go generate});
|
||||||
|
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 {
|
||||||
|
# we used to have a .bat file here, but no longer needed
|
||||||
|
}
|
||||||
78
config/config.go
Normal file
78
config/config.go
Normal 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.8"
|
||||||
|
|
||||||
|
// 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
156
data/config.html
Normal 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"> </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
110
data/dau.css
Normal 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
8
data/index.html
Normal 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
18
data/logs.html
Normal 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
59
data/wrapper.tmpl
Normal 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>
|
||||||
474
dau.go
474
dau.go
@@ -1,213 +1,361 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
//go:generate go-bindata -pkg assets -o assets/static.go -prefix data/ data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"bytes"
|
||||||
"strings"
|
"encoding/json"
|
||||||
"github.com/pborman/getopt"
|
"fmt"
|
||||||
"path/filepath"
|
"io"
|
||||||
"os"
|
"io/ioutil"
|
||||||
"time"
|
"log"
|
||||||
"net/http"
|
"mime/multipart"
|
||||||
"log"
|
"net/http"
|
||||||
"io"
|
"os"
|
||||||
"bytes"
|
"path/filepath"
|
||||||
"mime/multipart"
|
"strings"
|
||||||
"encoding/json"
|
"time"
|
||||||
"io/ioutil"
|
|
||||||
|
"image"
|
||||||
|
|
||||||
|
_ "image/gif"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
|
||||||
|
"github.com/fogleman/gg"
|
||||||
|
"github.com/pborman/getopt"
|
||||||
|
|
||||||
|
// "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 current_version = "0.2"
|
var lastCheck = time.Now()
|
||||||
var last_check = time.Now()
|
var newLastCheck = time.Now()
|
||||||
var new_last_check = time.Now()
|
|
||||||
var webhook_url string
|
|
||||||
|
|
||||||
type webhook_response struct {
|
|
||||||
Test string
|
|
||||||
}
|
|
||||||
|
|
||||||
func keepLines(s string, n int) string {
|
|
||||||
result := strings.Join(strings.Split(s, "\n")[:n], "\n")
|
|
||||||
return strings.Replace(result, "\r", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
webhook, path, watch := parse_options()
|
|
||||||
webhook_url = webhook
|
|
||||||
|
|
||||||
check_updates()
|
parseOptions()
|
||||||
|
|
||||||
// wander the path, forever
|
// log.Print("Opening web browser")
|
||||||
for {
|
// open.Start("http://localhost:9090")
|
||||||
err := filepath.Walk(path, check_file)
|
web.StartWebServer()
|
||||||
if err != nil { log.Fatal("oh dear") }
|
|
||||||
//fmt.Printf("filepath.Walk() returned %v\n", err)
|
checkUpdates()
|
||||||
last_check = new_last_check
|
|
||||||
time.Sleep(time.Duration(watch)*time.Second)
|
daulog.SendLog(fmt.Sprintf("Waiting for images to appear in %s", config.Config.Path), daulog.LogTypeInfo)
|
||||||
}
|
// wander the path, forever
|
||||||
|
for {
|
||||||
|
if checkPath(config.Config.Path) {
|
||||||
|
err := filepath.Walk(config.Config.Path,
|
||||||
|
func(path string, f os.FileInfo, err error) error { return checkFile(path, f, err) })
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("could not watch path", err)
|
||||||
|
}
|
||||||
|
lastCheck = newLastCheck
|
||||||
|
}
|
||||||
|
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 check_updates() {
|
func checkPath(path string) bool {
|
||||||
|
src, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Problem with path '%s': %s", path, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !src.IsDir() {
|
||||||
|
log.Printf("Problem with path '%s': is not a directory", path)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type GithubRelease struct {
|
func checkUpdates() {
|
||||||
Html_url string
|
|
||||||
Tag_name string
|
|
||||||
Name string
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Get("https://api.github.com/repos/tardisx/discord-auto-upload/releases/latest")
|
type GithubRelease struct {
|
||||||
if (err != nil) {
|
HTMLURL string
|
||||||
log.Fatal("could not check for updates")
|
TagName string
|
||||||
}
|
Name string
|
||||||
defer resp.Body.Close()
|
Body string
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
}
|
||||||
if (err != nil) {
|
|
||||||
log.Fatal("could not check read update response")
|
|
||||||
}
|
|
||||||
|
|
||||||
var latest GithubRelease
|
daulog.SendLog("checking for new version", daulog.LogTypeInfo)
|
||||||
err = json.Unmarshal(body, &latest)
|
|
||||||
|
|
||||||
if (err != nil) {
|
client := &http.Client{Timeout: time.Second * 5}
|
||||||
log.Fatal("could not parse JSON", err)
|
resp, err := client.Get("https://api.github.com/repos/tardisx/discord-auto-upload/releases/latest")
|
||||||
}
|
if err != nil {
|
||||||
|
daulog.SendLog(fmt.Sprintf("WARNING: Update check failed: %v", err), daulog.LogTypeError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("could not check read update response")
|
||||||
|
}
|
||||||
|
|
||||||
if (current_version != latest.Tag_name) {
|
var latest GithubRelease
|
||||||
fmt.Println("A new version is available:", latest.Tag_name)
|
err = json.Unmarshal(body, &latest)
|
||||||
fmt.Println("----------- Release Info -----------")
|
|
||||||
fmt.Println(latest.Body)
|
if err != nil {
|
||||||
fmt.Println("------------------------------------")
|
log.Fatal("could not parse JSON: ", err)
|
||||||
fmt.Println("( You are currently on version:", current_version, ")")
|
}
|
||||||
}
|
|
||||||
|
if config.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(latest.Body)
|
||||||
|
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"), daulog.LogTypeInfo)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOptions() {
|
||||||
|
|
||||||
func parse_options() (webhook_url string, path string, watch int) {
|
// Declare the flags to be used
|
||||||
|
helpFlag := getopt.BoolLong("help", 'h', "help")
|
||||||
|
versionFlag := getopt.BoolLong("version", 'v', "show version")
|
||||||
|
getopt.SetParameters("")
|
||||||
|
|
||||||
// Declare the flags to be used
|
getopt.Parse()
|
||||||
// helpFlag := getopt.Bool('h', "display help")
|
|
||||||
webhookFlag := getopt.StringLong("webhook", 'w', "", "webhook URL")
|
|
||||||
pathFlag := getopt.StringLong("directory", 'd', "", "directory")
|
|
||||||
watchFlag := getopt.Int16Long("watch", 's', 10, "time between scans")
|
|
||||||
|
|
||||||
getopt.Parse()
|
if *helpFlag {
|
||||||
|
getopt.PrintUsage(os.Stderr)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
return *webhookFlag, *pathFlag, int(*watchFlag)
|
if *versionFlag {
|
||||||
|
fmt.Println("dau - https://github.com/tardisx/discord-auto-upload")
|
||||||
|
fmt.Printf("Version: %s\n", config.CurrentVersion)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// grab the config
|
||||||
|
config.LoadOrInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func check_file(path string, f os.FileInfo, err error) error {
|
func checkFile(path string, f os.FileInfo, err error) error {
|
||||||
// fmt.Println("Comparing", f.ModTime(), "to", last_check, "for", path)
|
if f.ModTime().After(lastCheck) && f.Mode().IsRegular() {
|
||||||
|
|
||||||
if f.ModTime().After(last_check) && f.Mode().IsRegular() {
|
if fileEligible(path) {
|
||||||
|
// process file
|
||||||
|
processFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
if file_eligible(path) {
|
if newLastCheck.Before(f.ModTime()) {
|
||||||
// process file
|
newLastCheck = f.ModTime()
|
||||||
process_file(path)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if new_last_check.Before(f.ModTime()) {
|
return nil
|
||||||
new_last_check = f.ModTime()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func file_eligible(file string) (bool) {
|
func fileEligible(file string) bool {
|
||||||
extension := strings.ToLower(filepath.Ext(file))
|
|
||||||
if extension == ".png" || extension == ".jpg" || extension == ".gif" {
|
if config.Config.Exclude != "" && strings.Contains(file, config.Config.Exclude) {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
extension := strings.ToLower(filepath.Ext(file))
|
||||||
|
if extension == ".png" || extension == ".jpg" || extension == ".gif" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func process_file(file string) {
|
func processFile(file string) {
|
||||||
log.Print("Uploading ", file)
|
|
||||||
|
|
||||||
extraParams := map[string]string{
|
if !config.Config.NoWatermark {
|
||||||
// "username": "Some username",
|
daulog.SendLog("Copying to temp location and watermarking ", daulog.LogTypeInfo)
|
||||||
}
|
file = mungeFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
type DiscordAPIResponseAttachment struct {
|
if config.Config.WebHookURL == "" {
|
||||||
Url string
|
daulog.SendLog("WebHookURL is not configured - cannot upload!", daulog.LogTypeError)
|
||||||
Proxy_url string
|
return
|
||||||
Size int
|
}
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
Filename string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiscordAPIResponse struct {
|
daulog.SendLog("Uploading", daulog.LogTypeInfo)
|
||||||
Attachments []DiscordAPIResponseAttachment
|
|
||||||
id int64
|
|
||||||
}
|
|
||||||
|
|
||||||
request, err := newfileUploadRequest(webhook_url, extraParams, "file", file)
|
extraParams := map[string]string{}
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
log.Fatal("Error performing request:", err)
|
if config.Config.Username != "" {
|
||||||
|
log.Print("Overriding username with " + config.Config.Username)
|
||||||
|
extraParams["username"] = config.Config.Username
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
type DiscordAPIResponseAttachment struct {
|
||||||
|
URL string
|
||||||
|
ProxyURL string
|
||||||
|
Size int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
|
||||||
if (resp.StatusCode != 200) {
|
type DiscordAPIResponse struct {
|
||||||
log.Print("Bad response from server:", resp.StatusCode)
|
Attachments []DiscordAPIResponseAttachment
|
||||||
return
|
ID int64 `json:",string"`
|
||||||
}
|
}
|
||||||
|
|
||||||
res_body, err := ioutil.ReadAll(resp.Body)
|
var retriesRemaining = 5
|
||||||
if (err != nil) {
|
for retriesRemaining > 0 {
|
||||||
log.Fatal("could not deal with body", err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
var res DiscordAPIResponse
|
request, err := newfileUploadRequest(config.Config.WebHookURL, extraParams, "file", file)
|
||||||
err = json.Unmarshal(res_body, &res)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
client := &http.Client{Timeout: time.Second * 30}
|
||||||
|
resp, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("Error performing request:", err)
|
||||||
|
retriesRemaining--
|
||||||
|
sleepForRetries(retriesRemaining)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
|
||||||
if (err != nil) {
|
if resp.StatusCode != 200 {
|
||||||
log.Fatal("could not parse JSON", err)
|
log.Print("Bad response from server:", resp.StatusCode)
|
||||||
fmt.Println("Response was:", res_body)
|
if b, err := ioutil.ReadAll(resp.Body); err == nil {
|
||||||
return
|
log.Print("Body:", string(b))
|
||||||
}
|
}
|
||||||
if (len(res.Attachments) < 1) {
|
retriesRemaining--
|
||||||
log.Print("bad response - no attachments?")
|
sleepForRetries(retriesRemaining)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
var a = res.Attachments[0]
|
|
||||||
log.Printf("Uploaded to %s %dx%d, %d bytes\n", a.Url, a.Width, a.Height, a.Size)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("could not deal with body: ", err)
|
||||||
|
retriesRemaining--
|
||||||
|
sleepForRetries(retriesRemaining)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
var res DiscordAPIResponse
|
||||||
|
err = json.Unmarshal(resBody, &res)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Print("could not parse JSON: ", err)
|
||||||
|
fmt.Println("Response was:", string(resBody[:]))
|
||||||
|
retriesRemaining--
|
||||||
|
sleepForRetries(retriesRemaining)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(res.Attachments) < 1 {
|
||||||
|
log.Print("bad response - no attachments?")
|
||||||
|
retriesRemaining--
|
||||||
|
sleepForRetries(retriesRemaining)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var a = res.Attachments[0]
|
||||||
|
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.Config.NoWatermark {
|
||||||
|
daulog.SendLog(fmt.Sprintf("Removing temporary file: %s", file), daulog.LogTypeDebug)
|
||||||
|
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
|
||||||
|
daulog.SendLog(fmt.Sprintf("Will retry in %d seconds (%d remaining attempts)", retryTime, retry), daulog.LogTypeError)
|
||||||
|
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) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
body := &bytes.Buffer{}
|
body := &bytes.Buffer{}
|
||||||
writer := multipart.NewWriter(body)
|
writer := multipart.NewWriter(body)
|
||||||
part, err := writer.CreateFormFile(paramName, filepath.Base(path))
|
part, err := writer.CreateFormFile(paramName, filepath.Base(path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
err = writer.Close()
|
err = writer.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", uri, body)
|
req, err := http.NewRequest("POST", uri, body)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
12
go.mod
Normal file
12
go.mod
Normal 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
13
go.sum
Normal 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
45
log/log.go
Normal 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
307
web/server.go
Normal 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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user