main.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log"
  6. "net/url"
  7. "os"
  8. "os/signal"
  9. "runtime"
  10. "strings"
  11. "sync"
  12. "syscall"
  13. "time"
  14. "github.com/ChimeraCoder/anaconda"
  15. "github.com/Davincible/goinsta/v3"
  16. "github.com/HouzuoGuo/tiedot/db"
  17. "github.com/Necroforger/dgrouter/exrouter"
  18. "github.com/bwmarrin/discordgo"
  19. "github.com/fatih/color"
  20. "github.com/fsnotify/fsnotify"
  21. "github.com/hako/durafmt"
  22. orderedmap "github.com/wk8/go-ordered-map/v2"
  23. )
  24. /* v2.0.0 REWRITE TODO:
  25. * Finish Readme
  26. * Goification
  27. *** better universal implementation of goroutines and waitgroup management.
  28. *** better universal implementation of function wrappers to reduce clutter for things like perm checks, response messages
  29. *** better error handling and error response, ensure as many nil checks as reasonably possible
  30. * Better Settings Insight / Corrective Suggestions
  31. * Fix unknown message hangups
  32. * Fix links being ignored
  33. * Support Google Drive (folder; direct singles)
  34. * Support Dropbox (folder; direct singles)
  35. * Support Mediafire? (folder; direct singles)
  36. * Support Lightshot?
  37. * Support Gyazo?
  38. ** Support TikTok -- Tried, once the connection is closed the cdn URL is rendered invalid
  39. ** Support 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.
  40. ** Support Facebook Videos -- Previously supported but they split mp4 into separate audio and video streams
  41. ** Support Mastodon -- dropped due to login/api changes??
  42. ---------------------------------
  43. FUTURE PLANS:
  44. * UI/GUI interface (i still want to be as cross-platform as possible), and have bot commands be an option.
  45. * Implement Log Leveling?
  46. * Command: Reboot System
  47. */
  48. var (
  49. err error
  50. mainWg sync.WaitGroup
  51. // Bot
  52. bot *discordgo.Session
  53. botUser *discordgo.User
  54. botCommands *exrouter.Route
  55. selfbot bool = false
  56. botReady bool = false
  57. // Storage
  58. myDB *db.DB
  59. // APIs
  60. twitterConnected bool = false
  61. twitterClient *anaconda.TwitterApi
  62. instagramConnected bool = false
  63. instagramClient *goinsta.Instagram
  64. // Gen
  65. loop chan os.Signal
  66. startTime time.Time
  67. timeLastUpdated time.Time
  68. timeLastDownload time.Time
  69. timeLastMessage time.Time
  70. cachedDownloadID int
  71. configReloadLastTime time.Time
  72. // Validation
  73. invalidAdminChannels []string
  74. invalidChannels []string
  75. invalidCategories []string
  76. invalidServers []string
  77. )
  78. const (
  79. imgurClientID = "08af502a9e70d65"
  80. sneakyUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"
  81. )
  82. func versions(multiline bool) string {
  83. if multiline {
  84. return fmt.Sprintf("%s/%s / %s\ndiscordgo v%s (API v%s)",
  85. runtime.GOOS, runtime.GOARCH, runtime.Version(), discordgo.VERSION, discordgo.APIVersion)
  86. } else {
  87. return fmt.Sprintf("%s/%s / %s / discordgo v%s (API v%s)",
  88. runtime.GOOS, runtime.GOARCH, runtime.Version(), discordgo.VERSION, discordgo.APIVersion)
  89. }
  90. }
  91. func init() {
  92. log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
  93. log.SetOutput(color.Output)
  94. log.Println(color.HiCyanString(wrapHyphensW(fmt.Sprintf("Welcome to %s v%s", projectName, projectVersion))))
  95. loop = make(chan os.Signal, 1)
  96. startTime = time.Now()
  97. historyJobs = orderedmap.New[string, historyJob]()
  98. if len(os.Args) > 1 {
  99. configFileBase = os.Args[1]
  100. }
  101. log.Println(lg("Version", "", color.CyanString, versions(false)))
  102. // Github Update Check
  103. if config.GithubUpdateChecking {
  104. if !isLatestGithubRelease() {
  105. log.Println(lg("Version", "UPDATE", color.HiCyanString, "***\tUPDATE AVAILABLE\t***"))
  106. log.Println(lg("Version", "UPDATE", color.CyanString, projectReleaseURL))
  107. log.Println(lg("Version", "UPDATE", color.HiCyanString, "*** See changelog for information ***"))
  108. log.Println(lg("Version", "UPDATE", color.HiCyanString, "CHECK ALL CHANGELOGS SINCE YOUR LAST UPDATE"))
  109. log.Println(lg("Version", "UPDATE", color.HiCyanString, "SOME SETTINGS MAY NEED TO BE UPDATED"))
  110. time.Sleep(5 * time.Second)
  111. }
  112. }
  113. log.Println(lg("Info", "", color.HiCyanString, "** Need help? Discord: https://discord.gg/6Z6FJZVaDV **"))
  114. }
  115. func main() {
  116. mainWg.Add(2)
  117. go loadConfig()
  118. go openDatabase()
  119. // Regex
  120. mainWg.Add(1)
  121. go func() {
  122. if err = compileRegex(); err != nil {
  123. log.Println(lg("Regex", "", color.HiRedString, "Error initializing:\t%s", err))
  124. return
  125. }
  126. mainWg.Done()
  127. }()
  128. mainWg.Wait() // wait because api uses credentials from json
  129. mainWg.Add(2)
  130. go botLoadAPIs()
  131. go botLoadDiscord()
  132. mainWg.Wait()
  133. //#region MAIN STARTUP COMPLETE
  134. if config.Debug {
  135. log.Println(lg("Main", "", color.YellowString, "Startup finished, took %s...", uptime()))
  136. }
  137. log.Println(lg("Main", "", color.HiCyanString,
  138. wrapHyphensW(fmt.Sprintf("%s v%s is online with access to %d server%s",
  139. projectLabel, projectVersion, len(bot.State.Guilds), pluralS(len(bot.State.Guilds))))))
  140. log.Println(lg("Main", "", color.RedString, "CTRL+C to exit..."))
  141. // Log Status
  142. go sendStatusMessage(sendStatusStartup)
  143. //#endregion
  144. //#region BG Tasks
  145. //#region BG Tasks - Tickers
  146. tickerCheckup := time.NewTicker(time.Duration(config.CheckupRate) * time.Minute)
  147. tickerPresence := time.NewTicker(time.Duration(config.PresenceRefreshRate) * time.Minute)
  148. tickerConnection := time.NewTicker(time.Duration(config.ConnectionCheckRate) * time.Minute)
  149. go func() {
  150. for {
  151. select {
  152. case <-tickerCheckup.C:
  153. if config.Debug {
  154. //MARKER: history jobs polled for waiting count in checkup
  155. historyJobsWaiting := 0
  156. if historyJobs.Len() > 0 {
  157. for jobPair := historyJobs.Oldest(); jobPair != nil; jobPair.Next() {
  158. job := jobPair.Value
  159. if job.Status == historyStatusWaiting {
  160. historyJobsWaiting++
  161. }
  162. }
  163. }
  164. str := fmt.Sprintf("... %dms latency,\t\tlast discord heartbeat %s ago,\t\t%s uptime",
  165. bot.HeartbeatLatency().Milliseconds(),
  166. shortenTime(durafmt.ParseShort(time.Since(bot.LastHeartbeatSent)).String()),
  167. shortenTime(durafmt.ParseShort(time.Since(startTime)).String()))
  168. if !timeLastMessage.IsZero() {
  169. str += fmt.Sprintf(",\tlast message %s ago",
  170. shortenTime(durafmt.ParseShort(time.Since(timeLastMessage)).String()))
  171. }
  172. if !timeLastDownload.IsZero() {
  173. str += fmt.Sprintf(",\tlast download %s ago",
  174. shortenTime(durafmt.ParseShort(time.Since(timeLastDownload)).String()))
  175. }
  176. if historyJobsWaiting > 0 {
  177. str += fmt.Sprintf(",\t%d history jobs waiting", historyJobsWaiting)
  178. }
  179. log.Println(lg("Checkup", "", color.YellowString, str))
  180. }
  181. case <-tickerPresence.C:
  182. // If bot experiences connection interruption the status will go blank until updated by message, this fixes that
  183. go updateDiscordPresence()
  184. case <-tickerConnection.C:
  185. doReconnect := func() {
  186. log.Println(lg("Discord", "", color.YellowString, "Closing Discord connections..."))
  187. bot.Client.CloseIdleConnections()
  188. bot.CloseWithCode(1001)
  189. bot = nil
  190. log.Println(lg("Discord", "", color.RedString, "Discord connections closed!"))
  191. if config.ExitOnBadConnection {
  192. properExit()
  193. } else {
  194. log.Println(lg("Discord", "", color.GreenString, "Logging in..."))
  195. botLoad()
  196. log.Println(lg("Discord", "", color.HiGreenString,
  197. "Reconnected! The bot *should* resume working..."))
  198. // Log Status
  199. sendStatusMessage(sendStatusReconnect)
  200. }
  201. }
  202. gate, err := bot.Gateway()
  203. if err != nil || gate == "" {
  204. log.Println(lg("Discord", "", color.HiYellowString,
  205. "Bot encountered a gateway error: GATEWAY: %s,\tERR: %s", gate, err))
  206. go doReconnect()
  207. } else if time.Since(bot.LastHeartbeatAck).Seconds() > 4*60 {
  208. log.Println(lg("Discord", "", color.HiYellowString,
  209. "Bot has not received a heartbeat from Discord in 4 minutes..."))
  210. go doReconnect()
  211. }
  212. }
  213. }
  214. }()
  215. //#endregion
  216. //#region BG Tasks - Autorun History
  217. type arh struct{ channel, before, since string }
  218. var autoHistoryChannels []arh
  219. // Compile list of channels to autorun history
  220. for _, channel := range getAllRegisteredChannels() {
  221. channelConfig := getSource(&discordgo.Message{ChannelID: channel})
  222. if channelConfig.AutoHistory != nil {
  223. if *channelConfig.AutoHistory {
  224. var autoHistoryChannel arh
  225. autoHistoryChannel.channel = channel
  226. autoHistoryChannel.before = *channelConfig.AutoHistoryBefore
  227. autoHistoryChannel.since = *channelConfig.AutoHistorySince
  228. autoHistoryChannels = append(autoHistoryChannels, autoHistoryChannel)
  229. }
  230. continue
  231. }
  232. }
  233. // Process auto history
  234. for _, ah := range autoHistoryChannels {
  235. //MARKER: history jobs queued from auto
  236. if job, exists := historyJobs.Get(ah.channel); !exists ||
  237. (job.Status != historyStatusRunning && job.Status != historyStatusAbortRequested) {
  238. job.Status = historyStatusWaiting
  239. job.OriginChannel = "AUTORUN"
  240. job.OriginUser = "AUTORUN"
  241. job.TargetCommandingMessage = nil
  242. job.TargetChannelID = ah.channel
  243. job.TargetBefore = ah.before
  244. job.TargetSince = ah.since
  245. job.Updated = time.Now()
  246. job.Added = time.Now()
  247. historyJobs.Set(ah.channel, job)
  248. //TODO: signals for this and typical history cmd??
  249. }
  250. }
  251. if len(autoHistoryChannels) > 0 {
  252. log.Println(lg("History", "Autorun", color.HiYellowString,
  253. "History Autoruns completed (for %d channel%s)",
  254. len(autoHistoryChannels), pluralS(len(autoHistoryChannels))))
  255. log.Println(lg("History", "Autorun", color.CyanString,
  256. "Waiting for something else to do..."))
  257. }
  258. //#endregion
  259. //#region BG Tasks - History Job Processing
  260. go func() {
  261. for {
  262. // Empty Local Cache
  263. nhistoryJobCnt,
  264. nhistoryJobCntWaiting,
  265. nhistoryJobCntRunning,
  266. nhistoryJobCntAborted,
  267. nhistoryJobCntErrored,
  268. nhistoryJobCntCompleted := historyJobs.Len(), 0, 0, 0, 0, 0
  269. //MARKER: history jobs launch
  270. // do we even bother?
  271. if nhistoryJobCnt > 0 {
  272. // New Cache
  273. for pair := historyJobs.Oldest(); pair != nil; pair = pair.Next() {
  274. job := pair.Value
  275. if job.Status == historyStatusWaiting {
  276. nhistoryJobCntWaiting++
  277. } else if job.Status == historyStatusRunning {
  278. nhistoryJobCntRunning++
  279. } else if job.Status == historyStatusAbortRequested || job.Status == historyStatusAbortCompleted {
  280. nhistoryJobCntAborted++
  281. } else if job.Status == historyStatusErrorReadMessageHistoryPerms || job.Status == historyStatusErrorRequesting {
  282. nhistoryJobCntErrored++
  283. } else if job.Status >= historyStatusCompletedNoMoreMessages {
  284. nhistoryJobCntCompleted++
  285. }
  286. }
  287. // Should Start New Job(s)?
  288. if nhistoryJobCntRunning < config.HistoryMaxJobs {
  289. openSlots := config.HistoryMaxJobs - nhistoryJobCntRunning
  290. newJobs := make([]historyJob, openSlots)
  291. filledSlots := 0
  292. // Find Jobs
  293. for pair := historyJobs.Oldest(); pair != nil; pair = pair.Next() {
  294. if filledSlots == openSlots {
  295. break
  296. }
  297. if pair.Value.Status == historyStatusWaiting {
  298. newJobs = append(newJobs, pair.Value)
  299. filledSlots++
  300. }
  301. }
  302. // Start Jobs
  303. if len(newJobs) > 0 {
  304. for _, job := range newJobs {
  305. if job != (historyJob{}) {
  306. go handleHistory(job.TargetCommandingMessage, job.TargetChannelID, job.TargetBefore, job.TargetSince)
  307. }
  308. }
  309. }
  310. }
  311. }
  312. // Update Cache
  313. historyJobCnt = nhistoryJobCnt
  314. historyJobCntWaiting = nhistoryJobCntWaiting
  315. historyJobCntRunning = nhistoryJobCntRunning
  316. historyJobCntAborted = nhistoryJobCntAborted
  317. historyJobCntErrored = nhistoryJobCntErrored
  318. historyJobCntCompleted = nhistoryJobCntCompleted
  319. //~ optional setting?
  320. time.Sleep(5 * time.Second)
  321. }
  322. }()
  323. //#endregion
  324. //#region BG Tasks - Settings Watcher
  325. watcher, err := fsnotify.NewWatcher()
  326. if err != nil {
  327. log.Println(lg("Settings", "Watcher", color.HiRedString, "Error creating NewWatcher:\t%s", err))
  328. }
  329. defer watcher.Close()
  330. if err = watcher.Add(configFile); err != nil {
  331. log.Println(lg("Settings", "Watcher", color.HiRedString, "Error adding watcher for settings:\t%s", err))
  332. }
  333. go func() {
  334. for {
  335. select {
  336. case event, ok := <-watcher.Events:
  337. if !ok {
  338. return
  339. }
  340. if event.Op&fsnotify.Write == fsnotify.Write {
  341. // It double-fires the event without time check, might depend on OS but this works anyways
  342. if time.Since(configReloadLastTime).Milliseconds() > 1 {
  343. time.Sleep(1 * time.Second)
  344. log.Println(lg("Settings", "Watcher", color.YellowString,
  345. "Detected changes in \"%s\", reloading...", configFile))
  346. mainWg.Add(1)
  347. go loadConfig()
  348. allString := ""
  349. if config.All != nil {
  350. allString = ", ALL ENABLED"
  351. }
  352. log.Println(lg("Settings", "Watcher", color.HiYellowString,
  353. "Reloaded - bound to %d channel%s, %d categories, %d server%s, %d user%s%s",
  354. getBoundChannelsCount(), pluralS(getBoundChannelsCount()),
  355. getBoundCategoriesCount(),
  356. getBoundServersCount(), pluralS(getBoundServersCount()),
  357. getBoundUsersCount(), pluralS(getBoundUsersCount()), allString,
  358. ))
  359. go updateDiscordPresence()
  360. }
  361. configReloadLastTime = time.Now()
  362. }
  363. case err, ok := <-watcher.Errors:
  364. if !ok {
  365. return
  366. }
  367. log.Println(color.HiRedString("[Watchers] Error:\t%s", err))
  368. }
  369. }
  370. }()
  371. //#endregion
  372. //#endregion
  373. //#region Cache Constants
  374. constants := make(map[string]string)
  375. //--- Compile constants
  376. for _, server := range bot.State.Guilds {
  377. serverKey := fmt.Sprintf("SERVER_%s", stripSymbols(server.Name))
  378. serverKey = strings.ReplaceAll(serverKey, " ", "_")
  379. for strings.Contains(serverKey, "__") {
  380. serverKey = strings.ReplaceAll(serverKey, "__", "_")
  381. }
  382. serverKey = strings.ToUpper(serverKey)
  383. if constants[serverKey] == "" {
  384. constants[serverKey] = server.ID
  385. }
  386. for _, channel := range server.Channels {
  387. if channel.Type != discordgo.ChannelTypeGuildCategory {
  388. categoryName := ""
  389. if channel.ParentID != "" {
  390. channelParent, err := bot.State.Channel(channel.ParentID)
  391. if err == nil {
  392. categoryName = channelParent.Name
  393. }
  394. }
  395. channelKey := fmt.Sprintf("CHANNEL_%s_%s_%s",
  396. stripSymbols(server.Name), stripSymbols(categoryName), stripSymbols(channel.Name))
  397. channelKey = strings.ReplaceAll(channelKey, " ", "_")
  398. for strings.Contains(channelKey, "__") {
  399. channelKey = strings.ReplaceAll(channelKey, "__", "_")
  400. }
  401. channelKey = strings.ToUpper(channelKey)
  402. if constants[channelKey] == "" {
  403. constants[channelKey] = channel.ID
  404. }
  405. }
  406. }
  407. }
  408. //--- Save constants
  409. os.MkdirAll(cachePath, 0755)
  410. if _, err := os.Stat(constantsPath); err == nil {
  411. err = os.Remove(constantsPath)
  412. if err != nil {
  413. log.Println(lg("Constants", "", color.HiRedString, "Encountered error deleting cache file:\t%s", err))
  414. }
  415. }
  416. constantsStruct := constStruct{}
  417. constantsStruct.Constants = constants
  418. newJson, err := json.MarshalIndent(constantsStruct, "", "\t")
  419. if err != nil {
  420. log.Println(lg("Constants", "", color.HiRedString, "Failed to format constants...\t%s", err))
  421. } else {
  422. err := os.WriteFile(constantsPath, newJson, 0644)
  423. if err != nil {
  424. log.Println(lg("Constants", "", color.HiRedString, "Failed to save new constants file...\t%s", err))
  425. }
  426. }
  427. //#endregion
  428. // ~~~ RUNNING
  429. //#region ----------- TEST ENV / main
  430. //#endregion ------------------------
  431. //#region Exit...
  432. signal.Notify(loop, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, os.Interrupt, os.Kill)
  433. <-loop
  434. sendStatusMessage(sendStatusExit) // not goroutine because we want to wait to send this before logout
  435. log.Println(lg("Discord", "", color.GreenString, "Logging out of discord..."))
  436. bot.Close()
  437. log.Println(lg("Database", "", color.YellowString, "Closing database..."))
  438. myDB.Close()
  439. log.Println(lg("Main", "", color.HiRedString, "Exiting... "))
  440. //#endregion
  441. }
  442. func openDatabase() {
  443. // Database
  444. log.Println(lg("Database", "", color.YellowString, "Opening database...\t(this can take a second...)"))
  445. myDB, err = db.OpenDB(databasePath)
  446. if err != nil {
  447. log.Println(lg("Database", "", color.HiRedString, "Unable to open database: %s", err))
  448. return
  449. }
  450. if myDB.Use("Downloads") == nil {
  451. log.Println(lg("Database", "Setup", color.YellowString, "Creating database, please wait..."))
  452. if err := myDB.Create("Downloads"); err != nil {
  453. log.Println(lg("Database", "Setup", color.HiRedString, "Error while trying to create database: %s", err))
  454. return
  455. }
  456. log.Println(lg("Database", "Setup", color.HiYellowString, "Created new database..."))
  457. log.Println(lg("Database", "Setup", color.YellowString, "Indexing database, please wait..."))
  458. if err := myDB.Use("Downloads").Index([]string{"URL"}); err != nil {
  459. log.Println(lg("Database", "Setup", color.HiRedString, "Unable to create database index for URL: %s", err))
  460. return
  461. }
  462. if err := myDB.Use("Downloads").Index([]string{"ChannelID"}); err != nil {
  463. log.Println(lg("Database", "Setup", color.HiRedString, "Unable to create database index for ChannelID: %s", err))
  464. return
  465. }
  466. if err := myDB.Use("Downloads").Index([]string{"UserID"}); err != nil {
  467. log.Println(lg("Database", "Setup", color.HiRedString, "Unable to create database index for UserID: %s", err))
  468. return
  469. }
  470. log.Println(lg("Database", "Setup", color.HiYellowString, "Created new indexes..."))
  471. }
  472. // Cache download tally
  473. cachedDownloadID = dbDownloadCount()
  474. log.Println(lg("Database", "", color.HiYellowString, "Database opened, contains %d entries...", cachedDownloadID))
  475. mainWg.Done()
  476. }
  477. func botLoad() {
  478. mainWg.Add(1)
  479. botLoadAPIs()
  480. mainWg.Add(1)
  481. botLoadDiscord()
  482. }
  483. func botLoadAPIs() {
  484. // Twitter API
  485. if config.Credentials.TwitterAccessToken != "" &&
  486. config.Credentials.TwitterAccessTokenSecret != "" &&
  487. config.Credentials.TwitterConsumerKey != "" &&
  488. config.Credentials.TwitterConsumerSecret != "" {
  489. log.Println(lg("API", "Twitter", color.MagentaString, "Connecting to API..."))
  490. twitterLoginCount := 0
  491. do_twitter_login:
  492. twitterLoginCount++
  493. if twitterLoginCount > 1 {
  494. time.Sleep(3 * time.Second)
  495. }
  496. twitterClient = anaconda.NewTwitterApiWithCredentials(
  497. config.Credentials.TwitterAccessToken,
  498. config.Credentials.TwitterAccessTokenSecret,
  499. config.Credentials.TwitterConsumerKey,
  500. config.Credentials.TwitterConsumerSecret,
  501. )
  502. twitterSelf, err := twitterClient.GetSelf(url.Values{})
  503. if err != nil {
  504. log.Println(lg("API", "Twitter", color.HiRedString, "API Login Error: %s", err.Error()))
  505. if twitterLoginCount <= 3 {
  506. goto do_twitter_login
  507. } else {
  508. log.Println(lg("API", "Twitter", color.HiRedString,
  509. "Failed to login to Twitter API, the bot will not fetch tweet media..."))
  510. }
  511. } else {
  512. log.Println(lg("API", "Twitter", color.HiMagentaString,
  513. "Connected to API @%s", twitterSelf.ScreenName))
  514. twitterConnected = true
  515. }
  516. } else {
  517. log.Println(lg("API", "Twitter", color.MagentaString,
  518. "API credentials missing, the bot won't use the Twitter API."))
  519. }
  520. // Instagram API
  521. if config.Credentials.InstagramUsername != "" &&
  522. config.Credentials.InstagramPassword != "" {
  523. log.Println(lg("API", "Instagram", color.MagentaString, "Connecting to API..."))
  524. instagramLoginCount := 0
  525. do_instagram_login:
  526. instagramLoginCount++
  527. if instagramLoginCount > 1 {
  528. time.Sleep(3 * time.Second)
  529. }
  530. if instagramClient, err = goinsta.Import(instagramCachePath); err != nil {
  531. instagramClient = goinsta.New(config.Credentials.InstagramUsername, config.Credentials.InstagramPassword)
  532. if err := instagramClient.Login(); err != nil {
  533. log.Println(lg("API", "Instagram", color.HiRedString, "API Login Error: %s", err.Error()))
  534. if instagramLoginCount <= 3 {
  535. goto do_instagram_login
  536. } else {
  537. log.Println(lg("API", "Instagram", color.HiRedString,
  538. "Failed to login to Instagram API, the bot will not fetch tweet media..."))
  539. }
  540. } else {
  541. log.Println(lg("API", "Instagram", color.HiMagentaString,
  542. "Connected to API @%s via new login", instagramClient.Account.Username))
  543. instagramConnected = true
  544. }
  545. } else {
  546. log.Println(lg("API", "Instagram", color.HiMagentaString,
  547. "Connected to API @%s via cache", instagramClient.Account.Username))
  548. instagramConnected = true
  549. }
  550. defer instagramClient.Export(instagramCachePath)
  551. } else {
  552. log.Println(lg("API", "Instagram", color.MagentaString,
  553. "API credentials missing, the bot won't use the Instagram API."))
  554. }
  555. mainWg.Done()
  556. }
  557. func botLoadDiscord() {
  558. var err error
  559. // Discord Login
  560. connectBot := func() {
  561. // Connect Bot
  562. bot.LogLevel = -1 // to ignore dumb wsapi error
  563. err = bot.Open()
  564. if err != nil && !strings.Contains(strings.ToLower(err.Error()), "web socket already opened") {
  565. log.Println(lg("Discord", "", color.HiRedString, "Discord login failed:\t%s", err))
  566. properExit()
  567. }
  568. bot.LogLevel = config.DiscordLogLevel // reset
  569. bot.ShouldReconnectOnError = true
  570. dur, err := time.ParseDuration(fmt.Sprint(config.DiscordTimeout) + "s")
  571. if err != nil {
  572. dur, _ = time.ParseDuration("180s")
  573. }
  574. bot.Client.Timeout = dur
  575. bot.State.MaxMessageCount = 100000
  576. bot.State.TrackChannels = true
  577. bot.State.TrackThreads = true
  578. bot.State.TrackMembers = true
  579. bot.State.TrackThreadMembers = true
  580. botUser, err = bot.User("@me")
  581. if err != nil {
  582. botUser = bot.State.User
  583. }
  584. }
  585. discord_login_count := 0
  586. do_discord_login:
  587. discord_login_count++
  588. if discord_login_count > 1 {
  589. time.Sleep(3 * time.Second)
  590. }
  591. if config.Credentials.Token != "" && config.Credentials.Token != placeholderToken {
  592. // Login via Token (Bot or User)
  593. log.Println(lg("Discord", "", color.GreenString, "Connecting to Discord via Token..."))
  594. // attempt login without Bot prefix
  595. bot, err = discordgo.New(config.Credentials.Token)
  596. connectBot()
  597. if botUser.Bot { // is bot application, reconnect properly
  598. //log.Println(lg("Discord", "", color.GreenString, "Reconnecting as bot..."))
  599. bot, err = discordgo.New("Bot " + config.Credentials.Token)
  600. connectBot()
  601. }
  602. } else if (config.Credentials.Email != "" && config.Credentials.Email != placeholderEmail) &&
  603. (config.Credentials.Password != "" && config.Credentials.Password != placeholderPassword) {
  604. // Login via Email+Password (User Only obviously)
  605. log.Println(lg("Discord", "", color.GreenString, "Connecting via Login..."))
  606. bot, err = discordgo.New(config.Credentials.Email, config.Credentials.Password)
  607. } else {
  608. if discord_login_count > 5 {
  609. log.Println(lg("Discord", "", color.HiRedString, "No valid credentials for Discord..."))
  610. properExit()
  611. } else {
  612. goto do_discord_login
  613. }
  614. }
  615. if err != nil {
  616. if discord_login_count > 5 {
  617. log.Println(lg("Discord", "", color.HiRedString, "Error logging in: %s", err))
  618. properExit()
  619. } else {
  620. goto do_discord_login
  621. }
  622. }
  623. connectBot()
  624. // Fetch Bot's User Info
  625. botUser, err = bot.User("@me")
  626. if err != nil {
  627. botUser = bot.State.User
  628. if botUser == nil {
  629. if discord_login_count > 5 {
  630. log.Println(lg("Discord", "", color.HiRedString, "Error obtaining user details: %s", err))
  631. properExit()
  632. } else {
  633. goto do_discord_login
  634. }
  635. }
  636. } else if botUser == nil {
  637. if discord_login_count > 5 {
  638. log.Println(lg("Discord", "", color.HiRedString, "No error encountered obtaining user details, but it's empty..."))
  639. properExit()
  640. } else {
  641. goto do_discord_login
  642. }
  643. } else {
  644. botReady = true
  645. log.Println(lg("Discord", "", color.HiGreenString, "Logged into %s", getUserIdentifier(*botUser)))
  646. if botUser.Bot {
  647. log.Println(lg("Discord", "", color.MagentaString, "This is a genuine Discord Bot Application"))
  648. log.Println(lg("Discord", "", color.MagentaString, "- Presence details & state are disabled, only status will work."))
  649. log.Println(lg("Discord", "", color.MagentaString, "- The bot can only see servers you have added it to."))
  650. log.Println(lg("Discord", "", color.MagentaString, "- Nothing is wrong, this is just info :)"))
  651. } else {
  652. log.Println(lg("Discord", "", color.MagentaString, "This is a User Account (Self-Bot)"))
  653. log.Println(lg("Discord", "", color.MagentaString, "- Discord does not allow Automated User Accounts (Self-Bots), so by using this bot you potentially risk account termination."))
  654. log.Println(lg("Discord", "", color.MagentaString, "- See GitHub page for link to Discord's official statement."))
  655. log.Println(lg("Discord", "", color.MagentaString, "- If you wish to avoid this, use a Bot Application if possible."))
  656. log.Println(lg("Discord", "", color.MagentaString, "- Nothing is wrong, this is just info :)"))
  657. }
  658. }
  659. if bot.State.User != nil { // is selfbot
  660. selfbot = bot.State.User.Email != ""
  661. }
  662. // Event Handlers
  663. botCommands = handleCommands()
  664. bot.AddHandler(messageCreate)
  665. bot.AddHandler(messageUpdate)
  666. // Source Validation
  667. if config.Debug {
  668. log.Println(lg("Discord", "Validation", color.HiYellowString, "Validating configured sources..."))
  669. }
  670. //-
  671. if config.AdminChannels != nil {
  672. for _, adminChannel := range config.AdminChannels {
  673. if adminChannel.ChannelIDs != nil {
  674. for _, subchannel := range *adminChannel.ChannelIDs {
  675. _, err := bot.State.Channel(subchannel)
  676. if err != nil {
  677. invalidAdminChannels = append(invalidAdminChannels, subchannel)
  678. log.Println(lg("Discord", "Validation", color.HiRedString,
  679. "Bot cannot access admin subchannel %s...\t%s", subchannel, err))
  680. }
  681. }
  682. } else {
  683. _, err := bot.State.Channel(adminChannel.ChannelID)
  684. if err != nil {
  685. invalidAdminChannels = append(invalidAdminChannels, adminChannel.ChannelID)
  686. log.Println(lg("Discord", "Validation", color.HiRedString,
  687. "Bot cannot access admin channel %s...\t%s", adminChannel.ChannelID, err))
  688. }
  689. }
  690. }
  691. }
  692. //-
  693. for _, server := range config.Servers {
  694. if server.ServerIDs != nil {
  695. for _, subserver := range *server.ServerIDs {
  696. _, err := bot.State.Guild(subserver)
  697. if err != nil {
  698. invalidServers = append(invalidServers, subserver)
  699. log.Println(lg("Discord", "Validation", color.HiRedString,
  700. "Bot cannot access subserver %s...\t%s", subserver, err))
  701. }
  702. }
  703. } else {
  704. _, err := bot.State.Guild(server.ServerID)
  705. if err != nil {
  706. invalidServers = append(invalidServers, server.ServerID)
  707. log.Println(lg("Discord", "Validation", color.HiRedString,
  708. "Bot cannot access server %s...\t%s", server.ServerID, err))
  709. }
  710. }
  711. }
  712. for _, category := range config.Categories {
  713. if category.CategoryIDs != nil {
  714. for _, subcategory := range *category.CategoryIDs {
  715. _, err := bot.State.Channel(subcategory)
  716. if err != nil {
  717. invalidCategories = append(invalidCategories, subcategory)
  718. log.Println(lg("Discord", "Validation", color.HiRedString,
  719. "Bot cannot access subcategory %s...\t%s", subcategory, err))
  720. }
  721. }
  722. } else {
  723. _, err := bot.State.Channel(category.CategoryID)
  724. if err != nil {
  725. invalidCategories = append(invalidCategories, category.CategoryID)
  726. log.Println(lg("Discord", "Validation", color.HiRedString,
  727. "Bot cannot access category %s...\t%s", category.CategoryID, err))
  728. }
  729. }
  730. }
  731. for _, channel := range config.Channels {
  732. if channel.ChannelIDs != nil {
  733. for _, subchannel := range *channel.ChannelIDs {
  734. _, err := bot.State.Channel(subchannel)
  735. if err != nil {
  736. invalidChannels = append(invalidChannels, subchannel)
  737. log.Println(lg("Discord", "Validation", color.HiRedString,
  738. "Bot cannot access subchannel %s...\t%s", subchannel, err))
  739. }
  740. }
  741. } else {
  742. _, err := bot.State.Channel(channel.ChannelID)
  743. if err != nil {
  744. invalidChannels = append(invalidChannels, channel.ChannelID)
  745. log.Println(lg("Discord", "Validation", color.HiRedString,
  746. "Bot cannot access channel %s...\t%s", channel.ChannelID, err))
  747. }
  748. }
  749. }
  750. //-
  751. invalidSources := len(invalidAdminChannels) + len(invalidChannels) + len(invalidCategories) + len(invalidServers)
  752. if invalidSources > 0 {
  753. log.Println(lg("Discord", "Validation", color.HiRedString,
  754. "Found %d invalid channels/servers in configuration...", invalidSources))
  755. logMsg := fmt.Sprintf("Validation found %d invalid sources...\n", invalidSources)
  756. if len(invalidAdminChannels) > 0 {
  757. logMsg += fmt.Sprintf("\n**- Admin Channels: (%d)** - %s",
  758. len(invalidAdminChannels), strings.Join(invalidAdminChannels, ", "))
  759. }
  760. if len(invalidServers) > 0 {
  761. logMsg += fmt.Sprintf("\n**- Download Servers: (%d)** - %s",
  762. len(invalidServers), strings.Join(invalidServers, ", "))
  763. }
  764. if len(invalidCategories) > 0 {
  765. logMsg += fmt.Sprintf("\n**- Download Categories: (%d)** - %s",
  766. len(invalidCategories), strings.Join(invalidCategories, ", "))
  767. }
  768. if len(invalidChannels) > 0 {
  769. logMsg += fmt.Sprintf("\n**- Download Channels: (%d)** - %s",
  770. len(invalidChannels), strings.Join(invalidChannels, ", "))
  771. }
  772. sendErrorMessage(logMsg)
  773. } else if config.Debug {
  774. log.Println(lg("Discord", "Validation", color.HiGreenString, "All sources successfully validated!"))
  775. }
  776. // Start Presence
  777. timeLastUpdated = time.Now()
  778. go updateDiscordPresence()
  779. mainWg.Done()
  780. }