downloads.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. package main
  2. import (
  3. "bytes"
  4. "fmt"
  5. "image"
  6. "io/ioutil"
  7. "log"
  8. "math/rand"
  9. "mime"
  10. "net/http"
  11. "net/url"
  12. "os"
  13. "path/filepath"
  14. "sort"
  15. "strconv"
  16. "strings"
  17. "time"
  18. "github.com/bwmarrin/discordgo"
  19. "github.com/fatih/color"
  20. "github.com/hako/durafmt"
  21. "github.com/rivo/duplo"
  22. "mvdan.cc/xurls/v2"
  23. )
  24. type download struct {
  25. URL string
  26. Time time.Time
  27. Destination string
  28. Filename string
  29. ChannelID string
  30. UserID string
  31. }
  32. type downloadStatus int
  33. const (
  34. downloadSuccess downloadStatus = iota
  35. downloadIgnored
  36. downloadSkipped
  37. downloadSkippedDuplicate
  38. downloadSkippedUnpermittedDomain
  39. downloadSkippedUnpermittedType
  40. downloadSkippedUnpermittedExtension
  41. downloadSkippedDetectedDuplicate
  42. downloadFailed
  43. downloadFailedInvalidSource
  44. downloadFailedInvalidPath
  45. downloadFailedCreatingFolder
  46. downloadFailedRequesting
  47. downloadFailedDownloadingResponse
  48. downloadFailedReadResponse
  49. downloadFailedCreatingSubfolder
  50. downloadFailedWritingFile
  51. downloadFailedWritingDatabase
  52. )
  53. type downloadStatusStruct struct {
  54. Status downloadStatus
  55. Error error
  56. }
  57. func mDownloadStatus(status downloadStatus, _error ...error) downloadStatusStruct {
  58. if len(_error) == 0 {
  59. return downloadStatusStruct{
  60. Status: status,
  61. Error: nil,
  62. }
  63. }
  64. return downloadStatusStruct{
  65. Status: status,
  66. Error: _error[0],
  67. }
  68. }
  69. func getDownloadStatusString(status downloadStatus) string {
  70. switch status {
  71. case downloadSuccess:
  72. return "Download Succeeded"
  73. //
  74. case downloadIgnored:
  75. return "Download Ignored"
  76. //
  77. case downloadSkipped:
  78. return "Download Skipped"
  79. case downloadSkippedDuplicate:
  80. return "Download Skipped - Duplicate"
  81. case downloadSkippedUnpermittedDomain:
  82. return "Download Skipped - Unpermitted Domain"
  83. case downloadSkippedUnpermittedType:
  84. return "Download Skipped - Unpermitted File Type"
  85. case downloadSkippedUnpermittedExtension:
  86. return "Download Skipped - Unpermitted File Extension"
  87. case downloadSkippedDetectedDuplicate:
  88. return "Download Skipped - Detected Duplicate"
  89. //
  90. case downloadFailed:
  91. return "Download Failed"
  92. case downloadFailedInvalidSource:
  93. return "Download Failed - Invalid Source"
  94. case downloadFailedInvalidPath:
  95. return "Download Failed - Invalid Path"
  96. case downloadFailedCreatingFolder:
  97. return "Download Failed - Error Creating Folder"
  98. case downloadFailedRequesting:
  99. return "Download Failed - Error Requesting URL Data"
  100. case downloadFailedDownloadingResponse:
  101. return "Download Failed - Error Downloading URL Response"
  102. case downloadFailedReadResponse:
  103. return "Download Failed - Error Reading URL Response"
  104. case downloadFailedCreatingSubfolder:
  105. return "Download Failed - Error Creating Subfolder for Type"
  106. case downloadFailedWritingFile:
  107. return "Download Failed - Error Writing File"
  108. case downloadFailedWritingDatabase:
  109. return "Download Failed - Error Writing to Database"
  110. }
  111. return "Unknown Error"
  112. }
  113. // Trim duplicate links in link list
  114. func trimDuplicateLinks(fileItems []*fileItem) []*fileItem {
  115. var result []*fileItem
  116. seen := map[string]bool{}
  117. for _, item := range fileItems {
  118. if seen[item.Link] {
  119. continue
  120. }
  121. seen[item.Link] = true
  122. result = append(result, item)
  123. }
  124. return result
  125. }
  126. func getRawLinks(m *discordgo.Message) []*fileItem {
  127. var links []*fileItem
  128. if m.Author == nil {
  129. m.Author = new(discordgo.User)
  130. }
  131. for _, attachment := range m.Attachments {
  132. links = append(links, &fileItem{
  133. Link: attachment.URL,
  134. Filename: attachment.Filename,
  135. })
  136. }
  137. foundLinks := xurls.Strict().FindAllString(m.Content, -1)
  138. for _, foundLink := range foundLinks {
  139. links = append(links, &fileItem{
  140. Link: foundLink,
  141. })
  142. }
  143. for _, embed := range m.Embeds {
  144. if embed.URL != "" {
  145. links = append(links, &fileItem{
  146. Link: embed.URL,
  147. })
  148. }
  149. // Removing for now as this causes it to try and pull shit from things like YouTube descriptions
  150. /*if embed.Description != "" {
  151. foundLinks = xurls.Strict().FindAllString(embed.Description, -1)
  152. for _, foundLink := range foundLinks {
  153. links = append(links, &fileItem{
  154. Link: foundLink,
  155. })
  156. }
  157. }*/
  158. if embed.Image != nil && embed.Image.URL != "" {
  159. links = append(links, &fileItem{
  160. Link: embed.Image.URL,
  161. })
  162. }
  163. if embed.Video != nil && embed.Video.URL != "" {
  164. links = append(links, &fileItem{
  165. Link: embed.Video.URL,
  166. })
  167. }
  168. }
  169. return links
  170. }
  171. func getDownloadLinks(inputURL string, channelID string) map[string]string {
  172. logPrefixErrorHere := color.HiRedString("[getDownloadLinks]")
  173. /* TODO: Download Support...
  174. - TikTok: Tried, once the connection is closed the cdn URL is rendered invalid
  175. - 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.
  176. - Facebook Videos: Previously supported but they split mp4 into separate audio and video streams
  177. */
  178. if regexUrlTwitter.MatchString(inputURL) {
  179. links, err := getTwitterUrls(inputURL)
  180. if err != nil {
  181. if !strings.Contains(err.Error(), "suspended") {
  182. log.Println(logPrefixErrorHere, color.RedString("Twitter Media fetch failed for %s -- %s", inputURL, err))
  183. }
  184. } else if len(links) > 0 {
  185. return trimDownloadedLinks(links, channelID)
  186. }
  187. }
  188. if regexUrlTwitterStatus.MatchString(inputURL) {
  189. links, err := getTwitterStatusUrls(inputURL, channelID)
  190. if err != nil {
  191. if !strings.Contains(err.Error(), "suspended") && !strings.Contains(err.Error(), "No status found") {
  192. log.Println(logPrefixErrorHere, color.RedString("Twitter Status fetch failed for %s -- %s", inputURL, err))
  193. }
  194. } else if len(links) > 0 {
  195. return trimDownloadedLinks(links, channelID)
  196. }
  197. }
  198. if regexUrlInstagram.MatchString(inputURL) {
  199. links, err := getInstagramUrls(inputURL)
  200. if err != nil {
  201. log.Println(logPrefixErrorHere, color.RedString("Instagram fetch failed for %s -- %s", inputURL, err))
  202. } else if len(links) > 0 {
  203. return trimDownloadedLinks(links, channelID)
  204. }
  205. }
  206. if regexUrlImgurSingle.MatchString(inputURL) {
  207. links, err := getImgurSingleUrls(inputURL)
  208. if err != nil {
  209. log.Println(logPrefixErrorHere, color.RedString("Imgur Media fetch failed for %s -- %s", inputURL, err))
  210. } else if len(links) > 0 {
  211. return trimDownloadedLinks(links, channelID)
  212. }
  213. }
  214. if regexUrlImgurAlbum.MatchString(inputURL) {
  215. links, err := getImgurAlbumUrls(inputURL)
  216. if err != nil {
  217. log.Println(logPrefixErrorHere, color.RedString("Imgur Album fetch failed for %s -- %s", inputURL, err))
  218. } else if len(links) > 0 {
  219. return trimDownloadedLinks(links, channelID)
  220. }
  221. }
  222. if regexUrlStreamable.MatchString(inputURL) {
  223. links, err := getStreamableUrls(inputURL)
  224. if err != nil {
  225. log.Println(logPrefixErrorHere, color.RedString("Streamable fetch failed for %s -- %s", inputURL, err))
  226. } else if len(links) > 0 {
  227. return trimDownloadedLinks(links, channelID)
  228. }
  229. }
  230. if regexUrlGfycat.MatchString(inputURL) {
  231. links, err := getGfycatUrls(inputURL)
  232. if err != nil {
  233. log.Println(logPrefixErrorHere, color.RedString("Gfycat fetch failed for %s -- %s", inputURL, err))
  234. } else if len(links) > 0 {
  235. return trimDownloadedLinks(links, channelID)
  236. }
  237. }
  238. if regexUrlFlickrPhoto.MatchString(inputURL) {
  239. links, err := getFlickrPhotoUrls(inputURL)
  240. if err != nil {
  241. log.Println(logPrefixErrorHere, color.RedString("Flickr Photo fetch failed for %s -- %s", inputURL, err))
  242. } else if len(links) > 0 {
  243. return trimDownloadedLinks(links, channelID)
  244. }
  245. }
  246. if regexUrlFlickrAlbum.MatchString(inputURL) {
  247. links, err := getFlickrAlbumUrls(inputURL)
  248. if err != nil {
  249. log.Println(logPrefixErrorHere, color.RedString("Flickr Album fetch failed for %s -- %s", inputURL, err))
  250. } else if len(links) > 0 {
  251. return trimDownloadedLinks(links, channelID)
  252. }
  253. }
  254. if regexUrlFlickrAlbumShort.MatchString(inputURL) {
  255. links, err := getFlickrAlbumShortUrls(inputURL)
  256. if err != nil {
  257. log.Println(logPrefixErrorHere, color.RedString("Flickr Album (short) fetch failed for %s -- %s", inputURL, err))
  258. } else if len(links) > 0 {
  259. return trimDownloadedLinks(links, channelID)
  260. }
  261. }
  262. if config.Credentials.GoogleDriveCredentialsJSON != "" {
  263. if regexUrlGoogleDrive.MatchString(inputURL) {
  264. links, err := getGoogleDriveUrls(inputURL)
  265. if err != nil {
  266. log.Println(logPrefixErrorHere, color.RedString("Google Drive Album URL for %s -- %s", inputURL, err))
  267. } else if len(links) > 0 {
  268. return trimDownloadedLinks(links, channelID)
  269. }
  270. }
  271. if regexUrlGoogleDriveFolder.MatchString(inputURL) {
  272. links, err := getGoogleDriveFolderUrls(inputURL)
  273. if err != nil {
  274. log.Println(logPrefixErrorHere, color.RedString("Google Drive Folder URL for %s -- %s", inputURL, err))
  275. } else if len(links) > 0 {
  276. return trimDownloadedLinks(links, channelID)
  277. }
  278. }
  279. }
  280. if regexUrlTistory.MatchString(inputURL) {
  281. links, err := getTistoryUrls(inputURL)
  282. if err != nil {
  283. log.Println(logPrefixErrorHere, color.RedString("Tistory URL failed for %s -- %s", inputURL, err))
  284. } else if len(links) > 0 {
  285. return trimDownloadedLinks(links, channelID)
  286. }
  287. }
  288. if regexUrlTistoryLegacy.MatchString(inputURL) {
  289. links, err := getLegacyTistoryUrls(inputURL)
  290. if err != nil {
  291. log.Println(logPrefixErrorHere, color.RedString("Legacy Tistory URL failed for %s -- %s", inputURL, err))
  292. } else if len(links) > 0 {
  293. return trimDownloadedLinks(links, channelID)
  294. }
  295. }
  296. if regexUrlRedditPost.MatchString(inputURL) {
  297. links, err := getRedditPostUrls(inputURL)
  298. if err != nil {
  299. log.Println(logPrefixErrorHere, color.RedString("Reddit Post URL failed for %s -- %s", inputURL, err))
  300. } else if len(links) > 0 {
  301. return trimDownloadedLinks(links, channelID)
  302. }
  303. }
  304. if regexUrlMastodonPost1.MatchString(inputURL) || regexUrlMastodonPost2.MatchString(inputURL) {
  305. links, err := getMastodonPostUrls(inputURL)
  306. if err != nil {
  307. log.Println(logPrefixErrorHere, color.RedString("Mastodon Post URL failed for %s -- %s", inputURL, err))
  308. } else if len(links) > 0 {
  309. return trimDownloadedLinks(links, channelID)
  310. }
  311. }
  312. // The original project has this as an option,
  313. if regexUrlPossibleTistorySite.MatchString(inputURL) {
  314. links, err := getPossibleTistorySiteUrls(inputURL)
  315. if err != nil {
  316. log.Println(logPrefixErrorHere, color.RedString("Checking for Tistory site failed for %s -- %s", inputURL, err))
  317. } else if len(links) > 0 {
  318. return trimDownloadedLinks(links, channelID)
  319. }
  320. }
  321. if strings.HasPrefix(inputURL, "https://cdn.discordapp.com/emojis/") {
  322. return nil
  323. }
  324. // Try without queries
  325. parsedURL, err := url.Parse(inputURL)
  326. if err == nil {
  327. parsedURL.RawQuery = ""
  328. inputURLWithoutQueries := parsedURL.String()
  329. if inputURLWithoutQueries != inputURL {
  330. return trimDownloadedLinks(getDownloadLinks(inputURLWithoutQueries, channelID), channelID)
  331. }
  332. }
  333. return trimDownloadedLinks(map[string]string{inputURL: ""}, channelID)
  334. }
  335. func getFileLinks(m *discordgo.Message) []*fileItem {
  336. var fileItems []*fileItem
  337. linkTime, err := m.Timestamp.Parse()
  338. if err != nil {
  339. linkTime = time.Now()
  340. }
  341. rawLinks := getRawLinks(m)
  342. for _, rawLink := range rawLinks {
  343. downloadLinks := getDownloadLinks(
  344. rawLink.Link,
  345. m.ChannelID,
  346. )
  347. for link, filename := range downloadLinks {
  348. if rawLink.Filename != "" {
  349. filename = rawLink.Filename
  350. }
  351. fileItems = append(fileItems, &fileItem{
  352. Link: link,
  353. Filename: filename,
  354. Time: linkTime,
  355. })
  356. }
  357. }
  358. fileItems = trimDuplicateLinks(fileItems)
  359. return fileItems
  360. }
  361. func startDownload(inputURL string, filename string, path string, message *discordgo.Message, fileTime time.Time, historyCmd bool) downloadStatusStruct {
  362. status := mDownloadStatus(downloadFailed)
  363. logPrefixErrorHere := color.HiRedString("[startDownload]")
  364. for i := 0; i < config.DownloadRetryMax; i++ {
  365. status = tryDownload(inputURL, filename, path, message, fileTime, historyCmd)
  366. if status.Status < downloadFailed { // Success or Skip
  367. break
  368. } else {
  369. time.Sleep(5 * time.Second)
  370. }
  371. }
  372. if status.Status >= downloadFailed && !historyCmd { // Any kind of failure
  373. log.Println(logPrefixErrorHere, color.RedString("Gave up on downloading %s after %d failed attempts...\t%s", inputURL, config.DownloadRetryMax, getDownloadStatusString(status.Status)))
  374. if isChannelRegistered(message.ChannelID) {
  375. channelConfig := getChannelConfig(message.ChannelID)
  376. if !historyCmd && *channelConfig.ErrorMessages {
  377. content := fmt.Sprintf(
  378. "Gave up trying to download\n<%s>\nafter %d failed attempts...\n\n``%s``",
  379. inputURL, config.DownloadRetryMax, getDownloadStatusString(status.Status))
  380. if status.Error != nil {
  381. content += fmt.Sprintf("\n```ERROR: %s```", status.Error)
  382. }
  383. // Failure Notice
  384. if hasPerms(message.ChannelID, discordgo.PermissionSendMessages) {
  385. _, err := bot.ChannelMessageSendComplex(message.ChannelID,
  386. &discordgo.MessageSend{
  387. Content: fmt.Sprintf("<@!%s>", message.Author.ID),
  388. Embed: buildEmbed(message.ChannelID, "Download Failure", content),
  389. })
  390. if err != nil {
  391. log.Println(logPrefixErrorHere, color.HiRedString("Failed to send failure message to %s: %s", message.ChannelID, err))
  392. }
  393. } else {
  394. log.Println(logPrefixErrorHere, color.HiRedString(fmtBotSendPerm, message.ChannelID))
  395. }
  396. }
  397. if status.Error != nil {
  398. logErrorMessage(fmt.Sprintf("%s...\n%s", getDownloadStatusString(status.Status), status.Error))
  399. }
  400. }
  401. }
  402. return status
  403. }
  404. func tryDownload(inputURL string, filename string, path string, message *discordgo.Message, fileTime time.Time, historyCmd bool) downloadStatusStruct {
  405. cachedDownloadID++
  406. thisDownloadID := cachedDownloadID
  407. startTime := time.Now()
  408. logPrefixErrorHere := color.HiRedString("[tryDownload]")
  409. logPrefix := ""
  410. if historyCmd {
  411. logPrefix = logPrefixHistory + " "
  412. }
  413. if stringInSlice(message.ChannelID, getAllChannels()) {
  414. var channelConfig configurationChannel
  415. if isChannelRegistered(message.ChannelID) {
  416. channelConfig = getChannelConfig(message.ChannelID)
  417. } else {
  418. channelDefault(&channelConfig)
  419. }
  420. var err error
  421. // Source validation
  422. _, err = url.ParseRequestURI(inputURL)
  423. if err != nil {
  424. return mDownloadStatus(downloadFailedInvalidSource, err)
  425. }
  426. // Clean/fix path
  427. if path == "" || path == string(os.PathSeparator) {
  428. log.Println(logPrefixErrorHere, color.HiRedString("Destination cannot be empty path..."))
  429. return mDownloadStatus(downloadFailedInvalidPath, err)
  430. }
  431. if !strings.HasSuffix(path, string(os.PathSeparator)) {
  432. path = path + string(os.PathSeparator)
  433. }
  434. // Create folder
  435. err = os.MkdirAll(path, 0755)
  436. if err != nil {
  437. log.Println(logPrefixErrorHere, color.HiRedString("Error while creating destination folder \"%s\": %s", path, err))
  438. return mDownloadStatus(downloadFailedCreatingFolder, err)
  439. }
  440. // Request
  441. timeout := time.Duration(time.Duration(config.DownloadTimeout) * time.Second)
  442. client := &http.Client{
  443. Timeout: timeout,
  444. }
  445. request, err := http.NewRequest("GET", inputURL, nil)
  446. 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")
  447. if err != nil {
  448. log.Println(logPrefixErrorHere, color.HiRedString("Error while requesting \"%s\": %s", inputURL, err))
  449. return mDownloadStatus(downloadFailedRequesting, err)
  450. }
  451. request.Header.Add("Accept-Encoding", "identity")
  452. response, err := client.Do(request)
  453. if err != nil {
  454. if !strings.Contains(err.Error(), "no such host") && !strings.Contains(err.Error(), "connection refused") {
  455. log.Println(logPrefixErrorHere, color.HiRedString("Error while receiving response from \"%s\": %s", inputURL, err))
  456. }
  457. return mDownloadStatus(downloadFailedDownloadingResponse, err)
  458. }
  459. defer response.Body.Close()
  460. // Download duration
  461. if config.DebugOutput && !historyCmd {
  462. log.Println(logPrefixDebug, color.YellowString("#%d - %s to download.", thisDownloadID, durafmt.ParseShort(time.Since(startTime)).String()))
  463. }
  464. downloadTime := time.Now()
  465. // Read
  466. bodyOfResp, err := ioutil.ReadAll(response.Body)
  467. if err != nil {
  468. log.Println(logPrefixErrorHere, color.HiRedString("Could not read response from \"%s\": %s", inputURL, err))
  469. return mDownloadStatus(downloadFailedReadResponse, err)
  470. }
  471. // Filename
  472. if filename == "" {
  473. filename = filenameFromURL(response.Request.URL.String())
  474. for key, iHeader := range response.Header {
  475. if key == "Content-Disposition" {
  476. _, params, err := mime.ParseMediaType(iHeader[0])
  477. if err == nil {
  478. newFilename, err := url.QueryUnescape(params["filename"])
  479. if err != nil {
  480. newFilename = params["filename"]
  481. }
  482. if newFilename != "" {
  483. filename = newFilename
  484. }
  485. }
  486. }
  487. }
  488. }
  489. extension := strings.ToLower(filepath.Ext(filename))
  490. contentType := http.DetectContentType(bodyOfResp)
  491. contentTypeParts := strings.Split(contentType, "/")
  492. contentTypeFound := contentTypeParts[0]
  493. parsedURL, err := url.Parse(inputURL)
  494. if err != nil {
  495. log.Println(logPrefixErrorHere, color.RedString("Error while parsing url:\t%s", err))
  496. }
  497. // Check extension
  498. if stringInSlice(extension, *channelConfig.ExtensionBlacklist) || stringInSlice(extension, []string{".com", ".net", ".org"}) {
  499. if !historyCmd {
  500. log.Println(logPrefixFileSkip, color.GreenString("Unpermitted extension (%s) found at %s", extension, inputURL))
  501. }
  502. return mDownloadStatus(downloadSkippedUnpermittedExtension)
  503. }
  504. // Fix content type
  505. if stringInSlice(extension, []string{".mov"}) ||
  506. stringInSlice(extension, []string{".mp4"}) ||
  507. stringInSlice(extension, []string{".webm"}) {
  508. contentTypeFound = "video"
  509. } else if stringInSlice(extension, []string{".psd"}) ||
  510. stringInSlice(extension, []string{".nef"}) ||
  511. stringInSlice(extension, []string{".dng"}) ||
  512. stringInSlice(extension, []string{".tif"}) ||
  513. stringInSlice(extension, []string{".tiff"}) {
  514. contentTypeFound = "image"
  515. }
  516. // Filename extension fix
  517. if filepath.Ext(filename) == "" {
  518. possibleExtension, _ := mime.ExtensionsByType(contentType)
  519. if len(possibleExtension) > 0 {
  520. filename += possibleExtension[0]
  521. }
  522. }
  523. // Filename validation
  524. if !regexFilename.MatchString(filename) {
  525. filename = "InvalidFilename"
  526. possibleExtension, _ := mime.ExtensionsByType(contentType)
  527. if len(possibleExtension) > 0 {
  528. filename += possibleExtension[0]
  529. }
  530. }
  531. // Check Domain
  532. if channelConfig.DomainBlacklist != nil {
  533. if parsedURL != nil {
  534. if stringInSlice(parsedURL.Hostname(), *channelConfig.DomainBlacklist) {
  535. if !historyCmd {
  536. log.Println(logPrefixFileSkip, color.GreenString("Unpermitted domain (%s) found at %s", parsedURL.Hostname(), inputURL))
  537. }
  538. return mDownloadStatus(downloadSkippedUnpermittedDomain)
  539. }
  540. }
  541. }
  542. // Check content type
  543. if !((*channelConfig.SaveImages && contentTypeFound == "image") ||
  544. (*channelConfig.SaveVideos && contentTypeFound == "video") ||
  545. (*channelConfig.SaveAudioFiles && contentTypeFound == "audio") ||
  546. (*channelConfig.SaveTextFiles && contentTypeFound == "text") ||
  547. (*channelConfig.SaveOtherFiles && contentTypeFound == "application")) {
  548. if !historyCmd {
  549. log.Println(logPrefixFileSkip, color.GreenString("Unpermitted filetype (%s) found at %s", contentTypeFound, inputURL))
  550. }
  551. return mDownloadStatus(downloadSkippedUnpermittedType)
  552. }
  553. // Duplicate Image Filter
  554. if config.FilterDuplicateImages && contentTypeFound == "image" && extension != ".gif" && extension != ".webp" {
  555. img, _, err := image.Decode(bytes.NewReader(bodyOfResp))
  556. if err != nil {
  557. log.Println(color.HiRedString("Error converting buffer to image for hashing:\t%s", err))
  558. } else {
  559. hash, _ := duplo.CreateHash(img)
  560. matches := imgStore.Query(hash)
  561. sort.Sort(matches)
  562. for _, match := range matches {
  563. /*if config.DebugOutput {
  564. log.Println(color.YellowString("Similarity Score: %f", match.Score))
  565. }*/
  566. if match.Score < config.FilterDuplicateImagesThreshold {
  567. log.Println(logPrefixFileSkip, color.GreenString("Duplicate detected (Score of %f) found at %s", match.Score, inputURL))
  568. return mDownloadStatus(downloadSkippedDetectedDuplicate)
  569. }
  570. }
  571. imgStore.Add(cachedDownloadID, hash)
  572. }
  573. }
  574. // Names
  575. sourceChannelName := message.ChannelID
  576. sourceName := "UNKNOWN"
  577. sourceChannel, err := bot.State.Channel(message.ChannelID)
  578. if sourceChannel != nil {
  579. if sourceChannel.Name != "" {
  580. sourceChannelName = sourceChannel.Name
  581. }
  582. switch sourceChannel.Type {
  583. case discordgo.ChannelTypeGuildText:
  584. if sourceChannel.GuildID != "" {
  585. sourceGuild, _ := bot.State.Guild(sourceChannel.GuildID)
  586. if sourceGuild != nil && sourceGuild.Name != "" {
  587. sourceName = "\"" + sourceGuild.Name + "\""
  588. }
  589. }
  590. case discordgo.ChannelTypeDM:
  591. sourceName = "Direct Messages"
  592. case discordgo.ChannelTypeGroupDM:
  593. sourceName = "Group Messages"
  594. }
  595. }
  596. subfolder := ""
  597. if message.Author != nil {
  598. // Subfolder Division - Server Nesting
  599. if *channelConfig.DivideFoldersByServer {
  600. subfolderSuffix := ""
  601. if sourceName != "" && sourceName != "UNKNOWN" {
  602. subfolderSuffix = sourceName
  603. for _, key := range pathBlacklist {
  604. subfolderSuffix = strings.ReplaceAll(subfolderSuffix, key, "")
  605. }
  606. }
  607. if subfolderSuffix != "" {
  608. subfolderSuffix = subfolderSuffix + string(os.PathSeparator)
  609. subfolder = subfolder + subfolderSuffix
  610. // Create folder.
  611. err := os.MkdirAll(path+subfolder, 0755)
  612. if err != nil {
  613. log.Println(logPrefixErrorHere, color.HiRedString("Error while creating server subfolder \"%s\": %s", path, err))
  614. return mDownloadStatus(downloadFailedCreatingSubfolder, err)
  615. }
  616. }
  617. }
  618. // Subfolder Division - Channel Nesting
  619. if *channelConfig.DivideFoldersByChannel {
  620. subfolderSuffix := ""
  621. if sourceChannelName != "" {
  622. subfolderSuffix = sourceChannelName
  623. for _, key := range pathBlacklist {
  624. subfolderSuffix = strings.ReplaceAll(subfolderSuffix, key, "")
  625. }
  626. }
  627. if subfolderSuffix != "" {
  628. subfolder = subfolder + subfolderSuffix + string(os.PathSeparator)
  629. // Create folder.
  630. err := os.MkdirAll(path+subfolder, 0755)
  631. if err != nil {
  632. log.Println(logPrefixErrorHere, color.HiRedString("Error while creating channel subfolder \"%s\": %s", path, err))
  633. return mDownloadStatus(downloadFailedCreatingSubfolder, err)
  634. }
  635. }
  636. }
  637. // Subfolder Division - User Nesting
  638. if *channelConfig.DivideFoldersByUser {
  639. subfolderSuffix := message.Author.ID
  640. if message.Author.Username != "" {
  641. subfolderSuffix = message.Author.Username + "#" + message.Author.Discriminator
  642. for _, key := range pathBlacklist {
  643. subfolderSuffix = strings.ReplaceAll(subfolderSuffix, key, "")
  644. }
  645. }
  646. if subfolderSuffix != "" {
  647. subfolder = subfolder + subfolderSuffix + string(os.PathSeparator)
  648. // Create folder.
  649. err := os.MkdirAll(path+subfolder, 0755)
  650. if err != nil {
  651. log.Println(logPrefixErrorHere, color.HiRedString("Error while creating user subfolder \"%s\": %s", path, err))
  652. return mDownloadStatus(downloadFailedCreatingSubfolder, err)
  653. }
  654. }
  655. }
  656. }
  657. // Subfolder Division - Content Type
  658. if *channelConfig.DivideFoldersByType && message.Author != nil {
  659. subfolderSuffix := ""
  660. switch contentTypeFound {
  661. case "image":
  662. subfolderSuffix = "images"
  663. case "video":
  664. subfolderSuffix = "videos"
  665. case "audio":
  666. subfolderSuffix = "audio"
  667. case "text":
  668. subfolderSuffix = "text"
  669. case "application":
  670. subfolderSuffix = "applications"
  671. }
  672. if subfolderSuffix != "" {
  673. subfolder = subfolder + subfolderSuffix + string(os.PathSeparator)
  674. // Create folder.
  675. err := os.MkdirAll(path+subfolder, 0755)
  676. if err != nil {
  677. log.Println(logPrefixErrorHere, color.HiRedString("Error while creating type subfolder \"%s\": %s", path+subfolder, err))
  678. return mDownloadStatus(downloadFailedCreatingSubfolder, err)
  679. }
  680. }
  681. }
  682. // Format filename/path
  683. filenameDateFormat := config.FilenameDateFormat
  684. if channelConfig.OverwriteFilenameDateFormat != nil {
  685. if *channelConfig.OverwriteFilenameDateFormat != "" {
  686. filenameDateFormat = *channelConfig.OverwriteFilenameDateFormat
  687. }
  688. }
  689. messageTime := time.Now()
  690. if message.Timestamp != "" {
  691. messageTimestamp, err := message.Timestamp.Parse()
  692. if err == nil {
  693. messageTime = messageTimestamp
  694. }
  695. }
  696. completePath := path + subfolder + messageTime.Format(filenameDateFormat) + filename
  697. // Check if exists
  698. if _, err := os.Stat(completePath); err == nil {
  699. if *channelConfig.SavePossibleDuplicates {
  700. tmpPath := completePath
  701. i := 1
  702. for {
  703. // Append number to name
  704. completePath = tmpPath[0:len(tmpPath)-len(filepathExtension(tmpPath))] +
  705. "-" + strconv.Itoa(i) + filepathExtension(tmpPath)
  706. if _, err := os.Stat(completePath); os.IsNotExist(err) {
  707. break
  708. }
  709. i = i + 1
  710. }
  711. if !historyCmd {
  712. log.Println(color.GreenString("Matching filenames, possible duplicate? Saving \"%s\" as \"%s\" instead", tmpPath, completePath))
  713. }
  714. } else {
  715. if !historyCmd {
  716. log.Println(logPrefixFileSkip, color.GreenString("Matching filenames, possible duplicate..."))
  717. }
  718. return mDownloadStatus(downloadSkippedDuplicate)
  719. }
  720. }
  721. // Write
  722. err = ioutil.WriteFile(completePath, bodyOfResp, 0644)
  723. if err != nil {
  724. log.Println(logPrefixErrorHere, color.HiRedString("Error while writing file to disk \"%s\": %s", inputURL, err))
  725. return mDownloadStatus(downloadFailedWritingFile, err)
  726. }
  727. // Change file time
  728. err = os.Chtimes(completePath, fileTime, fileTime)
  729. if err != nil {
  730. log.Println(logPrefixErrorHere, color.RedString("Error while changing metadata date \"%s\": %s", inputURL, err))
  731. }
  732. // Write duration
  733. if config.DebugOutput && !historyCmd {
  734. log.Println(logPrefixDebug, color.YellowString("#%d - %s to save.", thisDownloadID, durafmt.ParseShort(time.Since(downloadTime)).String()))
  735. }
  736. writeTime := time.Now()
  737. // Output
  738. log.Println(logPrefix + color.HiGreenString("SAVED %s sent in %s#%s to \"%s\"", strings.ToUpper(contentTypeFound), sourceName, sourceChannelName, completePath))
  739. userID := user.ID
  740. if message.Author != nil {
  741. userID = message.Author.ID
  742. }
  743. // Store in db
  744. err = dbInsertDownload(&download{
  745. URL: inputURL,
  746. Time: time.Now(),
  747. Destination: completePath,
  748. Filename: filename,
  749. ChannelID: message.ChannelID,
  750. UserID: userID,
  751. })
  752. if err != nil {
  753. log.Println(logPrefixErrorHere, color.HiRedString("Error writing to database: %s", err))
  754. return mDownloadStatus(downloadFailedWritingDatabase, err)
  755. }
  756. // Storage & output duration
  757. if config.DebugOutput && !historyCmd {
  758. log.Println(logPrefixDebug, color.YellowString("#%d - %s to update database.", thisDownloadID, durafmt.ParseShort(time.Since(writeTime)).String()))
  759. }
  760. finishTime := time.Now()
  761. // React
  762. if !historyCmd && *channelConfig.ReactWhenDownloaded && message.Author != nil {
  763. reaction := ""
  764. if *channelConfig.ReactWhenDownloadedEmoji == "" {
  765. if message.GuildID != "" {
  766. guild, err := bot.State.Guild(message.GuildID)
  767. if err != nil {
  768. log.Println(logPrefixErrorHere, color.RedString("Error fetching guild state for emojis from %s: %s", message.GuildID, err))
  769. } else {
  770. emojis := guild.Emojis
  771. if len(emojis) > 1 {
  772. for {
  773. rand.Seed(time.Now().UnixNano())
  774. chosenEmoji := emojis[rand.Intn(len(emojis))]
  775. formattedEmoji := chosenEmoji.APIName()
  776. if !chosenEmoji.Animated && !stringInSlice(formattedEmoji, *channelConfig.BlacklistReactEmojis) {
  777. reaction = formattedEmoji
  778. break
  779. }
  780. }
  781. } else {
  782. reaction = defaultReact
  783. }
  784. }
  785. } else {
  786. reaction = defaultReact
  787. }
  788. } else {
  789. reaction = *channelConfig.ReactWhenDownloadedEmoji
  790. }
  791. // Add Reaction
  792. if hasPerms(message.ChannelID, discordgo.PermissionAddReactions) {
  793. err = bot.MessageReactionAdd(message.ChannelID, message.ID, reaction)
  794. if err != nil {
  795. log.Println(logPrefixErrorHere, color.RedString("Error adding reaction to message: %s", err))
  796. }
  797. } else {
  798. log.Println(logPrefixErrorHere, color.RedString("Bot does not have permission to add reactions in %s", message.ChannelID))
  799. }
  800. // React duration
  801. if config.DebugOutput {
  802. log.Println(logPrefixDebug, color.YellowString("#%d - %s to react with \"%s\".", thisDownloadID, durafmt.ParseShort(time.Since(finishTime)).String(), reaction))
  803. }
  804. }
  805. if !historyCmd {
  806. timeLastUpdated = time.Now()
  807. if *channelConfig.UpdatePresence {
  808. updateDiscordPresence()
  809. }
  810. }
  811. if config.DebugOutput && !historyCmd {
  812. log.Println(logPrefixDebug, color.YellowString("#%d - %s total.", thisDownloadID, time.Since(startTime)))
  813. }
  814. // Save All Links to File
  815. if channelConfig.SaveAllLinksToFile != nil {
  816. filepath := *channelConfig.SaveAllLinksToFile
  817. if filepath != "" {
  818. f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
  819. if err != nil {
  820. log.Println(color.RedString("[SaveAllLinksToFile] Failed to open file:\t%s", err))
  821. f.Close()
  822. }
  823. defer f.Close()
  824. var addedContent string
  825. rawLinks := getRawLinks(message)
  826. for _, rawLink := range rawLinks {
  827. addedContent = addedContent + "\n" + rawLink.Link
  828. }
  829. if _, err = f.WriteString(addedContent); err != nil {
  830. log.Println(color.RedString("[SaveAllLinksToFile] Failed to append file:\t%s", err))
  831. }
  832. }
  833. }
  834. if thisDownloadID > 0 {
  835. // Filter Duplicate Images
  836. if config.FilterDuplicateImages {
  837. encodedStore, err := imgStore.GobEncode()
  838. if err != nil {
  839. log.Println(color.HiRedString("Failed to encode imgStore:\t%s"))
  840. } else {
  841. f, err := os.OpenFile(imgStorePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
  842. if err != nil {
  843. log.Println(color.HiRedString("Failed to open imgStore file:\t%s"))
  844. }
  845. _, err = f.Write(encodedStore)
  846. if err != nil {
  847. log.Println(color.HiRedString("Failed to update imgStore file:\t%s"))
  848. }
  849. err = f.Close()
  850. if err != nil {
  851. log.Println(color.HiRedString("Failed to close imgStore file:\t%s"))
  852. }
  853. }
  854. }
  855. }
  856. return mDownloadStatus(downloadSuccess)
  857. }
  858. return mDownloadStatus(downloadIgnored)
  859. }