123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951 |
- package main
- import (
- "bytes"
- "fmt"
- "image"
- "io/ioutil"
- "log"
- "math/rand"
- "mime"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "time"
- "github.com/bwmarrin/discordgo"
- "github.com/fatih/color"
- "github.com/hako/durafmt"
- "github.com/rivo/duplo"
- "mvdan.cc/xurls/v2"
- )
- type download struct {
- URL string
- Time time.Time
- Destination string
- Filename string
- ChannelID string
- UserID string
- }
- type downloadStatus int
- const (
- downloadSuccess downloadStatus = iota
- downloadIgnored
- downloadSkipped
- downloadSkippedDuplicate
- downloadSkippedUnpermittedDomain
- downloadSkippedUnpermittedType
- downloadSkippedUnpermittedExtension
- downloadSkippedDetectedDuplicate
- downloadFailed
- downloadFailedInvalidSource
- downloadFailedInvalidPath
- downloadFailedCreatingFolder
- downloadFailedRequesting
- downloadFailedDownloadingResponse
- downloadFailedReadResponse
- downloadFailedCreatingSubfolder
- downloadFailedWritingFile
- downloadFailedWritingDatabase
- )
- type downloadStatusStruct struct {
- Status downloadStatus
- Error error
- }
- func mDownloadStatus(status downloadStatus, _error ...error) downloadStatusStruct {
- if len(_error) == 0 {
- return downloadStatusStruct{
- Status: status,
- Error: nil,
- }
- }
- return downloadStatusStruct{
- Status: status,
- Error: _error[0],
- }
- }
- func getDownloadStatusString(status downloadStatus) string {
- switch status {
- case downloadSuccess:
- return "Download Succeeded"
- //
- case downloadIgnored:
- return "Download Ignored"
- //
- case downloadSkipped:
- return "Download Skipped"
- case downloadSkippedDuplicate:
- return "Download Skipped - Duplicate"
- case downloadSkippedUnpermittedDomain:
- return "Download Skipped - Unpermitted Domain"
- case downloadSkippedUnpermittedType:
- return "Download Skipped - Unpermitted File Type"
- case downloadSkippedUnpermittedExtension:
- return "Download Skipped - Unpermitted File Extension"
- case downloadSkippedDetectedDuplicate:
- return "Download Skipped - Detected Duplicate"
- //
- case downloadFailed:
- return "Download Failed"
- case downloadFailedInvalidSource:
- return "Download Failed - Invalid Source"
- case downloadFailedInvalidPath:
- return "Download Failed - Invalid Path"
- case downloadFailedCreatingFolder:
- return "Download Failed - Error Creating Folder"
- case downloadFailedRequesting:
- return "Download Failed - Error Requesting URL Data"
- case downloadFailedDownloadingResponse:
- return "Download Failed - Error Downloading URL Response"
- case downloadFailedReadResponse:
- return "Download Failed - Error Reading URL Response"
- case downloadFailedCreatingSubfolder:
- return "Download Failed - Error Creating Subfolder for Type"
- case downloadFailedWritingFile:
- return "Download Failed - Error Writing File"
- case downloadFailedWritingDatabase:
- return "Download Failed - Error Writing to Database"
- }
- return "Unknown Error"
- }
- // Trim duplicate links in link list
- func trimDuplicateLinks(fileItems []*fileItem) []*fileItem {
- var result []*fileItem
- seen := map[string]bool{}
- for _, item := range fileItems {
- if seen[item.Link] {
- continue
- }
- seen[item.Link] = true
- result = append(result, item)
- }
- return result
- }
- func getRawLinks(m *discordgo.Message) []*fileItem {
- var links []*fileItem
- if m.Author == nil {
- m.Author = new(discordgo.User)
- }
- for _, attachment := range m.Attachments {
- links = append(links, &fileItem{
- Link: attachment.URL,
- Filename: attachment.Filename,
- })
- }
- foundLinks := xurls.Strict().FindAllString(m.Content, -1)
- for _, foundLink := range foundLinks {
- links = append(links, &fileItem{
- Link: foundLink,
- })
- }
- for _, embed := range m.Embeds {
- if embed.URL != "" {
- links = append(links, &fileItem{
- Link: embed.URL,
- })
- }
- // Removing for now as this causes it to try and pull shit from things like YouTube descriptions
- /*if embed.Description != "" {
- foundLinks = xurls.Strict().FindAllString(embed.Description, -1)
- for _, foundLink := range foundLinks {
- links = append(links, &fileItem{
- Link: foundLink,
- })
- }
- }*/
- if embed.Image != nil && embed.Image.URL != "" {
- links = append(links, &fileItem{
- Link: embed.Image.URL,
- })
- }
- if embed.Video != nil && embed.Video.URL != "" {
- links = append(links, &fileItem{
- Link: embed.Video.URL,
- })
- }
- }
- return links
- }
- func getDownloadLinks(inputURL string, channelID string) map[string]string {
- logPrefixErrorHere := color.HiRedString("[getDownloadLinks]")
- /* TODO: Download Support...
- - TikTok: Tried, once the connection is closed the cdn URL is rendered invalid
- - Facebook Photos: Tried, it doesn't preload image data, it's loaded in after. Would have to keep connection open, find alternative way to grab, or use api.
- - Facebook Videos: Previously supported but they split mp4 into separate audio and video streams
- */
- if regexUrlTwitter.MatchString(inputURL) {
- links, err := getTwitterUrls(inputURL)
- if err != nil {
- if !strings.Contains(err.Error(), "suspended") {
- log.Println(logPrefixErrorHere, color.RedString("Twitter Media fetch failed for %s -- %s", inputURL, err))
- }
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlTwitterStatus.MatchString(inputURL) {
- links, err := getTwitterStatusUrls(inputURL, channelID)
- if err != nil {
- if !strings.Contains(err.Error(), "suspended") && !strings.Contains(err.Error(), "No status found") {
- log.Println(logPrefixErrorHere, color.RedString("Twitter Status fetch failed for %s -- %s", inputURL, err))
- }
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlInstagram.MatchString(inputURL) {
- links, err := getInstagramUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Instagram fetch failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlImgurSingle.MatchString(inputURL) {
- links, err := getImgurSingleUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Imgur Media fetch failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlImgurAlbum.MatchString(inputURL) {
- links, err := getImgurAlbumUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Imgur Album fetch failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlStreamable.MatchString(inputURL) {
- links, err := getStreamableUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Streamable fetch failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlGfycat.MatchString(inputURL) {
- links, err := getGfycatUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Gfycat fetch failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlFlickrPhoto.MatchString(inputURL) {
- links, err := getFlickrPhotoUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Flickr Photo fetch failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlFlickrAlbum.MatchString(inputURL) {
- links, err := getFlickrAlbumUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Flickr Album fetch failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlFlickrAlbumShort.MatchString(inputURL) {
- links, err := getFlickrAlbumShortUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Flickr Album (short) fetch failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if config.Credentials.GoogleDriveCredentialsJSON != "" {
- if regexUrlGoogleDrive.MatchString(inputURL) {
- links, err := getGoogleDriveUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Google Drive Album URL for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlGoogleDriveFolder.MatchString(inputURL) {
- links, err := getGoogleDriveFolderUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Google Drive Folder URL for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- }
- if regexUrlTistory.MatchString(inputURL) {
- links, err := getTistoryUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Tistory URL failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlTistoryLegacy.MatchString(inputURL) {
- links, err := getLegacyTistoryUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Legacy Tistory URL failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlRedditPost.MatchString(inputURL) {
- links, err := getRedditPostUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Reddit Post URL failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if regexUrlMastodonPost1.MatchString(inputURL) || regexUrlMastodonPost2.MatchString(inputURL) {
- links, err := getMastodonPostUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Mastodon Post URL failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- // The original project has this as an option,
- if regexUrlPossibleTistorySite.MatchString(inputURL) {
- links, err := getPossibleTistorySiteUrls(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Checking for Tistory site failed for %s -- %s", inputURL, err))
- } else if len(links) > 0 {
- return trimDownloadedLinks(links, channelID)
- }
- }
- if strings.HasPrefix(inputURL, "https://cdn.discordapp.com/emojis/") {
- return nil
- }
- // Try without queries
- parsedURL, err := url.Parse(inputURL)
- if err == nil {
- parsedURL.RawQuery = ""
- inputURLWithoutQueries := parsedURL.String()
- if inputURLWithoutQueries != inputURL {
- return trimDownloadedLinks(getDownloadLinks(inputURLWithoutQueries, channelID), channelID)
- }
- }
- return trimDownloadedLinks(map[string]string{inputURL: ""}, channelID)
- }
- func getFileLinks(m *discordgo.Message) []*fileItem {
- var fileItems []*fileItem
- linkTime, err := m.Timestamp.Parse()
- if err != nil {
- linkTime = time.Now()
- }
- rawLinks := getRawLinks(m)
- for _, rawLink := range rawLinks {
- downloadLinks := getDownloadLinks(
- rawLink.Link,
- m.ChannelID,
- )
- for link, filename := range downloadLinks {
- if rawLink.Filename != "" {
- filename = rawLink.Filename
- }
- fileItems = append(fileItems, &fileItem{
- Link: link,
- Filename: filename,
- Time: linkTime,
- })
- }
- }
- fileItems = trimDuplicateLinks(fileItems)
- return fileItems
- }
- func startDownload(inputURL string, filename string, path string, message *discordgo.Message, fileTime time.Time, historyCmd bool) downloadStatusStruct {
- status := mDownloadStatus(downloadFailed)
- logPrefixErrorHere := color.HiRedString("[startDownload]")
- for i := 0; i < config.DownloadRetryMax; i++ {
- status = tryDownload(inputURL, filename, path, message, fileTime, historyCmd)
- if status.Status < downloadFailed { // Success or Skip
- break
- } else {
- time.Sleep(5 * time.Second)
- }
- }
- if status.Status >= downloadFailed && !historyCmd { // Any kind of failure
- log.Println(logPrefixErrorHere, color.RedString("Gave up on downloading %s after %d failed attempts...\t%s", inputURL, config.DownloadRetryMax, getDownloadStatusString(status.Status)))
- if isChannelRegistered(message.ChannelID) {
- channelConfig := getChannelConfig(message.ChannelID)
- if !historyCmd && *channelConfig.ErrorMessages {
- content := fmt.Sprintf(
- "Gave up trying to download\n<%s>\nafter %d failed attempts...\n\n``%s``",
- inputURL, config.DownloadRetryMax, getDownloadStatusString(status.Status))
- if status.Error != nil {
- content += fmt.Sprintf("\n```ERROR: %s```", status.Error)
- }
- // Failure Notice
- if hasPerms(message.ChannelID, discordgo.PermissionSendMessages) {
- _, err := bot.ChannelMessageSendComplex(message.ChannelID,
- &discordgo.MessageSend{
- Content: fmt.Sprintf("<@!%s>", message.Author.ID),
- Embed: buildEmbed(message.ChannelID, "Download Failure", content),
- })
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Failed to send failure message to %s: %s", message.ChannelID, err))
- }
- } else {
- log.Println(logPrefixErrorHere, color.HiRedString(fmtBotSendPerm, message.ChannelID))
- }
- }
- if status.Error != nil {
- logErrorMessage(fmt.Sprintf("%s...\n%s", getDownloadStatusString(status.Status), status.Error))
- }
- }
- }
- return status
- }
- func tryDownload(inputURL string, filename string, path string, message *discordgo.Message, fileTime time.Time, historyCmd bool) downloadStatusStruct {
- cachedDownloadID++
- thisDownloadID := cachedDownloadID
- startTime := time.Now()
- logPrefixErrorHere := color.HiRedString("[tryDownload]")
- logPrefix := ""
- if historyCmd {
- logPrefix = logPrefixHistory + " "
- }
- if stringInSlice(message.ChannelID, getAllChannels()) {
- var channelConfig configurationChannel
- if isChannelRegistered(message.ChannelID) {
- channelConfig = getChannelConfig(message.ChannelID)
- } else {
- channelDefault(&channelConfig)
- }
- var err error
- // Source validation
- _, err = url.ParseRequestURI(inputURL)
- if err != nil {
- return mDownloadStatus(downloadFailedInvalidSource, err)
- }
- // Clean/fix path
- if path == "" || path == string(os.PathSeparator) {
- log.Println(logPrefixErrorHere, color.HiRedString("Destination cannot be empty path..."))
- return mDownloadStatus(downloadFailedInvalidPath, err)
- }
- if !strings.HasSuffix(path, string(os.PathSeparator)) {
- path = path + string(os.PathSeparator)
- }
- // Create folder
- err = os.MkdirAll(path, 0755)
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Error while creating destination folder \"%s\": %s", path, err))
- return mDownloadStatus(downloadFailedCreatingFolder, err)
- }
- // Request
- timeout := time.Duration(time.Duration(config.DownloadTimeout) * time.Second)
- client := &http.Client{
- Timeout: timeout,
- }
- request, err := http.NewRequest("GET", inputURL, nil)
- request.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36")
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Error while requesting \"%s\": %s", inputURL, err))
- return mDownloadStatus(downloadFailedRequesting, err)
- }
- request.Header.Add("Accept-Encoding", "identity")
- response, err := client.Do(request)
- if err != nil {
- if !strings.Contains(err.Error(), "no such host") && !strings.Contains(err.Error(), "connection refused") {
- log.Println(logPrefixErrorHere, color.HiRedString("Error while receiving response from \"%s\": %s", inputURL, err))
- }
- return mDownloadStatus(downloadFailedDownloadingResponse, err)
- }
- defer response.Body.Close()
- // Download duration
- if config.DebugOutput && !historyCmd {
- log.Println(logPrefixDebug, color.YellowString("#%d - %s to download.", thisDownloadID, durafmt.ParseShort(time.Since(startTime)).String()))
- }
- downloadTime := time.Now()
- // Read
- bodyOfResp, err := ioutil.ReadAll(response.Body)
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Could not read response from \"%s\": %s", inputURL, err))
- return mDownloadStatus(downloadFailedReadResponse, err)
- }
- // Filename
- if filename == "" {
- filename = filenameFromURL(response.Request.URL.String())
- for key, iHeader := range response.Header {
- if key == "Content-Disposition" {
- _, params, err := mime.ParseMediaType(iHeader[0])
- if err == nil {
- newFilename, err := url.QueryUnescape(params["filename"])
- if err != nil {
- newFilename = params["filename"]
- }
- if newFilename != "" {
- filename = newFilename
- }
- }
- }
- }
- }
- extension := strings.ToLower(filepath.Ext(filename))
- contentType := http.DetectContentType(bodyOfResp)
- contentTypeParts := strings.Split(contentType, "/")
- contentTypeFound := contentTypeParts[0]
- parsedURL, err := url.Parse(inputURL)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Error while parsing url:\t%s", err))
- }
- // Check extension
- if stringInSlice(extension, *channelConfig.ExtensionBlacklist) || stringInSlice(extension, []string{".com", ".net", ".org"}) {
- if !historyCmd {
- log.Println(logPrefixFileSkip, color.GreenString("Unpermitted extension (%s) found at %s", extension, inputURL))
- }
- return mDownloadStatus(downloadSkippedUnpermittedExtension)
- }
- // Fix content type
- if stringInSlice(extension, []string{".mov"}) ||
- stringInSlice(extension, []string{".mp4"}) ||
- stringInSlice(extension, []string{".webm"}) {
- contentTypeFound = "video"
- } else if stringInSlice(extension, []string{".psd"}) ||
- stringInSlice(extension, []string{".nef"}) ||
- stringInSlice(extension, []string{".dng"}) ||
- stringInSlice(extension, []string{".tif"}) ||
- stringInSlice(extension, []string{".tiff"}) {
- contentTypeFound = "image"
- }
- // Filename extension fix
- if filepath.Ext(filename) == "" {
- possibleExtension, _ := mime.ExtensionsByType(contentType)
- if len(possibleExtension) > 0 {
- filename += possibleExtension[0]
- }
- }
- // Filename validation
- if !regexFilename.MatchString(filename) {
- filename = "InvalidFilename"
- possibleExtension, _ := mime.ExtensionsByType(contentType)
- if len(possibleExtension) > 0 {
- filename += possibleExtension[0]
- }
- }
- // Check Domain
- if channelConfig.DomainBlacklist != nil {
- if parsedURL != nil {
- if stringInSlice(parsedURL.Hostname(), *channelConfig.DomainBlacklist) {
- if !historyCmd {
- log.Println(logPrefixFileSkip, color.GreenString("Unpermitted domain (%s) found at %s", parsedURL.Hostname(), inputURL))
- }
- return mDownloadStatus(downloadSkippedUnpermittedDomain)
- }
- }
- }
- // Check content type
- if !((*channelConfig.SaveImages && contentTypeFound == "image") ||
- (*channelConfig.SaveVideos && contentTypeFound == "video") ||
- (*channelConfig.SaveAudioFiles && contentTypeFound == "audio") ||
- (*channelConfig.SaveTextFiles && contentTypeFound == "text") ||
- (*channelConfig.SaveOtherFiles && contentTypeFound == "application")) {
- if !historyCmd {
- log.Println(logPrefixFileSkip, color.GreenString("Unpermitted filetype (%s) found at %s", contentTypeFound, inputURL))
- }
- return mDownloadStatus(downloadSkippedUnpermittedType)
- }
- // Duplicate Image Filter
- if config.FilterDuplicateImages && contentTypeFound == "image" && extension != ".gif" && extension != ".webp" {
- img, _, err := image.Decode(bytes.NewReader(bodyOfResp))
- if err != nil {
- log.Println(color.HiRedString("Error converting buffer to image for hashing:\t%s", err))
- } else {
- hash, _ := duplo.CreateHash(img)
- matches := imgStore.Query(hash)
- sort.Sort(matches)
- for _, match := range matches {
- /*if config.DebugOutput {
- log.Println(color.YellowString("Similarity Score: %f", match.Score))
- }*/
- if match.Score < config.FilterDuplicateImagesThreshold {
- log.Println(logPrefixFileSkip, color.GreenString("Duplicate detected (Score of %f) found at %s", match.Score, inputURL))
- return mDownloadStatus(downloadSkippedDetectedDuplicate)
- }
- }
- imgStore.Add(cachedDownloadID, hash)
- }
- }
- // Names
- sourceChannelName := message.ChannelID
- sourceName := "UNKNOWN"
- sourceChannel, err := bot.State.Channel(message.ChannelID)
- if sourceChannel != nil {
- if sourceChannel.Name != "" {
- sourceChannelName = sourceChannel.Name
- }
- switch sourceChannel.Type {
- case discordgo.ChannelTypeGuildText:
- if sourceChannel.GuildID != "" {
- sourceGuild, _ := bot.State.Guild(sourceChannel.GuildID)
- if sourceGuild != nil && sourceGuild.Name != "" {
- sourceName = "\"" + sourceGuild.Name + "\""
- }
- }
- case discordgo.ChannelTypeDM:
- sourceName = "Direct Messages"
- case discordgo.ChannelTypeGroupDM:
- sourceName = "Group Messages"
- }
- }
- subfolder := ""
- if message.Author != nil {
- // Subfolder Division - Server Nesting
- if *channelConfig.DivideFoldersByServer {
- subfolderSuffix := ""
- if sourceName != "" && sourceName != "UNKNOWN" {
- subfolderSuffix = sourceName
- for _, key := range pathBlacklist {
- subfolderSuffix = strings.ReplaceAll(subfolderSuffix, key, "")
- }
- }
- if subfolderSuffix != "" {
- subfolderSuffix = subfolderSuffix + string(os.PathSeparator)
- subfolder = subfolder + subfolderSuffix
- // Create folder.
- err := os.MkdirAll(path+subfolder, 0755)
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Error while creating server subfolder \"%s\": %s", path, err))
- return mDownloadStatus(downloadFailedCreatingSubfolder, err)
- }
- }
- }
- // Subfolder Division - Channel Nesting
- if *channelConfig.DivideFoldersByChannel {
- subfolderSuffix := ""
- if sourceChannelName != "" {
- subfolderSuffix = sourceChannelName
- for _, key := range pathBlacklist {
- subfolderSuffix = strings.ReplaceAll(subfolderSuffix, key, "")
- }
- }
- if subfolderSuffix != "" {
- subfolder = subfolder + subfolderSuffix + string(os.PathSeparator)
- // Create folder.
- err := os.MkdirAll(path+subfolder, 0755)
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Error while creating channel subfolder \"%s\": %s", path, err))
- return mDownloadStatus(downloadFailedCreatingSubfolder, err)
- }
- }
- }
- // Subfolder Division - User Nesting
- if *channelConfig.DivideFoldersByUser {
- subfolderSuffix := message.Author.ID
- if message.Author.Username != "" {
- subfolderSuffix = message.Author.Username + "#" + message.Author.Discriminator
- for _, key := range pathBlacklist {
- subfolderSuffix = strings.ReplaceAll(subfolderSuffix, key, "")
- }
- }
- if subfolderSuffix != "" {
- subfolder = subfolder + subfolderSuffix + string(os.PathSeparator)
- // Create folder.
- err := os.MkdirAll(path+subfolder, 0755)
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Error while creating user subfolder \"%s\": %s", path, err))
- return mDownloadStatus(downloadFailedCreatingSubfolder, err)
- }
- }
- }
- }
- // Subfolder Division - Content Type
- if *channelConfig.DivideFoldersByType && message.Author != nil {
- subfolderSuffix := ""
- switch contentTypeFound {
- case "image":
- subfolderSuffix = "images"
- case "video":
- subfolderSuffix = "videos"
- case "audio":
- subfolderSuffix = "audio"
- case "text":
- subfolderSuffix = "text"
- case "application":
- subfolderSuffix = "applications"
- }
- if subfolderSuffix != "" {
- subfolder = subfolder + subfolderSuffix + string(os.PathSeparator)
- // Create folder.
- err := os.MkdirAll(path+subfolder, 0755)
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Error while creating type subfolder \"%s\": %s", path+subfolder, err))
- return mDownloadStatus(downloadFailedCreatingSubfolder, err)
- }
- }
- }
- // Format filename/path
- filenameDateFormat := config.FilenameDateFormat
- if channelConfig.OverwriteFilenameDateFormat != nil {
- if *channelConfig.OverwriteFilenameDateFormat != "" {
- filenameDateFormat = *channelConfig.OverwriteFilenameDateFormat
- }
- }
- messageTime := time.Now()
- if message.Timestamp != "" {
- messageTimestamp, err := message.Timestamp.Parse()
- if err == nil {
- messageTime = messageTimestamp
- }
- }
- completePath := path + subfolder + messageTime.Format(filenameDateFormat) + filename
- // Check if exists
- if _, err := os.Stat(completePath); err == nil {
- if *channelConfig.SavePossibleDuplicates {
- tmpPath := completePath
- i := 1
- for {
- // Append number to name
- completePath = tmpPath[0:len(tmpPath)-len(filepathExtension(tmpPath))] +
- "-" + strconv.Itoa(i) + filepathExtension(tmpPath)
- if _, err := os.Stat(completePath); os.IsNotExist(err) {
- break
- }
- i = i + 1
- }
- if !historyCmd {
- log.Println(color.GreenString("Matching filenames, possible duplicate? Saving \"%s\" as \"%s\" instead", tmpPath, completePath))
- }
- } else {
- if !historyCmd {
- log.Println(logPrefixFileSkip, color.GreenString("Matching filenames, possible duplicate..."))
- }
- return mDownloadStatus(downloadSkippedDuplicate)
- }
- }
- // Write
- err = ioutil.WriteFile(completePath, bodyOfResp, 0644)
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Error while writing file to disk \"%s\": %s", inputURL, err))
- return mDownloadStatus(downloadFailedWritingFile, err)
- }
- // Change file time
- err = os.Chtimes(completePath, fileTime, fileTime)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Error while changing metadata date \"%s\": %s", inputURL, err))
- }
- // Write duration
- if config.DebugOutput && !historyCmd {
- log.Println(logPrefixDebug, color.YellowString("#%d - %s to save.", thisDownloadID, durafmt.ParseShort(time.Since(downloadTime)).String()))
- }
- writeTime := time.Now()
- // Output
- log.Println(logPrefix + color.HiGreenString("SAVED %s sent in %s#%s to \"%s\"", strings.ToUpper(contentTypeFound), sourceName, sourceChannelName, completePath))
- userID := user.ID
- if message.Author != nil {
- userID = message.Author.ID
- }
- // Store in db
- err = dbInsertDownload(&download{
- URL: inputURL,
- Time: time.Now(),
- Destination: completePath,
- Filename: filename,
- ChannelID: message.ChannelID,
- UserID: userID,
- })
- if err != nil {
- log.Println(logPrefixErrorHere, color.HiRedString("Error writing to database: %s", err))
- return mDownloadStatus(downloadFailedWritingDatabase, err)
- }
- // Storage & output duration
- if config.DebugOutput && !historyCmd {
- log.Println(logPrefixDebug, color.YellowString("#%d - %s to update database.", thisDownloadID, durafmt.ParseShort(time.Since(writeTime)).String()))
- }
- finishTime := time.Now()
- // React
- if !historyCmd && *channelConfig.ReactWhenDownloaded && message.Author != nil {
- reaction := ""
- if *channelConfig.ReactWhenDownloadedEmoji == "" {
- if message.GuildID != "" {
- guild, err := bot.State.Guild(message.GuildID)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Error fetching guild state for emojis from %s: %s", message.GuildID, err))
- } else {
- emojis := guild.Emojis
- if len(emojis) > 1 {
- for {
- rand.Seed(time.Now().UnixNano())
- chosenEmoji := emojis[rand.Intn(len(emojis))]
- formattedEmoji := chosenEmoji.APIName()
- if !chosenEmoji.Animated && !stringInSlice(formattedEmoji, *channelConfig.BlacklistReactEmojis) {
- reaction = formattedEmoji
- break
- }
- }
- } else {
- reaction = defaultReact
- }
- }
- } else {
- reaction = defaultReact
- }
- } else {
- reaction = *channelConfig.ReactWhenDownloadedEmoji
- }
- // Add Reaction
- if hasPerms(message.ChannelID, discordgo.PermissionAddReactions) {
- err = bot.MessageReactionAdd(message.ChannelID, message.ID, reaction)
- if err != nil {
- log.Println(logPrefixErrorHere, color.RedString("Error adding reaction to message: %s", err))
- }
- } else {
- log.Println(logPrefixErrorHere, color.RedString("Bot does not have permission to add reactions in %s", message.ChannelID))
- }
- // React duration
- if config.DebugOutput {
- log.Println(logPrefixDebug, color.YellowString("#%d - %s to react with \"%s\".", thisDownloadID, durafmt.ParseShort(time.Since(finishTime)).String(), reaction))
- }
- }
- if !historyCmd {
- timeLastUpdated = time.Now()
- if *channelConfig.UpdatePresence {
- updateDiscordPresence()
- }
- }
- if config.DebugOutput && !historyCmd {
- log.Println(logPrefixDebug, color.YellowString("#%d - %s total.", thisDownloadID, time.Since(startTime)))
- }
- // Save All Links to File
- if channelConfig.SaveAllLinksToFile != nil {
- filepath := *channelConfig.SaveAllLinksToFile
- if filepath != "" {
- f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
- if err != nil {
- log.Println(color.RedString("[SaveAllLinksToFile] Failed to open file:\t%s", err))
- f.Close()
- }
- defer f.Close()
- var addedContent string
- rawLinks := getRawLinks(message)
- for _, rawLink := range rawLinks {
- addedContent = addedContent + "\n" + rawLink.Link
- }
- if _, err = f.WriteString(addedContent); err != nil {
- log.Println(color.RedString("[SaveAllLinksToFile] Failed to append file:\t%s", err))
- }
- }
- }
- if thisDownloadID > 0 {
- // Filter Duplicate Images
- if config.FilterDuplicateImages {
- encodedStore, err := imgStore.GobEncode()
- if err != nil {
- log.Println(color.HiRedString("Failed to encode imgStore:\t%s"))
- } else {
- f, err := os.OpenFile(imgStorePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
- if err != nil {
- log.Println(color.HiRedString("Failed to open imgStore file:\t%s"))
- }
- _, err = f.Write(encodedStore)
- if err != nil {
- log.Println(color.HiRedString("Failed to update imgStore file:\t%s"))
- }
- err = f.Close()
- if err != nil {
- log.Println(color.HiRedString("Failed to close imgStore file:\t%s"))
- }
- }
- }
- }
- return mDownloadStatus(downloadSuccess)
- }
- return mDownloadStatus(downloadIgnored)
- }
|