From 28df20cfba066541cac04da348caf0f4f19c8646 Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Thu, 18 Aug 2016 07:28:37 +0200 Subject: [PATCH] initial commit --- .gitignore | 29 ++++++++ LICENSE | 21 ++++++ main.go | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 9 +++ 4 files changed, 268 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 main.go create mode 100644 readme.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d411f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +config.ini diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5ff45a4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Sebastian Winkler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/main.go b/main.go new file mode 100644 index 0000000..196cae5 --- /dev/null +++ b/main.go @@ -0,0 +1,209 @@ +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "regexp" + + "strconv" + "strings" + + "github.com/bwmarrin/discordgo" + "github.com/mvdan/xurls" + "gopkg.in/ini.v1" +) + +var ( + Email string + Password string + Token string + ChannelWhitelist map[string]string + BaseDownloadPath string + RegexpUrlTwitter *regexp.Regexp + RegexpUrlTistory *regexp.Regexp +) + +func main() { + var err error + cfg, err := ini.Load("config.ini") + if err != nil { + fmt.Println("unable to read config file", err) + cfg = ini.Empty() + } + + if !cfg.Section("auth").HasKey("email") || + !cfg.Section("auth").HasKey("password") { + cfg.Section("auth").NewKey("email", "your@email.com") + cfg.Section("auth").NewKey("password", "yourpassword") + cfg.Section("channels").NewKey("channelid1", "C:\\full\\path\\1") + cfg.Section("channels").NewKey("channelid2", "C:\\full\\path\\2") + cfg.Section("channels").NewKey("channelid3", "C:\\full\\path\\3") + err = cfg.SaveTo("config.ini") + + if err != nil { + fmt.Println("unable to write config file", err) + return + } + fmt.Println("Wrote config file, please fill out and restart the program") + return + } + + ChannelWhitelist = cfg.Section("channels").KeysHash() + + RegexpUrlTwitter, err = regexp.Compile( + `^http(s?):\/\/pbs\.twimg\.com\/media\/[a-zA-Z]+\.jpg((\:[a-z]+)?)$`) + if err != nil { + fmt.Println("Regexp error", err) + return + } + RegexpUrlTistory, err = regexp.Compile( + `^http(s?):\/\/[a-z0-9]+\.uf\.tistory\.com\/(image|original)\/[A-Z0-9]+$`) + if err != nil { + fmt.Println("Regexp error", err) + return + } + + dg, err := discordgo.New( + cfg.Section("auth").Key("email").String(), + cfg.Section("auth").Key("password").String()) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + dg.AddHandler(messageCreate) + + err = dg.Open() + if err != nil { + fmt.Println("error opening connection,", err) + return + } + + fmt.Println("Client is now connected. Press CTRL-C to exit.") + // keep program running until CTRL-C is pressed. + <-make(chan struct{}) + return +} + +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + if folderName, ok := ChannelWhitelist[m.ChannelID]; ok { + downloadPath := folderName + for _, iAttachment := range m.Attachments { + downloadFromUrl(iAttachment.URL, iAttachment.Filename, downloadPath) + } + foundUrls := xurls.Strict.FindAllString(m.Content, -1) + for _, iFoundUrl := range foundUrls { + // Twitter url? + if RegexpUrlTwitter.MatchString(iFoundUrl) { + err := handleTwitterUrl(iFoundUrl, downloadPath) + if err != nil { + fmt.Println("Twitter url failed,", iFoundUrl, ",", err) + continue + } + // Tistory url? + } else if RegexpUrlTistory.MatchString(iFoundUrl) { + err := handleTistoryUrl(iFoundUrl, downloadPath) + if err != nil { + fmt.Println("Tistory url failed,", iFoundUrl, ",", err) + continue + } + } else { + // Any other url + downloadFromUrl(iFoundUrl, + getContentDispositionFilename(iFoundUrl), downloadPath) + } + } + } +} + +func handleTwitterUrl(url string, folder string) error { + parts := strings.Split(url, ":") + if len(parts) < 2 { + errors.New("unable to format twitter url") + } else { + downloadFromUrl("https:"+parts[1]+":orig", path.Base(parts[1]), folder) + } + return nil +} + +func handleTistoryUrl(url string, folder string) error { + url = strings.Replace(url, "/image/", "/original/", -1) + downloadFromUrl(url, getContentDispositionFilename(url), folder) + return nil +} + +func getContentDispositionFilename(dUrl string) string { + resp, err := http.Head(dUrl) + if err != nil { + return path.Base(dUrl) + } + for key, iHeader := range resp.Header { + if key == "Content-Disposition" { + parts := strings.Split(iHeader[0], "\"") + if len(parts) == 3 { + filename, err := url.QueryUnescape(parts[1]) + if err != nil { + return parts[1] + } else { + return filename + } + } + } + } + return path.Base(dUrl) +} + +func downloadFromUrl(url string, filename string, path string) { + err := os.MkdirAll(path, 755) + if err != nil { + fmt.Println("Error while creating folder", path, "-", err) + return + } + + completePath := path + string(os.PathSeparator) + filename + if _, err := os.Stat(completePath); err == nil { + tmpPath := completePath + i := 1 + for { + completePath = tmpPath[0:len(tmpPath)-len(filepath.Ext(tmpPath))] + + "-" + strconv.Itoa(i) + filepath.Ext(tmpPath) + if _, err := os.Stat(completePath); os.IsNotExist(err) { + break + } + i = i + 1 + } + } + + response, err := http.Get(url) + if err != nil { + fmt.Println("Error while downloading", url, "-", err) + return + } + defer response.Body.Close() + + bodyOfResp, err := ioutil.ReadAll(response.Body) + if err != nil { + fmt.Println("Could not read response", url, "-", err) + return + } + contentType := http.DetectContentType(bodyOfResp) + contentTypeParts := strings.Split(contentType, "/") + if contentTypeParts[0] != "image" { + fmt.Println("No image found", url) + return + } + + err = ioutil.WriteFile(completePath, bodyOfResp, 0644) + if err != nil { + fmt.Println("Error while writing to disk", url, "-", err) + return + } + + fmt.Printf("Downloaded url: %s to %s\n", url, completePath) +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9e3088b --- /dev/null +++ b/readme.md @@ -0,0 +1,9 @@ +# discord-image-downloader-go + +This is a simple tool which downloads pictures posted in discord channels of your choice to a local folder. It handles various sources like twitter differently to make sure to download the best quality available. It is written in go and the code is very ugly. + +## How to use? +When you run the tool for the first time it creates an `config.ini` file with example values. Edit these values and run the tool for a second time. It should connect to discords api and wait for new messages. + +### Where do I get the channel id? +Open discord in your browser and go to the channel you want to monitor. In your adress bar should be an URL like `https://discordapp.com/channels/1234/5678`. The number after the last slash is the channel id, in this case `5678`.