20 Commits
0.7 ... 0.10

Author SHA1 Message Date
c47660addf Add uploads page 2021-06-08 22:20:11 +09:30
2d0e294af6 Fix title 2021-06-08 22:19:54 +09:30
283e0f3584 Bump version 2021-06-08 22:19:37 +09:30
9ef6ab71c7 Add upload package and update dependencies 2021-06-07 21:13:57 +09:30
701583d3fd Update year 2021-06-07 21:13:18 +09:30
0c156a19f0 Fix punctuation 2021-06-06 22:22:44 +09:30
cc54bb6469 Enable filtering by debug and automatic scroll to bottom 2021-06-06 17:15:38 +09:30
71c097e578 Update gitignore 2021-06-06 17:15:19 +09:30
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
16 changed files with 523 additions and 270 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ discord-auto-upload.exe
assets assets
*.png *.png
*.jpg *.jpg
.DS_Store

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2017 Justin Hawkins Copyright (c) 2021 Justin Hawkins
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -20,7 +20,7 @@ Binaries are available for Mac, Linux and Windows [here](https://github.com/tard
#### From source #### From source
You'll need to [download Go](https://golang.org/dl/) check the code out somewhere, run 'go generate' and then '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
@@ -80,6 +80,11 @@ thumbnail files.
* Files to upload are determined by the file modification time. If you drag and drop existing files they will * 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. 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 relatively 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.

View File

@@ -1,11 +1,15 @@
package config package config
import ( import (
"github.com/mitchellh/go-homedir" "encoding/json"
"fmt"
"io/ioutil"
"log" "log"
"os" "os"
"encoding/json"
"io/ioutil" daulog "github.com/tardisx/discord-auto-upload/log"
"github.com/mitchellh/go-homedir"
) )
// Config for the application // Config for the application
@@ -18,16 +22,16 @@ var Config struct {
Exclude string Exclude string
} }
const CurrentVersion string = "0.7" const CurrentVersion string = "0.10"
// Load the current config or initialise with defaults // Load the current config or initialise with defaults
func LoadOrInit() { func LoadOrInit() {
configPath := configPath() configPath := configPath()
log.Printf("Trying to load from %s\n", configPath) daulog.SendLog(fmt.Sprintf("Trying to load config from %s", configPath), daulog.LogTypeDebug)
_, err := os.Stat(configPath) _, err := os.Stat(configPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Printf("NOTE: No config file, writing out sample configuration") daulog.SendLog("NOTE: No config file, writing out sample configuration", daulog.LogTypeInfo)
log.Printf("You need to set the configuration via the web interface") daulog.SendLog("You need to set the configuration via the web interface", daulog.LogTypeInfo)
Config.WebHookURL = "" Config.WebHookURL = ""
Config.Path = homeDir() + string(os.PathSeparator) + "screenshots" Config.Path = homeDir() + string(os.PathSeparator) + "screenshots"
@@ -51,19 +55,21 @@ func LoadConfig() {
} }
func SaveConfig() { func SaveConfig() {
log.Print("saving configuration") daulog.SendLog("saving configuration", daulog.LogTypeInfo)
path := configPath() path := configPath()
jsonString, _ := json.Marshal(Config) jsonString, _ := json.Marshal(Config)
err := ioutil.WriteFile(path, jsonString, os.ModePerm) err := ioutil.WriteFile(path, jsonString, os.ModePerm)
if (err != nil) { if err != nil {
log.Fatalf("Cannot save config %s: %s", path, err.Error()) log.Fatalf("Cannot save config %s: %s", path, err.Error())
} }
} }
func homeDir() string { func homeDir() string {
dir, err := homedir.Dir() dir, err := homedir.Dir()
if (err != nil) { panic (err) } if err != nil {
return dir; panic(err)
}
return dir
} }
func configPath() string { func configPath() string {

View File

@@ -2,6 +2,8 @@
<main role="main" class="inner DAU"> <main role="main" class="inner DAU">
<h1 class="DAU-heading">Config</h1> <h1 class="DAU-heading">Config</h1>
<p class="lead">Discord-auto-upload configuration</p> <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=""> <form class="">
<div class="form-row align-items-center config-item" data-key="webhook"> <div class="form-row align-items-center config-item" data-key="webhook">
@@ -121,10 +123,11 @@ $(document).ready(function() {
.done(function(data) { .done(function(data) {
var this_el = $(".config-item[data-key='"+key+"']").find('.rest-field'); var this_el = $(".config-item[data-key='"+key+"']").find('.rest-field');
if (this_el.hasClass('rest-field-boolean')) { if (this_el.hasClass('rest-field-boolean')) {
this_el.prop('checked', data.Value); this_el.prop('checked', data.value);
} }
else { else {
this_el.val(data.Value);
this_el.val(data.value);
} }
update_sadness(); update_sadness();
@@ -140,10 +143,10 @@ $(document).ready(function() {
} }
$.post('/rest/config/'+key, { value: val }) $.post('/rest/config/'+key, { value: val })
.done(function(d) { .done(function(d) {
if (d.Success) { if (d.success) {
alert('Updated config'); alert('Updated config');
} else { } else {
alert("Error: " + d.Error); alert("Error: " + d.error);
} }
}); });

View File

@@ -42,6 +42,10 @@ body {
max-width: 42em; max-width: 42em;
} }
pre {
background-color: black;
color: aliceblue;
}
/* /*
* Header * Header

View File

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

53
data/logs.html Normal file
View File

@@ -0,0 +1,53 @@
<main role="main" class="inner DAU">
<h1 class="DAU-heading">Logs</h1>
<p class="lead">Discord-auto-upload logs</p>
<div class="container">
<div class="row">
<div class="col-sm">
<button type="button" onClick="debug=1; get_logs();" class="btn btn-primary">all logs</button>
</div>
<div class="col-sm">
<button type="button" onClick="debug=0; get_logs();" class="btn btn-primary">no debug</button>
</div>
<div class="col-sm">
<button type="button" id="scroll-button" onClick="toggle_scroll();" class="btn btn-primary">disable auto-scroll</button>
</div>
</div>
</div>
<pre id="logs" class="text-left pre-scrollable">
</pre>
</main>
<script>
var debug = 0;
var scrl = true;
$(document).ready(function() {
get_logs();
setInterval(function() { get_logs(); }, 1000);
});
function toggle_scroll() {
scrl = !scrl;
if (scrl) {
$('#scroll-button').text('disable auto-scroll');
}
else {
$('#scroll-button').text('auto-scroll');
}
}
function get_logs() {
$.ajax({ method: 'get', url: '/rest/logs', data: { debug : debug }})
.done(function(data) {
$('#logs').text(data);
console.log('scrl is ', scrl);
if (scrl) {
$('#logs').scrollTop(10000);
}
});
}
</script>

39
data/uploads.html Normal file
View File

@@ -0,0 +1,39 @@
<main role="main" class="inner DAU">
<h1 class="DAU-heading">Uploads</h1>
<p class="lead">Discord-auto-upload uploads</p>
<table class="table table-condensed table-dark">
<thead>
<tr><th>uploaded</th><th>dt</th><th>thumb</th></tr>
</thead>
<tbody id="uploads">
</tbody>
</table>
</main>
<script>
$(document).ready(function() {
get_uploads();
});
function get_uploads() {
$.ajax({ method: 'get', url: '/rest/uploads'})
.done(function(data) {
console.log(data);
$('#uploads').empty();
if (! data) { return }
data.forEach(i => {
// {uploaded: true, uploaded_at: "2021-06-08T21:59:52.855936+09:30", url: "https://cdn.discordapp.com/attachments/849615269706203171/851800197046468628/dau736004285.png", width: 640, height: 640}
console.log(i);
row = $('<tr>');
row.append($('<td>').text(i.uploaded ? 'yes' : 'no'));
row.append($('<td>').text(i.uploaded_at));
row.append($('<td>').html($('<img>', { width : i.width/10, height : i.height/10, src : i.url })));
$('#uploads').prepend(row);
});
});
}
</script>

View File

@@ -40,6 +40,8 @@
<nav class="nav nav-masthead justify-content-center"> <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 "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 "config.html"}} active {{ end }}" href="/config.html">Config</a>
<a class="nav-link {{ if eq .Path "uploads.html"}} active {{ end }}" href="/uploads.html">Uploads</a>
<a class="nav-link {{ if eq .Path "logs.html"}} active {{ end }}" href="/logs.html">Logs</a>
</nav> </nav>
</div> </div>
</header> </header>

218
dau.go
View File

@@ -3,31 +3,27 @@ package main
//go:generate go-bindata -pkg assets -o assets/static.go -prefix data/ data //go:generate go-bindata -pkg assets -o assets/static.go -prefix data/ data
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
"mime/multipart"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"image"
_ "image/gif" _ "image/gif"
_ "image/jpeg" _ "image/jpeg"
_ "image/png" _ "image/png"
"github.com/fogleman/gg"
"github.com/pborman/getopt" "github.com/pborman/getopt"
// "github.com/skratchdot/open-golang/open" // "github.com/skratchdot/open-golang/open"
"golang.org/x/image/font/inconsolata"
"github.com/tardisx/discord-auto-upload/config" "github.com/tardisx/discord-auto-upload/config"
daulog "github.com/tardisx/discord-auto-upload/log"
"github.com/tardisx/discord-auto-upload/uploads"
"github.com/tardisx/discord-auto-upload/web" "github.com/tardisx/discord-auto-upload/web"
) )
@@ -40,11 +36,11 @@ func main() {
// log.Print("Opening web browser") // log.Print("Opening web browser")
// open.Start("http://localhost:9090") // open.Start("http://localhost:9090")
go web.StartWebServer() web.StartWebServer()
checkUpdates() checkUpdates()
log.Print("Waiting for images to appear in ", config.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 {
if checkPath(config.Config.Path) { if checkPath(config.Config.Path) {
@@ -55,7 +51,7 @@ func main() {
} }
lastCheck = newLastCheck lastCheck = newLastCheck
} }
log.Printf("sleeping for %ds before next check of %s", config.Config.Watch, config.Config.Path) 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) time.Sleep(time.Duration(config.Config.Watch) * time.Second)
} }
} }
@@ -76,16 +72,18 @@ func checkPath(path string) bool {
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.Print("WARNING: Update check failed: ", err) daulog.SendLog(fmt.Sprintf("WARNING: Update check failed: %v", err), daulog.LogTypeError)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -107,6 +105,7 @@ func checkUpdates() {
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") 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)
} }
} }
@@ -136,7 +135,6 @@ func parseOptions() {
} }
func checkFile(path string, f os.FileInfo, err error) error { func checkFile(path string, f os.FileInfo, err error) error {
if f.ModTime().After(lastCheck) && f.Mode().IsRegular() { if f.ModTime().After(lastCheck) && f.Mode().IsRegular() {
if fileEligible(path) { if fileEligible(path) {
@@ -168,190 +166,6 @@ func fileEligible(file string) bool {
func processFile(file string) { func processFile(file string) {
if !config.Config.NoWatermark { daulog.SendLog("Sending to uploader", daulog.LogTypeInfo)
log.Print("Copying to temp location and watermarking ", file) uploads.AddFile(file)
file = mungeFile(file)
}
if config.Config.WebHookURL == "" {
log.Print("WebHookURL is not configured - cannot upload!")
return
}
log.Print("Uploading ", file)
extraParams := map[string]string{}
if config.Config.Username != "" {
log.Print("Overriding username with " + config.Config.Username)
extraParams["username"] = config.Config.Username
}
type DiscordAPIResponseAttachment struct {
URL string
ProxyURL string
Size int
Width int
Height int
Filename string
}
type DiscordAPIResponse struct {
Attachments []DiscordAPIResponseAttachment
ID int64 `json:",string"`
}
var retriesRemaining = 5
for retriesRemaining > 0 {
request, err := newfileUploadRequest(config.Config.WebHookURL, extraParams, "file", file)
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 resp.StatusCode != 200 {
log.Print("Bad response from server:", resp.StatusCode)
if b, err := ioutil.ReadAll(resp.Body); err == nil {
log.Print("Body:", string(b))
}
retriesRemaining--
sleepForRetries(retriesRemaining)
continue
}
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 {
log.Print("Removing temporary file ", file)
os.Remove(file)
}
if retriesRemaining == 0 {
log.Fatal("Failed to upload, even after retries")
}
}
func sleepForRetries(retry int) {
if retry == 0 {
return
}
retryTime := (6-retry)*(6-retry) + 6
log.Printf("Will retry in %d seconds (%d remaining attempts)", retryTime, retry)
time.Sleep(time.Duration(retryTime) * time.Second)
}
func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile(paramName, filepath.Base(path))
if err != nil {
return nil, err
}
_, err = io.Copy(part, file)
if err != nil {
log.Fatal("Could not copy: ", err)
}
for key, val := range params {
_ = writer.WriteField(key, val)
}
err = writer.Close()
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", uri, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
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
} }

3
go.mod
View File

@@ -7,6 +7,5 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/pborman/getopt 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-20210504121937-7319ad40d33e
golang.org/x/image v0.0.0-20201208152932-35266b937fa6
) )

6
go.sum
View File

@@ -6,8 +6,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk= 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= golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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= 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,
}
}

249
uploads/uploads.go Normal file
View File

@@ -0,0 +1,249 @@
// The uploads pacakge encapsulates dealing with file uploads to
// discord
package uploads
import (
"bytes"
"encoding/json"
"fmt"
"image"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
"github.com/fogleman/gg"
"github.com/tardisx/discord-auto-upload/config"
daulog "github.com/tardisx/discord-auto-upload/log"
"golang.org/x/image/font/inconsolata"
)
type Upload struct {
Uploaded bool `json:"uploaded"` // has this file been uploaded to discord
UploadedAt time.Time `json:"uploaded_at"`
originalFilename string // path on the local disk
mungedFilename string // post-watermark
Url string `json:"url"` // url on the discord CDN
Width int `json:"width"`
Height int `json:"height"`
}
var Uploads []*Upload
func AddFile(file string) {
thisUpload := Upload{
Uploaded: false,
originalFilename: file,
}
Uploads = append(Uploads, &thisUpload)
ProcessUpload(&thisUpload)
}
func ProcessUpload(up *Upload) {
file := up.originalFilename
if !config.Config.NoWatermark {
daulog.SendLog("Copying to temp location and watermarking ", daulog.LogTypeInfo)
file = mungeFile(file)
up.mungedFilename = file
}
if config.Config.WebHookURL == "" {
daulog.SendLog("WebHookURL is not configured - cannot upload!", daulog.LogTypeError)
return
}
extraParams := map[string]string{}
if config.Config.Username != "" {
daulog.SendLog("Overriding username with "+config.Config.Username, daulog.LogTypeInfo)
extraParams["username"] = config.Config.Username
}
type DiscordAPIResponseAttachment struct {
URL string
ProxyURL string
Size int
Width int
Height int
Filename string
}
type DiscordAPIResponse struct {
Attachments []DiscordAPIResponseAttachment
ID int64 `json:",string"`
}
var retriesRemaining = 5
for retriesRemaining > 0 {
request, err := newfileUploadRequest(config.Config.WebHookURL, extraParams, "file", file)
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 resp.StatusCode != 200 {
// {"message": "Request entity too large", "code": 40005}
log.Print("Bad response from server:", resp.StatusCode)
if b, err := ioutil.ReadAll(resp.Body); err == nil {
log.Print("Body:", string(b))
daulog.SendLog(fmt.Sprintf("Bad response: %s", string(b)), daulog.LogTypeError)
}
retriesRemaining--
sleepForRetries(retriesRemaining)
continue
}
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)
// {"id": "851092588608880670", "type": 0, "content": "", "channel_id": "849615269706203171", "author": {"bot": true, "id": "849615314274484224", "username": "abcdedf", "avatar": null, "discriminator": "0000"}, "attachments": [{"id": "851092588332449812", "filename": "dau480457962.png", "size": 859505, "url": "https://cdn.discordapp.com/attachments/849615269706203171/851092588332449812/dau480457962.png", "proxy_url": "https://media.discordapp.net/attachments/849615269706203171/851092588332449812/dau480457962.png", "width": 640, "height": 640, "content_type": "image/png"}], "embeds": [], "mentions": [], "mention_roles": [], "pinned": false, "mention_everyone": false, "tts": false, "timestamp": "2021-06-06T13:38:05.660000+00:00", "edited_timestamp": null, "flags": 0, "components": [], "webhook_id": "849615314274484224"}
daulog.SendLog(fmt.Sprintf("Response: %s", string(resBody[:])), daulog.LogTypeDebug)
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
daulog.SendLog(fmt.Sprintf("Uploaded to %s %dx%d", a.URL, a.Width, a.Height), daulog.LogTypeInfo)
daulog.SendLog(fmt.Sprintf("id: %d, %d bytes transferred in %.2f seconds (%.2f KiB/s)", res.ID, a.Size, elapsed.Seconds(), rate), daulog.LogTypeInfo)
up.Url = a.URL
up.Uploaded = true
up.Width = a.Width
up.Height = a.Height
up.UploadedAt = time.Now()
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 newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile(paramName, filepath.Base(path))
if err != nil {
return nil, err
}
_, err = io.Copy(part, file)
if err != nil {
log.Fatal("Could not copy: ", err)
}
for key, val := range params {
_ = writer.WriteField(key, val)
}
err = writer.Close()
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", uri, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
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
}
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)
}

View File

@@ -3,8 +3,6 @@ package web
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/tardisx/discord-auto-upload/assets"
"github.com/tardisx/discord-auto-upload/config"
"log" "log"
"mime" "mime"
"net/http" "net/http"
@@ -13,6 +11,11 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"text/template" "text/template"
"github.com/tardisx/discord-auto-upload/assets"
"github.com/tardisx/discord-auto-upload/config"
daulog "github.com/tardisx/discord-auto-upload/log"
"github.com/tardisx/discord-auto-upload/uploads"
) )
// DAUWebServer - stuff for the web server // DAUWebServer - stuff for the web server
@@ -21,23 +24,22 @@ type DAUWebServer struct {
} }
type valueStringResponse struct { type valueStringResponse struct {
Success bool `json: 'success'` Success bool `json:"success"`
Value string `json: 'value'` Value string `json:"value"`
} }
type valueBooleanResponse struct { type valueBooleanResponse struct {
Success bool `json: 'success'` Success bool `json:"success"`
Value bool `json: 'value'` Value bool `json:"value"`
} }
type errorResponse struct { type errorResponse struct {
Success bool `json: 'success'` Success bool `json:"success"`
Error string `json: 'error'` Error string `json:"error"`
} }
func getStatic(w http.ResponseWriter, r *http.Request) { func getStatic(w http.ResponseWriter, r *http.Request) {
// haha this is dumb and I should change it // haha this is dumb and I should change it
// fmt.Println(r.URL)
re := regexp.MustCompile(`[^a-zA-Z0-9\.]`) re := regexp.MustCompile(`[^a-zA-Z0-9\.]`)
path := r.URL.Path[1:] path := r.URL.Path[1:]
sanitized_path := re.ReplaceAll([]byte(path), []byte("_")) sanitized_path := re.ReplaceAll([]byte(path), []byte("_"))
@@ -270,8 +272,38 @@ func getSetExclude(w http.ResponseWriter, r *http.Request) {
} }
} }
func getLogs(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
showDebug := false
debug, present := r.URL.Query()["debug"]
if present && len(debug[0]) > 0 && debug[0] != "0" {
showDebug = true
}
text := ""
for _, log := range daulog.LogEntries {
if !showDebug && log.Type == daulog.LogTypeDebug {
continue
}
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 getUploads(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
ups := uploads.Uploads
text, _ := json.Marshal(ups)
w.Write([]byte(text))
}
func StartWebServer() { func StartWebServer() {
http.HandleFunc("/", getStatic) http.HandleFunc("/", getStatic)
http.HandleFunc("/rest/config/webhook", getSetWebhook) http.HandleFunc("/rest/config/webhook", getSetWebhook)
http.HandleFunc("/rest/config/username", getSetUsername) http.HandleFunc("/rest/config/username", getSetUsername)
@@ -280,11 +312,14 @@ func StartWebServer() {
http.HandleFunc("/rest/config/directory", getSetDirectory) http.HandleFunc("/rest/config/directory", getSetDirectory)
http.HandleFunc("/rest/config/exclude", getSetExclude) http.HandleFunc("/rest/config/exclude", getSetExclude)
http.HandleFunc("/rest/logs", getLogs)
http.HandleFunc("/rest/uploads", getUploads)
go func() {
log.Print("Starting web server on http://localhost:9090") log.Print("Starting web server on http://localhost:9090")
err := http.ListenAndServe(":9090", nil) // set listen port err := http.ListenAndServe(":9090", nil) // set listen port
if err != nil { if err != nil {
log.Fatal("ListenAndServe: ", err) log.Fatal("ListenAndServe: ", err)
} }
}()
} }