├── .gitignore ├── README.md ├── config.json ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | *.agent 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Music recognition bot for Reddit 2 | 3 | [u/auddbot](https://www.reddit.com/user/auddbot) identifies music on Reddit. 4 | 5 | When someone mentions it or writes a question like "what's the song", it sends URL of the video (or livestream) to the AudD's [Music Recognition API](https://audd.io). 6 | 7 | Note that the code currently needs some cleaning up and doesn't follow the best practices. 8 | 9 | You can [support the bot on Patreon](https://patreon.com/audd). 10 | 11 | 12 | 13 | Alternatively, please **[consider donating to the most effective charities in the world](https://github.com/AudDMusic/RedditBot/wiki/Please-consider-donating)**. 14 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "AudDToken": "test", 3 | "UserAgent": "", 4 | "ClientID": "", 5 | "ClientSecret": "", 6 | "BotPasswords": {"auddbot": "", "RecognizeSong": ""}, 7 | "Triggers": [ 8 | "whats the song", "what is the song", "whats this song", "what is this song", 9 | "what song is this", "what song is playing", "what track is this" 10 | ], 11 | "AntiTriggers": [ 12 | "has been automatically removed", "your comment was removed", "I am a bot", 13 | "comment here with such low karma", "bot wants to find the best and worst bots" 14 | ], 15 | "MaxTriggerTextLength": 400, 16 | "LiveStreamMinScore": 70, 17 | "CommentsMinScore": 65, 18 | "ReplySettings": { 19 | "mention": { 20 | "SendLinks": true, 21 | "ReplyAlways": true 22 | }, 23 | "comment": { 24 | "SendLinks": false, 25 | "ReplyAlways": false 26 | }, 27 | "post": { 28 | "SendLinks": false, 29 | "ReplyAlways": false 30 | } 31 | }, 32 | "IgnoreSubreddits": [ 33 | "// subreddits bots should generally avoid", 34 | "suicidewatch", 35 | "depression", 36 | 37 | "// subreddits with account age thresholds", 38 | "wallstreetbets", "// 45 days", 39 | "yeagerbomb", "// 30 days", 40 | "NormalDayInArabia", "// 30 days", 41 | 42 | "// subreddits that banned the bot", 43 | "tiktokthots", 44 | "actuallesbians", 45 | "GraceBoorBoutineLA", 46 | "Minecraft", 47 | "anime", 48 | "BackshotsOnly", 49 | "TheArtistStudio", 50 | "MarioKart8Deluxe", 51 | "kansascity", 52 | "ChiefKeef", 53 | "Lostwave", 54 | "kpop", 55 | 56 | "LatinoPeopleTwitter", "// deletes all comments", 57 | ], 58 | "SubredditsBannedOn": [ 59 | "// subreddits that banned the bot", 60 | "deadbydaylight", 61 | "unrealengine", "dataisbeautiful", "UberEATS", "RealGirls", "ak47", "technology", "cosplay", "motorcycles", "popheads", "Biochemistry", "FortniteCreative", "Instagramreality", "PremierLeague", "INEEEEDIT", "WaterCoolerWednesday", "Warhammer", "gif", "sheetz", "Ningen", "LipsThatGrip", "OnOff", "InternetStars", "NoStupidQuestions", "TheGamerLounge", "Izlam", "TheCinemassacre", "mariokart", "Planetside", "pegging_unkinked", "specializedtools", "trees", "teenagers", "Music", "criticalrole", "blackdesertonline", "trashpandas", "bizarrelife", "Rainmeter", "interestingasfuck", "Naruto", "fightporn", "Drugs", "spaceengineers", "theticket", "magicthecirclejerking", "splatoon", "VRchat", "WTF", "TittyDrop", "benzodiazepines", "BlackMediaPresents", "avicii", "ThirdLifeSMP", "darkestdungeon", "NSFW_Korea", "BlackOutBoyz", "nba", "hiphopheads", "VerifiedFeet", "greenday", "pyrocynical", "playboicarti", "holdmyredbull", "chubby", "space", "electronicmusic", "pcmasterrace", "China", "UNBGBBIIVCHIDCTIICBG", "fpvracing", "SonicTheHedgehog", "Whatisthis", "INEEEEDIT", "Dashcam", "boobbounce", "Colorado", "FortNiteBR", "thelastofus", "vaporents", "FaceFuck", "French", "Jixaw", "beerporn", "DesiMeta", "camping", "hookah", "Rabbits", "AnimalCrossing", "LoveForAnimesexuals", "Eyebleach", "entwives", "Kakegurui", "KGATLW", "cars", "Muse", "BubbleHash", "CodeGeass", "StickyBrickLabs", "okbuddyretard", "projectzomboid", "Stormworks", "2bulgar4you", "THPS", "KerbalSpaceProgram", "tumblr", "Arkansas", "UFOs", "Kirby", "BollyBlindsNGossip", "kettlebell", "denvernuggets", "arttocope", "Relatable", "JusticeServed", "okbuddyredacted", "Ebony", "NLSSCircleJerk", "SquaredCircle", "TrollXChromosomes", "GetMoreViewsYT", "Romania", "dayz", "BaldursGate3", "HermitCraft", "chefknives", "youseeingthisshit", "German", "Blowjobs", "MercyMains", "ketamine", "TheGoodPlace", "NingguangMains", "CreepyArt", "NoFuckingComment", "Dragonballsuper", "GRAMBADDIES", "Awwducational", "CannabisExtracts", "craftymighty", "hermitcraftmemes", "Dinosaurs", "KitchenConfidential", "lotr", "terriblefacebookmemes", "Justfuckmyshitup", "srilanka", "HuskyTantrums", "NotMyProblem", "NASCAR", "nsfw", "CampingandHiking", "FastLED", "familyguy", "PillowHumping", "Warthunder", "okbuddyhetero", "awwnverts", "ALLTHEANIMALS", "RedLetterMedia", "ZeroWaste", "ik_ihe", "dauntless", "lianli", "traaaaaaannnnnnnnnns", "Stellaris", "GakiNoTsukai", "RedditDayOf", "gonewild", "blackpeoplegifs", "iran", "weddingplanning", "afghanistan", "collapse", "bizarrelife", "myog", "skylanders", "drugscirclejerk", "Quebec", "chefknives", "supermoto", "CHIBears", "listentothis", "dragonquest", "polls", "Whatcouldgowrong", "AnimalsBeingStrange", "hoi4", "2007scape", "ac_newhorizons", "Astros", "pokemonmemes", "Catloaf", "WorldWar2", "HVAC", "halifax", "Sourdough", "AnimalsOnReddit", "VALORANT", "onejob", "HypixelSkyblock", "trashpandas", "CrappyDesign", "MercyMains", "JoeRogan", "low_poly", "SCJerk", "awfullydark", "newretrowave", "bicycling", "RPClipsGTA", "DojaCat", "weed", "AskAnAmerican", "RealLifeShinies", "Barotrauma", "gravityfalls", "MMA", "SigmaGrindset", "farcry", "widgy", "ledzeppelin", "uAlberta", "1000ccplus", "FuckYouKaren", "GooglePixel", "gonewildaudio", "pinkfloyd", "LooneyTunesLogic", "HumanTippyTaps", "wow", "backpacking", "twerking", "Games", "raining", "dbz", "juicyasians", "leagueoflegends", "shrimptank", "BackdoorGoRe2", "blackladies", "piercing", "twicemedia", "foraging", "torontoraptors", "woodworking", "worldpolitics", "gtaonline", "GothStyle", "Catswithjobs", "AnimalsBeingJerks", "TikTok_Ass", "Panera", "Fantasy", "Godzillamemes", "creepydesign", "gardening", "geometrydash", "gtaonline", "thenetherlands", "CovIdiots", "cosplaygirls", "GenZedong", "formula1", "Haywire_Hill", "Autoflowers", "mbtimemes", "supersecretyachtclub", "dndmemes", "bullcity", "Nofans", "war", "arknights", "apple", "Frugal", "BlancaSoler", "Supernatural", "ich_iel", "RatchetAndClank", "Cryptomains", "kep1er", "jameswebb", "brushybrushy", "UCSD", "ZeroWaste", "ShinyPokemon", "aesthetic", "bostonceltics", "nsfwhardcore", "Braves", "TeamSolomid", "HighStrangeness", "furry", "The8BitRyanReddit", "blackchickswhitedicks", "GoreGore", "darkwingsdankmemes", "StrangeNewWorlds", "Bossfight", "NSFW_GIF", "Cumontits", "capybara", "vandwellers", "yurimemes", "MinecraftHelp", "shorthairchicks", "Eldenring", "translator", "moviescirclejerk", "LateStageCapitalism", "findareddit", "MTFSelfieTrain", "texas", "RussiaUkraineWar2022", "centuryhomes", "OneSecondBeforeDisast", "GunMemes", "NFA", "UkraineWarVideoReport", "BacktotheFuture", "proceduralgeneration", "RedHotChiliPeppers", "cannabiscentral", "Stormworks", "CasualUK", "AirForce", "couriersofreddit", "China_irl", "minipainting", "bioniclelego", "sandiego", "EscapefromTarkov", "AmericanFascism2020", "football", "shanghai", "NothingUnder", "bookscirclejerk", "indianews", "yesyesyesyesno", "argentina", "magicTCG", "Shadowverse", "catburnouts", "JizzedToThis", "cosplay","The_Crew", "vinyltoys", "composting", "ask", "420", "WastedTalent", "shittyrainbow6", "UkrainianConflict", "topcommentoftheday", "CitiesSkylines", "Colombia", "18_19", "ChoicesVIP", "Samaj", "redneckengineering", "gamedev", "Kazakhstan", "My600lbLife", "MarchAgainstNazis", "IWantItSoBad", "StrangerThingsMemes", "antifeminists", "babies", "YUROP", "arizona", "roblox", "psilocybin", "help", "Zoomies", "TheHandmaidsTale", "animalkingdom", "weaponsystems", "VShojo", "SALEM", "Thorgasm", "Gaming4Gamers", "Bloomer", "tuckedinkitties", "corgi", "FunnyAnimals", "trypophobia", "cute", "wii", "killteam", "jacksepticeye", "gopro", "me_irlgbt", "orangecounty", "Aquariums", "USAmemes", "libertarianmeme", "MasturbationGoneWild", "survivor", "freefolk", "cozy", "Barber", "overheardsex", "lastweektonight", "Xcom", "CoupleMemes", "lebanon", "DarkBRANDON", "Hawaii", "trump", "CrazyFuckingVideos", "NSFW_Social", "Dualsport", "Edgerunners", "lordoftherings", "audius", "PetTheDamnCat", "army", "weather", "washingtondc", "StanleyKubrick", "sailormoon", "sabaton", "GlitchInTheMatrix", "outdoorgrowing", "MusicMatch", "rainworld", "FORTnITE", "formuladank", "blackmagicfuckery", "lofi", "Dirtbikes", "overthegardenwall", "ich_iel", "MedicalCannabisAus", "cirkeltrek", "INDYCAR", "folkmetal", "Orgasms", "TearsOfThemis", "MemeHunter", "nfl", "tippytaps", "liberalgunowners", "vfx", "Notion", "DidntKnowIWantedThat", "microgrowery", "HimachalPradesh", "Truckers", "ModelY", "ytp", "ChiefKeef", "eyeblech", "IndianBabes", "CatholicMemes", "OldSchoolCool", 62 | "Damnthatsinteresting", 63 | 64 | "// deletes all comments", 65 | "LatinoPeopleTwitter", 66 | "4CHR" 67 | ], 68 | "ApprovedOn": [ 69 | "// subreddits where BotDefense banned the bot, but the mods unbanned it after I contacted them", 70 | "okbuddyretard", 71 | "nextfuckinglevel", 72 | "jacksepticeye", 73 | "MinecraftMemes", 74 | "BisexualTeens", 75 | "southafrica", 76 | "1000lbsisters", 77 | "NarcoFootage", 78 | "OnePunchMan", 79 | "BecomingTheIceman", 80 | "led", 81 | "Relatable", 82 | "funnyvideos", 83 | "Chodi", 84 | "brisbane", 85 | "hamsters", 86 | "trippy", 87 | "SeattleWA", 88 | "Choices", 89 | "bayarea", 90 | "neoliberal", 91 | "PewdiepieSubmissions", 92 | "Tools", 93 | "couplesgonewild", 94 | 95 | "// subreddits where links work", 96 | "TikTokCringe", 97 | "roblox", 98 | "shitposting", 99 | "Cringetopia", 100 | 101 | "// subreddits where links were approved", 102 | "blackmagicfuckery", 103 | 104 | "// subreddits asked to deliver the message in a single comment", 105 | "Bitcoin" 106 | ], 107 | "DontPostPatreonLinkOn": [ 108 | "BLACKPINK" 109 | ], 110 | "DontUseFormattingOn": [ 111 | "tipofmytongue" 112 | ], 113 | "RavenDSN": "" 114 | } 115 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module redditBot 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/AudDMusic/audd-go v0.2.5 7 | github.com/Mihonarium/go-profanity v0.0.0-20220116125849-662cde2e1ad4 8 | github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 // indirect 9 | github.com/fsnotify/fsnotify v1.4.9 10 | github.com/getsentry/raven-go v0.2.0 11 | github.com/golang/protobuf v1.3.2 12 | github.com/kodova/html-to-markdown v1.0.1 13 | github.com/pkg/errors v0.9.1 // indirect 14 | github.com/ttgmpsn/mira v0.1.12 15 | github.com/turnage/graw v0.0.0-20201204201853-a177df1b5c91 16 | github.com/turnage/redditproto v0.0.0-20151223012412-afedf1b6eddb 17 | github.com/vartanbeno/go-reddit/v2 v2.0.1 18 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba 19 | mvdan.cc/xurls/v2 v2.2.0 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/AudDMusic/audd-go v0.2.5 h1:t1h4Gw1LntcR1ySpC2Kq9W9rTK/XVgHUwKstRkQc3lI= 3 | github.com/AudDMusic/audd-go v0.2.5/go.mod h1:dCMyo6NXmoFxUNUAZv3Aq3GZm9EzzFrbqobt6fSipUk= 4 | github.com/Mihonarium/go-profanity v0.0.0-20220116125849-662cde2e1ad4 h1:ba4IJQbV0Z/z1fz29NovRTG5zwvvxSQ5fXPSYYSFuZQ= 5 | github.com/Mihonarium/go-profanity v0.0.0-20220116125849-662cde2e1ad4/go.mod h1:gmQbYS/Epg+w5M5WbbMy3tBBpN84r2wtJRnTY1g8YoI= 6 | github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= 7 | github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 8 | github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= 9 | github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= 10 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 13 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 14 | github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= 15 | github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= 16 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 17 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= 18 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 19 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 21 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 22 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 23 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 24 | github.com/jcuga/golongpoll v1.3.0 h1:00lQC7C1a/4YcGnWcdWi1YzJYfat1Hal2+Cnlvgyado= 25 | github.com/jcuga/golongpoll v1.3.0/go.mod h1:1ijFh83w68ylU44F+xSEyrXChP/7NnoAvgCVHWMggWA= 26 | github.com/kodova/html-to-markdown v1.0.1 h1:MJxQAnqxtss3DaPnm72DRV65HZiMQZF3DUAfEaTg+14= 27 | github.com/kodova/html-to-markdown v1.0.1/go.mod h1:NhDrT7QdSrdpezFg/0EQx9zeobCHR5oAguzrKrC6mVU= 28 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 31 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 32 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 33 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 34 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 35 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 36 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 42 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 43 | github.com/ttgmpsn/mira v0.1.12 h1:+YHDVvzdhlEJUocGAACNcoQLO+dpKBA/RECbSjS/CAo= 44 | github.com/ttgmpsn/mira v0.1.12/go.mod h1:M1GDCgTk+qajoPfwz3nUMJr+gym8Zb2MsDCYvKXaHwE= 45 | github.com/turnage/graw v0.0.0-20201204201853-a177df1b5c91 h1:vYoyWnsUWuvaLGe6369mItyePB2EVFRjrvkev7xFuGQ= 46 | github.com/turnage/graw v0.0.0-20201204201853-a177df1b5c91/go.mod h1:aAkq4I/q1izZSSwHvzhDn9NA+eGxgTSuibwP3MZRlQY= 47 | github.com/turnage/redditproto v0.0.0-20151223012412-afedf1b6eddb h1:qR56NGRvs2hTUbkn6QF8bEJzxPIoMw3Np3UigBeJO5A= 48 | github.com/turnage/redditproto v0.0.0-20151223012412-afedf1b6eddb/go.mod h1:GyqJdEoZSNoxKDb7Z2Lu/bX63jtFukwpaTP9ZIS5Ei0= 49 | github.com/vartanbeno/go-reddit/v2 v2.0.1 h1:P6ITpf5YHjdy7DHZIbUIDn/iNAoGcEoDQnMa+L4vutw= 50 | github.com/vartanbeno/go-reddit/v2 v2.0.1/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI= 51 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 52 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 53 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 54 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 55 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 56 | golang.org/x/net v0.0.0-20191109021931-daa7c04131f5/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 57 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= 58 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 59 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 60 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 61 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 62 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= 65 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 67 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= 68 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 69 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 70 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 72 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 73 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 75 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 76 | gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= 77 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 78 | mvdan.cc/xurls/v2 v2.2.0 h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A= 79 | mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8= 80 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "container/ring" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/AudDMusic/audd-go" 8 | "github.com/Mihonarium/go-profanity" 9 | "github.com/fsnotify/fsnotify" 10 | "github.com/getsentry/raven-go" 11 | "github.com/kodova/html-to-markdown/escape" 12 | "github.com/ttgmpsn/mira" 13 | "github.com/ttgmpsn/mira/models" 14 | "github.com/turnage/graw" 15 | reddit1 "github.com/turnage/graw/reddit" 16 | "golang.org/x/time/rate" 17 | "io/ioutil" 18 | "math" 19 | "mvdan.cc/xurls/v2" 20 | "net/http" 21 | "net/url" 22 | "os" 23 | "regexp" 24 | "runtime" 25 | "strconv" 26 | "strings" 27 | "sync" 28 | "sync/atomic" 29 | "time" 30 | ) 31 | 32 | type BotConfig struct { 33 | AudDToken string `required:"true" default:"test" usage:"the token from dashboard.audd.io" json:"AudDToken"` 34 | Triggers []string `usage:"phrases bot will react to" json:"Triggers"` 35 | AntiTriggers []string `usage:"phrases bot will avoid replying to" json:"AudDTAntiTriggers"` 36 | IgnoreSubreddits []string `usage:"subreddits to ignore" json:"IgnoreSubreddits"` 37 | SubredditsBannedOn []string `usage:"subreddits auddbot is banned on by BotDefense" json:"SubredditsBannedOn"` 38 | ApprovedOn []string `usage:"subreddits the bot can post links on" json:"ApprovedOn"` 39 | DontPostPatreonLinkOn []string `usage:"subreddits not to post Patreon links on'" json:"DontPostPatreonLinkOn"` 40 | DontUseFormattingOn []string `usage:"subreddits not to use formatting on'" json:"DontUseFormattingOn"` 41 | ReplySettings map[string]ReplyConfig `required:"true" json:"ReplySettings"` 42 | MaxTriggerTextLength int `json:"MaxTriggerTextLength"` 43 | LiveStreamMinScore int `json:"LiveStreamMinScore"` 44 | CommentsMinScore int `json:"CommentsMinScore"` 45 | PatreonSupporters []string `json:"patreon_supporters"` 46 | RavenDSN string `default:"" usage:"add a Sentry DSN to capture errors" json:"RavenDSN"` 47 | UserAgent string `json:"UserAgent"` 48 | ClientID string `json:"ClientID"` 49 | ClientSecret string `json:"ClientSecret"` 50 | BotPasswords map[string]string `json:"BotPasswords"` 51 | } 52 | 53 | var stats = map[string]int{} 54 | var statsMu = &sync.Mutex{} 55 | 56 | func addToStats(t string) int { 57 | statsMu.Lock() 58 | defer statsMu.Unlock() 59 | stats[t]++ 60 | return stats[t] 61 | } 62 | 63 | func (r *auddBot) getPatreonSupporter(botMentioned, fullMatch bool) string { 64 | if !botMentioned || !fullMatch { 65 | return "" 66 | } 67 | if len(r.config.PatreonSupporters) == 0 { 68 | return "" 69 | } 70 | i := addToStats("patreon shout-outs") 71 | return r.config.PatreonSupporters[i%len(r.config.PatreonSupporters)] 72 | } 73 | 74 | type ReplyConfig struct { 75 | SendLinks bool `required:"true" usage:"should bot share links in the first reply" json:"SendLinks"` 76 | ReplyAlways bool `required:"true" usage:"will bot reply when hasn't recognized a song" json:"ReplyAlways"` 77 | } 78 | 79 | var commentsCounter int64 = 0 80 | var postsCounter int64 = 0 81 | 82 | var markdownRegex = regexp.MustCompile(`\[[^][]+]\((https?://[^()]+)\)`) 83 | var rxStrict = xurls.Strict() 84 | 85 | const configFile = "config.json" 86 | const AudDBotUsername = "auddbot" 87 | const RecognizeSongBotUsername = "RecognizeSong" 88 | 89 | func stringInSlice(slice []string, s string) bool { 90 | for i := range slice { 91 | if s == slice[i] { 92 | return true 93 | } 94 | } 95 | return false 96 | } 97 | func substringInSlice(s string, slice []string) bool { 98 | for i := range slice { 99 | if strings.Contains(s, slice[i]) { 100 | return true 101 | } 102 | } 103 | return false 104 | } 105 | 106 | func TimeStringToSeconds(s string) (int, error) { 107 | list := strings.Split(s, ":") 108 | if len(list) > 3 { 109 | return 0, fmt.Errorf("too many : thingies") 110 | } 111 | result, multiplier := 0, 1 112 | for i := len(list) - 1; i >= 0; i-- { 113 | c, err := strconv.Atoi(list[i]) 114 | if err != nil { 115 | return 0, err 116 | } 117 | result += c * multiplier 118 | multiplier *= 60 119 | } 120 | return result, nil 121 | } 122 | 123 | func SecondsToTimeString(i int, includeHours bool) string { 124 | if includeHours { 125 | return fmt.Sprintf("%02d:%02d:%02d", i/3600, (i%3600)/60, i%60) 126 | } 127 | return fmt.Sprintf("%02d:%02d", i/60, i%60) 128 | } 129 | 130 | func GetTimeFromText(s string) (int, int) { 131 | s = strings.ReplaceAll(s, " - ", "") 132 | s = strings.ReplaceAll(s, " @", " ") 133 | s = strings.ReplaceAll(s, "?", " ") 134 | words := strings.Split(s, " ") 135 | Time := 0 136 | TimeTo := 0 137 | maxScore := 0 138 | for _, w := range words { 139 | score := 0 140 | w2 := "" 141 | if strings.Contains(w, "-") { 142 | w2 = strings.Split(w, "-")[1] 143 | w = strings.Split(w, "-")[0] 144 | score += 1 145 | } 146 | w = strings.TrimSuffix(w, "s") 147 | w2 = strings.TrimSuffix(w2, "s") 148 | if strings.Contains(w, ":") { 149 | score += 2 150 | } 151 | if score > maxScore { 152 | t, err := TimeStringToSeconds(w) 153 | if err == nil { 154 | Time = t 155 | TimeTo, _ = TimeStringToSeconds(w2) // if w2 is empty or not a correct time, TimeTo is 0 156 | maxScore = score 157 | } 158 | } 159 | } 160 | return Time, TimeTo 161 | } 162 | 163 | func linksFromBody(body string) [][]string { 164 | results := markdownRegex.FindAllStringSubmatch(body, -1) 165 | //if len(results) == 0 { 166 | plaintextUrls := rxStrict.FindAllString(body, -1) 167 | for i := range plaintextUrls { 168 | plaintextUrls[i] = strings.ReplaceAll(plaintextUrls[i], "\\", "") 169 | results = append(results, []string{plaintextUrls[i], plaintextUrls[i]}) 170 | } 171 | //} 172 | return results 173 | } 174 | 175 | func getJSON(URL string, v interface{}) error { 176 | resp, err := http.Get(URL) 177 | if err != nil { 178 | return err 179 | } 180 | defer captureFunc(resp.Body.Close) 181 | body, err := ioutil.ReadAll(resp.Body) 182 | if err != nil { 183 | return err 184 | } 185 | err = json.Unmarshal(body, v) 186 | return err 187 | } 188 | 189 | func (r *auddBot) GetLinkFromComment(mention *reddit1.Message, commentsTree []*models.Comment, post *models.Post) (string, error) { 190 | // Check: 191 | // - first for the links in the comment 192 | // - then for the links in the comment to which it was a reply 193 | // - then for the links in the post 194 | var resultUrl string 195 | if post != nil { 196 | resultUrl = post.URL 197 | } 198 | if strings.Contains(resultUrl, "reddit.com/rpan") { 199 | s := strings.Split(resultUrl, "/") 200 | jsonUrl := "https://strapi.reddit.com/videos/t3_" + s[len(s)-1] 201 | var page RedditStreamJSON 202 | err := getJSON(jsonUrl, &page) 203 | if err != nil { 204 | return "", err 205 | } 206 | resultUrl = page.Data.Stream.HlsURL 207 | if resultUrl != "" { 208 | return resultUrl, nil 209 | } 210 | } 211 | if len(commentsTree) > 0 { 212 | if commentsTree[0] != nil { 213 | if resultUrl == "" { 214 | //resultUrl = commentsTree[0].r2.LinkURL 215 | } 216 | if mention == nil { 217 | mention = commentToMessage(commentsTree[0]) 218 | if len(commentsTree) > 1 { 219 | commentsTree = commentsTree[1:] 220 | } else { 221 | commentsTree = make([]*models.Comment, 0) 222 | } 223 | } 224 | } 225 | } else { 226 | if mention == nil { 227 | if post == nil { 228 | return "", fmt.Errorf("mention, commentsTree, and post are all nil") 229 | } 230 | mention = &reddit1.Message{ 231 | Context: post.Permalink, 232 | Body: post.Selftext, 233 | } 234 | } 235 | } 236 | if resultUrl == "" { 237 | j, _ := json.Marshal(post) 238 | fmt.Printf("Got a post that's not a link or an empty post (https://www.reddit.com%s, %s)\n", mention.Context, string(j)) 239 | //return "", fmt.Errorf("got a post without any URL (%s)", string(j)) 240 | resultUrl = "https://www.reddit.com" + mention.Context 241 | } 242 | if strings.Contains(resultUrl, "reddit.com/") || resultUrl == "" { 243 | if post != nil { 244 | if strings.Contains(post.Selftext, "https://reddit.com/link/"+post.ID+"/video/") { 245 | s := strings.Split(post.Selftext, "https://reddit.com/link/"+post.ID+"/video/") 246 | s = strings.Split(s[1], "/") 247 | resultUrl = "https://v.redd.it/" + s[0] + "/" 248 | } 249 | } 250 | } 251 | // Use links: 252 | results := linksFromBody(mention.Body) // from the comment 253 | if len(commentsTree) > 0 { 254 | if commentsTree[0] != nil { 255 | results = append(results, linksFromBody(commentsTree[0].Body)...) // from the comment it's a reply to 256 | } else { 257 | capture(fmt.Errorf("commentTree shouldn't contain an r2 entry at this point! %v", commentsTree)) 258 | } 259 | } 260 | if !strings.Contains(resultUrl, "reddit.com/") && resultUrl != "" { 261 | results = append(results, []string{resultUrl, resultUrl}) // that's the post link 262 | } 263 | if post != nil { 264 | results = append(results, linksFromBody(post.Selftext)...) // from the post body 265 | } 266 | if len(results) != 0 { 267 | // And now get the first link that's OK 268 | fmt.Println("Parsed from the text:", results) 269 | for u := range results { 270 | if strings.HasPrefix(results[u][1], "/") { 271 | continue 272 | } 273 | if strings.HasPrefix(results[u][1], "https://www.reddit.com/") { 274 | continue 275 | } 276 | resultUrl = results[u][1] 277 | break 278 | } 279 | } 280 | 281 | if strings.Contains(resultUrl, "reddit.com/") { 282 | jsonUrl := resultUrl + ".json" 283 | var page RedditPageJSON 284 | err := getJSON(jsonUrl, &page) 285 | if !capture(err) { 286 | if len(page) > 0 { 287 | if len(page[0].Data.Children) > 0 { 288 | if page[0].Data.Children[0].Data.RpanVideo.HlsURL != "" { 289 | resultUrl = page[0].Data.Children[0].Data.RpanVideo.HlsURL 290 | } 291 | if strings.Contains(page[0].Data.Children[0].Data.URL, "v.redd.it") { 292 | resultUrl = page[0].Data.Children[0].Data.URL 293 | } else { 294 | if len(page[0].Data.Children[0].Data.MediaMetadata) > 0 { 295 | for s := range page[0].Data.Children[0].Data.MediaMetadata { 296 | resultUrl = "https://v.redd.it/" + s + "/" 297 | } 298 | } 299 | } 300 | } 301 | } 302 | } 303 | } 304 | return resultUrl, nil 305 | } 306 | 307 | func (r *auddBot) GetVideoLink(mention *reddit1.Message, comment *models.Comment) (string, string, error) { 308 | var post *models.Post 309 | if mention == nil && comment == nil { 310 | return "", "", fmt.Errorf("empty mention and comment") 311 | } 312 | var parentId models.RedditID 313 | commentsTree := make([]*models.Comment, 0) 314 | if mention != nil { 315 | parentId = models.RedditID(mention.ParentID) 316 | } else { 317 | parentId = comment.ParentID 318 | commentsTree = append(commentsTree, comment) 319 | } 320 | postId := parentId 321 | for parentId != "" { 322 | postId = parentId 323 | //posts, comments, _, _, err := r.client.Listings.Get(context.Background(), parentId) 324 | parent, err := r.r.SubmissionInfoID(parentId) 325 | //r.bot.Listing(parentId, ) 326 | j, _ := json.Marshal(parent) 327 | fmt.Printf("parent [%s]: %s\n", string(parentId), string(j)) 328 | if err != nil && err.Error() != "no results" { 329 | return "", "", err 330 | } 331 | if err == nil { 332 | if p, ok := parent.(*models.Post); ok { 333 | post = p 334 | break 335 | } 336 | if c, ok := parent.(*models.Comment); ok { 337 | commentsTree = append(commentsTree, c) 338 | parentId = c.ParentID 339 | } else { 340 | return "", "", fmt.Errorf("got a result that's neither a post nor a comment, parent ID %s, %v", 341 | parentId, parent) 342 | } 343 | } else { 344 | parentId = "" 345 | } 346 | } 347 | l, err := r.GetLinkFromComment(mention, commentsTree, post) 348 | return l, string(postId), err 349 | } 350 | 351 | func isEmpty(e ...string) bool { 352 | for _, s := range e { 353 | if s != "" { 354 | return false 355 | } 356 | } 357 | return true 358 | } 359 | 360 | func GetReply(result []audd.RecognitionEnterpriseResult, withLinks, matched, full, showLabel bool, minScore int) string { 361 | if len(result) == 0 { 362 | return "" 363 | } 364 | links := map[string]bool{} 365 | texts := make([]string, 0) 366 | numResults := 0 367 | for _, results := range result { 368 | for _, song := range results.Songs { 369 | if song.Score >= minScore { 370 | numResults++ 371 | } 372 | } 373 | } 374 | for _, results := range result { 375 | if len(results.Songs) == 0 { 376 | capture(fmt.Errorf("enterprise response has a result without any songs")) 377 | } 378 | for _, song := range results.Songs { 379 | if song.Score < minScore { 380 | continue 381 | } 382 | 383 | if song.SongLink == "https://lis.tn/rvXTou" || song.SongLink == "https://lis.tn/XIhppO" { 384 | song.Artist = "The Caretaker (Leyland James Kirby)" 385 | song.Title = "Everywhere at the End of Time - Stage 1" 386 | song.Album = "Everywhere at the End of Time - Stage 1" 387 | song.ReleaseDate = "2016-09-22" 388 | song.SongLink = "https://www.youtube.com/watch?v=wJWksPWDKOc" 389 | } 390 | if strings.Contains(song.SongLink, "youtube.com") { 391 | song.SongLink = strings.ReplaceAll(song.SongLink, "https://www.youtube.com/watch?v=", "https://lis.tn/yt/") 392 | song.SongLink = strings.ReplaceAll(song.SongLink, "https://youtube.com/watch?v=", "https://lis.tn/yt/") 393 | } 394 | if strings.Contains(song.SongLink, "youtu.be") { 395 | song.SongLink = strings.ReplaceAll(song.SongLink, "https://youtu.be/", "https://lis.tn/yt/") 396 | } 397 | if song.SongLink != "" { 398 | if _, exists := links[song.SongLink]; exists { // making sure this song isn't a duplicate 399 | continue 400 | } 401 | links[song.SongLink] = true 402 | } 403 | song.Title = profanity.MaskProfanityWithoutKeepingSpaceTypes(song.Title, "*", 2) 404 | song.Artist = profanity.MaskProfanityWithoutKeepingSpaceTypes(song.Artist, "*", 2) 405 | song.Album = profanity.MaskProfanityWithoutKeepingSpaceTypes(song.Album, "*", 2) 406 | song.Label = profanity.MaskProfanityWithoutKeepingSpaceTypes(song.Label, "*", 2) 407 | song.Title = escape.Markdown(song.Title) 408 | song.Artist = escape.Markdown(song.Artist) 409 | song.Album = escape.Markdown(song.Album) 410 | song.Label = escape.Markdown(song.Label) 411 | if strings.Contains(song.Timecode, ":") { 412 | ms := strings.Split(song.Timecode, ":") 413 | m, _ := strconv.Atoi(ms[0]) 414 | s, _ := strconv.Atoi(ms[1]) 415 | song.SongLink += "?t=" + strconv.Itoa(m*60+s) 416 | } 417 | score := strconv.Itoa(song.Score) + "%" 418 | text := fmt.Sprintf("[**%s** by %s](%s)", 419 | song.Title, song.Artist, song.SongLink) 420 | if !withLinks { 421 | text = fmt.Sprintf("**%s** by %s", 422 | song.Title, song.Artist) 423 | } 424 | scoreInfo := "" 425 | if matched { 426 | text += fmt.Sprintf(" (%s; matched: `%s`)", song.Timecode, score) 427 | scoreInfo = fmt.Sprintf("\n\n**Score:** %s (timecode: %s)", score, song.Timecode) 428 | } 429 | if numResults == 1 && full { 430 | text = fmt.Sprintf("**Name:** %s\n\n**Artist:** %s%s", 431 | song.Title, song.Artist, scoreInfo) 432 | // if full { 433 | text += fmt.Sprintf("\n\n**Album:** %s\n\n**Label:** %s\n\n**Released on:** %s", 434 | song.Album, song.Label, song.ReleaseDate) 435 | // } 436 | if withLinks { 437 | text += fmt.Sprintf("\n\n[Apple Music, Spotify, YouTube, etc.](%s)", song.SongLink) 438 | } 439 | } 440 | if full && numResults > 1 { 441 | album := "" 442 | label := "" 443 | releaseDate := "" 444 | if song.Title != song.Album && song.Album != "" { 445 | album = "**Album**: " + song.Album + ". " 446 | } 447 | if song.Artist != song.Label && song.Label != "Self-released" && song.Label != "" { 448 | if showLabel { 449 | label = " **by** " + song.Label 450 | } 451 | } 452 | if song.ReleaseDate != "" { 453 | releaseDate = "**Released on** " + song.ReleaseDate 454 | } else { 455 | if label != "" { 456 | label = "**Label**: " + song.Label 457 | } 458 | } 459 | if !isEmpty(album, label, releaseDate) { 460 | text += fmt.Sprintf("\n\n%s%s%s.", 461 | album, releaseDate, label) 462 | } 463 | } 464 | texts = append(texts, text) 465 | } 466 | } 467 | if len(texts) == 0 { 468 | return "" 469 | } 470 | response := texts[0] 471 | if len(texts) > 1 { 472 | response = "" 473 | if full { 474 | response = "I got matches with these songs:" 475 | } 476 | for _, text := range texts { 477 | // response += fmt.Sprintf("\n\n%d. %s", i+1, text) 478 | response += fmt.Sprintf("\n\n• %s", text) 479 | } 480 | } else { 481 | if full { 482 | response = "**Song Found!**\n\n" + response 483 | } 484 | } 485 | return response 486 | } 487 | 488 | type d struct { 489 | b bool 490 | u string 491 | } 492 | 493 | var avoidDuplicates = map[string]chan d{} 494 | var avoidDoubleDuplicates = map[string]bool{} 495 | var avoidDuplicatesMu = &sync.Mutex{} 496 | 497 | var myReplies = map[string]myComment{} 498 | 499 | type myComment struct { 500 | summoned bool 501 | commentID string 502 | additionalCommentID string 503 | } 504 | 505 | var myRepliesMu = &sync.Mutex{} 506 | 507 | func GetSkipFirstFromLink(Url string) int { 508 | skip := 0 509 | if strings.HasSuffix(Url, ".m3u8") { 510 | return skip 511 | } 512 | u, err := url.Parse(Url) 513 | if err == nil { 514 | t := u.Query().Get("t") 515 | if t == "" { 516 | t = u.Query().Get("time_continue") 517 | if t == "" { 518 | t = u.Query().Get("start") 519 | } 520 | } 521 | if t != "" { 522 | t = strings.ToLower(strings.ReplaceAll(t, "s", "")) 523 | tInt := 0 524 | if strings.Contains(t, "m") { 525 | s := strings.Split(t, "m") 526 | tsInt, _ := strconv.Atoi(s[1]) 527 | tInt += tsInt 528 | if strings.Contains(s[0], "h") { 529 | s := strings.Split(s[0], "h") 530 | if tmInt, err := strconv.Atoi(s[1]); !capture(err) { 531 | tInt += tmInt * 60 532 | } 533 | if thInt, err := strconv.Atoi(s[0]); !capture(err) { 534 | tInt += thInt * 60 * 60 535 | } 536 | } else { 537 | if tmInt, err := strconv.Atoi(s[0]); !capture(err) { 538 | tInt += tmInt * 60 539 | } 540 | } 541 | } else { 542 | if tsInt, err := strconv.Atoi(t); !capture(err) { 543 | tInt = tsInt 544 | } 545 | } 546 | skip += tInt 547 | fmt.Println("skip:", skip) 548 | } 549 | } 550 | return skip 551 | } 552 | 553 | func removeFormatting(response string) string { 554 | response = strings.ReplaceAll(response, "**", "*") 555 | response = strings.ReplaceAll(response, "*", "\\*") 556 | response = strings.ReplaceAll(response, "`", "'") 557 | response = strings.ReplaceAll(response, "^", "") 558 | return response 559 | } 560 | 561 | const enterpriseChunkLength = 12 562 | 563 | func (r *auddBot) HandleQuery(mention *reddit1.Message, comment *models.Comment, post *models.Post) { 564 | var resultUrl, t, parentID, body, subreddit, author, permalink, postId string 565 | var err error 566 | if mention != nil { 567 | t, parentID, body, subreddit, author = 568 | "mention", mention.Name, mention.Body, mention.Subreddit, mention.Author 569 | fmt.Println("\n ! Processing the mention") 570 | } 571 | if comment != nil { 572 | t, parentID, body, subreddit, author, permalink = 573 | "comment", string(comment.GetID()), comment.Body, comment.Subreddit, comment.Author, comment.Permalink 574 | } 575 | if post != nil { 576 | t, parentID, body, subreddit, author, permalink = 577 | "post", string(post.GetID()), post.Selftext, post.Subreddit, post.Author, post.Permalink 578 | } 579 | 580 | body = strings.ToLower(body) 581 | rs := strings.Contains(body, "recognizesong") 582 | summoned := rs || strings.Contains(body, "auddbot") 583 | 584 | if len(body) > r.config.MaxTriggerTextLength && r.config.MaxTriggerTextLength != 0 && !summoned { 585 | fmt.Println("The comment is too long, skipping", body) 586 | return 587 | } 588 | 589 | // Avoid handling of both the comment from r/all and the mention 590 | var previousUrl string 591 | var c chan d 592 | var exists bool 593 | avoidDuplicatesMu.Lock() 594 | if c, exists = avoidDuplicates[parentID]; exists { 595 | delete(avoidDuplicates, parentID) 596 | avoidDoubleDuplicates[parentID] = true 597 | avoidDuplicatesMu.Unlock() 598 | results := <-c 599 | if results.b { 600 | fmt.Println("Ignored a duplicate") 601 | return 602 | } 603 | fmt.Println("Attempting to recognize the song again") 604 | c = make(chan d, 1) 605 | previousUrl = results.u 606 | } else { 607 | if avoidDoubleDuplicates[parentID] { 608 | avoidDuplicatesMu.Unlock() 609 | fmt.Println("Ignored a double duplicate") 610 | return 611 | } 612 | c = make(chan d, 1) 613 | avoidDuplicates[parentID] = c 614 | avoidDuplicatesMu.Unlock() 615 | } 616 | 617 | if post != nil { 618 | resultUrl, err = r.GetLinkFromComment(nil, nil, post) 619 | } else { 620 | resultUrl, postId, err = r.GetVideoLink(mention, comment) 621 | if t == "mention" { 622 | permalink = fmt.Sprintf("/r/%s/comments/%s/%s", subreddit, postId, parentID) 623 | } 624 | } 625 | if capture(err) { 626 | return 627 | } 628 | 629 | if strings.Contains(resultUrl, "https://lis.tn/") { 630 | fmt.Println("Skipping a reply to our comment") 631 | return 632 | } 633 | if resultUrl == previousUrl { 634 | fmt.Println("Got the same URL, skipping") 635 | return 636 | } 637 | fmt.Println(resultUrl) 638 | limit := 2 639 | if strings.Contains(resultUrl, "v.redd.it") { 640 | limit = 3 641 | } 642 | isLivestream := strings.HasSuffix(resultUrl, ".m3u8") 643 | if isLivestream { 644 | fmt.Println("\nGot a livestream", resultUrl) 645 | if summoned { 646 | reply := "I'll listen to the next " + strconv.Itoa(enterpriseChunkLength) + " seconds of the stream and try to identify the song" 647 | if rs { 648 | go r.r.ReplyWithID(parentID, reply) 649 | 650 | } else { 651 | go r.r2.ReplyWithID(parentID, reply) 652 | } 653 | } 654 | limit = 1 655 | } 656 | minScore := r.config.CommentsMinScore 657 | if isLivestream { 658 | minScore = r.config.LiveStreamMinScore 659 | } 660 | withLinks := (summoned || r.config.ReplySettings[t].SendLinks || stringInSlice(r.config.ApprovedOn, subreddit)) && 661 | !strings.Contains(body, "without links") && !strings.Contains(body, "/wl") || isLivestream 662 | timestampTo := 0 663 | timestamp := GetSkipFirstFromLink(resultUrl) 664 | if timestamp == 0 { 665 | timestamp, timestampTo = GetTimeFromText(body) 666 | } 667 | if timestampTo != 0 && timestampTo-timestamp > limit*enterpriseChunkLength { 668 | // recognize music at the middle of the specified interval 669 | timestamp += (timestampTo - timestamp - limit*enterpriseChunkLength) / 2 670 | } 671 | timestampTo = timestamp + limit*enterpriseChunkLength 672 | atTheEnd := "false" 673 | if timestamp == 0 && strings.Contains(body, "at the end") && !isLivestream { 674 | atTheEnd = "true" 675 | } 676 | result, err := r.audd.RecognizeLongAudio(resultUrl, 677 | map[string]string{"accurate_offsets": "true", "limit": strconv.Itoa(limit), 678 | "skip_first_seconds": strconv.Itoa(timestamp), "reversed_order": atTheEnd}) 679 | useFormatting := !stringInSlice(r.config.DontUseFormattingOn, subreddit) 680 | response := GetReply(result, withLinks, true, !isLivestream, false, minScore) 681 | if err != nil { 682 | if v, ok := err.(*audd.Error); ok { 683 | if v.ErrorCode == 501 { 684 | response = fmt.Sprintf("Sorry, I couldn't get any audio from the [link](%s)", resultUrl) 685 | if strings.Contains(resultUrl, "youtube.com") || strings.Contains(resultUrl, "youtu.be") { 686 | response += ". \n\nSometimes I have trouble with YouTube videos, and don't work for long (usually 1.5h+) videos or ones that are geo-blocked/age-gated. If relevant also note that I don't work for YouTube Clips - I need the direct link to the video and the timestamp, for example `https://youtu.be/AbCdEfGhI at 1:48` or timestamped like `https://youtu.be/AbCdEfGhI?t=1m48s`." 687 | } 688 | if !r.config.ReplySettings[t].ReplyAlways && !summoned { 689 | fmt.Println("not summoned and couldn't get any audio, exiting") 690 | return 691 | } 692 | } 693 | } 694 | if response == "" { 695 | capture(err) 696 | c <- d{false, resultUrl} 697 | return 698 | } 699 | } 700 | footerLinks := []string{ 701 | "*I am a bot and this action was performed automatically*", 702 | "[GitHub](https://github.com/AudDMusic/RedditBot) " + 703 | "[^(new issue)](https://github.com/AudDMusic/RedditBot/issues/new)", 704 | "[Donate](https://github.com/AudDMusic/RedditBot/wiki/Please-consider-donating)", 705 | //"[Feedback](/message/compose?to=Mihonarium&subject=Music%20recognition%20" + parentID + ")", 706 | } 707 | donateLink := 2 708 | if response == "" || len(result) == 0 { 709 | if exists { 710 | fmt.Println("Couldn't recognize in a duplicate") 711 | return 712 | } 713 | if strings.Contains(resultUrl, "https://www.reddit.com/") { 714 | c <- d{true, resultUrl} 715 | } else { 716 | c <- d{false, resultUrl} 717 | } 718 | if !r.config.ReplySettings[t].ReplyAlways && !summoned { 719 | fmt.Println("No result") 720 | return 721 | } 722 | } else { 723 | highestScore := 0 724 | for i := range result { 725 | if result[i].Songs[0].Score > highestScore { 726 | highestScore = result[i].Songs[0].Score 727 | } 728 | } 729 | shoutOutToPatreonSupporter := r.getPatreonSupporter(summoned, highestScore == 100) 730 | if shoutOutToPatreonSupporter != "" { 731 | footerLinks[2] += " ^(Music recognition costs a lot. This result was brought to you by our Patreon supporter, " + 732 | shoutOutToPatreonSupporter + ")" 733 | } else { 734 | if highestScore == 100 { 735 | footerLinks[2] += " ^(Please consider supporting me on Patreon. Music recognition costs a lot)" 736 | } 737 | } 738 | if highestScore < 100 && !isLivestream { 739 | footerLinks[0] += " | If the matched percent is less than 100, it could be a false positive result. " + 740 | "I'm still posting it, because sometimes I get it right even if I'm not sure, so it could be helpful. " + 741 | "But please don't be mad at me if I'm wrong! I'm trying my best!" 742 | } 743 | c <- d{true, resultUrl} 744 | } 745 | if strings.Contains(body, "find-song") && !summoned { 746 | footerLinks[0] += " | ^(find-song's creator gave me a permission to react to the find-song mentions)" 747 | } 748 | //if len(result) == 0 || !summoned { 749 | if len(result) == 0 || stringInSlice(r.config.DontPostPatreonLinkOn, subreddit) { 750 | footerLinks = append(footerLinks[:donateLink], footerLinks[donateLink+1:]...) 751 | } 752 | footer := "\n\n" + strings.Join(footerLinks, " | ") 753 | 754 | if isLivestream { 755 | fmt.Println("\nStream results:", result) 756 | } 757 | if response == "" { 758 | at := SecondsToTimeString(timestamp, timestampTo >= 3600) + "-" + SecondsToTimeString(timestampTo, timestampTo >= 3600) 759 | if atTheEnd == "true" { 760 | at = "the end" 761 | } 762 | response = fmt.Sprintf("Sorry, I couldn't recognize the song."+ 763 | "\n\nI tried to identify music from the [link](%s) at %s.", 764 | resultUrl, at) 765 | if strings.Contains(resultUrl, "https://www.reddit.com/") { 766 | response = "Sorry, I couldn't get the video URL from the post or your comment." 767 | } 768 | } 769 | if withLinks { 770 | response += footer 771 | } 772 | if !useFormatting { 773 | response = removeFormatting(response) 774 | } 775 | // fmt.Println(response) 776 | var cr *models.CommentActionResponse 777 | if rs { 778 | cr, err = r.r.ReplyWithID(parentID, response) 779 | } else { 780 | cr, err = r.r2.ReplyWithID(parentID, response) 781 | } 782 | if err != nil { 783 | capture(fmt.Errorf("%v from r/%s", err, subreddit)) 784 | if summoned { 785 | subject := "Music found! (but I couldn't post the reply)" 786 | if len(result) > 0 && !withLinks { 787 | response = GetReply(result, true, true, !isLivestream, true, 0) 788 | // PM the result 789 | } 790 | if len(result) == 0 { 791 | subject = "Sorry, I couldn't identify the song" 792 | } 793 | response = "(In reply to your [mention](https://reddit.com" + permalink + "):)\n\n" + response 794 | if rs { 795 | r.r.Redditor(author).Compose(subject, response) 796 | } else { 797 | r.r2.Redditor(author).Compose(subject, response) 798 | } 799 | } 800 | } else { 801 | if len(cr.JSON.Data.Things) > 0 { 802 | sentID := string(cr.JSON.Data.Things[0].Data.GetID()) 803 | comment := myComment{ 804 | summoned: summoned, 805 | commentID: sentID, 806 | } 807 | if !withLinks { 808 | response = "Apple Music, Spotify, YouTube, etc.:\n\n" 809 | response += GetReply(result, true, false, false, false, minScore) 810 | response += footer 811 | if !useFormatting { 812 | response = removeFormatting(response) 813 | } 814 | if rs { 815 | cr, err = r.r.ReplyWithID(sentID, response) 816 | } else { 817 | cr, err = r.r2.ReplyWithID(sentID, response) 818 | } 819 | if !capture(err) { 820 | if len(cr.JSON.Data.Things) > 0 { 821 | sentID := string(cr.JSON.Data.Things[0].Data.GetID()) 822 | comment.additionalCommentID = sentID 823 | myRepliesMu.Lock() 824 | myReplies[sentID] = comment 825 | myRepliesMu.Unlock() 826 | } 827 | } 828 | } 829 | myRepliesMu.Lock() 830 | myReplies[sentID] = comment 831 | myRepliesMu.Unlock() 832 | } 833 | } 834 | if summoned && len(result) > 0 && err == nil { 835 | var lowConfidenceResult []audd.RecognitionEnterpriseResult 836 | for i := range result { 837 | if result[i].Songs[0].Score < minScore { 838 | lowConfidenceResult = append(lowConfidenceResult, result[i]) 839 | } 840 | } 841 | if len(lowConfidenceResult) > 0 { 842 | // PM the result 843 | response = GetReply(lowConfidenceResult, true, true, false, true, 0) 844 | response = "In reply to your [mention](https://reddit.com" + permalink + "), " + 845 | "some additional low-confidence results (likely to be false-positives, but sharing just in case):\n\n" + response 846 | if rs { 847 | r.r.Redditor(author).Compose("Some additional low-confidence results", response) 848 | } else { 849 | r.r2.Redditor(author).Compose("Some additional low-confidence results", response) 850 | } 851 | } 852 | } 853 | } 854 | 855 | func getBannedText(subreddit string) string { 856 | return "Sorry, the bot was banned on r/" + subreddit + ". It's likely a mistake or done automatically by BotDefense. " + 857 | "There are many bots on Reddit used for spam, and protecting the community from those is important; but sometimes, " + 858 | "genuinely helpful bots, like our u/auddbot, get in the way. We'd be grateful if you could contact the subreddit " + 859 | "moderators, explain the situation to them, and ask them to lift the ban. \n\n" + 860 | "If they agree to lift the ban, please let us know by replying to this message. We'll then change the bot config " + 861 | "so it knows it's unbanned and can now reply to comments on r/" + subreddit + ". Thank you! <3 \n\n" + 862 | "(You can also mention u/RecognizeSong instead.)" 863 | } 864 | 865 | func (r *auddBot) Mention(p *reddit1.Message) error { 866 | // Note: it looks like we don't get all the mentions through this 867 | // In particular, we don't get mentions in replies to our comments 868 | //j, _ := json.Marshal(p) 869 | //fmt.Println("\n😻 Got a mention", string(j)) 870 | fmt.Println("\n😻 Got a mention", p.Body) 871 | if !p.New { 872 | fmt.Println("Not a new mention") 873 | return nil 874 | } 875 | compare := getBodyToCompare(p.Body) 876 | if substringInSlice(compare, r.config.AntiTriggers) { 877 | fmt.Println("Got an anti-trigger", p.Body) 878 | return nil 879 | } 880 | if stringInSlice([]string{"auddbot", "RecognizeSong"}, p.Author) { 881 | fmt.Println("Ignoring a comment from itself", p.Body) 882 | return nil 883 | } 884 | if stringInSlice(r.config.IgnoreSubreddits, p.Subreddit) { 885 | return nil 886 | } 887 | if stringInSlice(r.config.SubredditsBannedOn, p.Subreddit) && !strings.Contains(compare, "u/recognizesong") { 888 | avoidDuplicatesMu.Lock() 889 | if _, exists := avoidDuplicates[p.ParentID]; exists { 890 | avoidDuplicatesMu.Unlock() 891 | return nil 892 | } 893 | c := make(chan d, 1) 894 | avoidDuplicates[p.ParentID] = c 895 | avoidDuplicatesMu.Unlock() 896 | r.r2.Redditor(p.Author).Compose("Sorry, I'm banned on r/"+p.Subreddit, getBannedText(p.Subreddit)) 897 | fmt.Println("Ignoring a mention from", p.Subreddit, "https://reddit.com"+p.Name) 898 | return nil 899 | } 900 | go func() { 901 | capture(r.r.Me().ReadMessage(p.Name)) 902 | }() 903 | r.HandleQuery(p, nil, nil) 904 | return nil 905 | } 906 | func (r *auddBot) CommentReply(p *reddit1.Message) error { 907 | // Note: it looks like we don't get all the mentions through this 908 | // In particular, we don't get mentions in replies to our comments 909 | //j, _ := json.Marshal(p) 910 | //fmt.Println("\n😻 Got a mention", string(j)) 911 | fmt.Println("\n😻 Got a mention", p.Body) 912 | if !p.New { 913 | fmt.Println("Not a new mention") 914 | return nil 915 | } 916 | compare := getBodyToCompare(p.Body) 917 | if substringInSlice(compare, r.config.AntiTriggers) { 918 | fmt.Println("Got an anti-trigger", p.Body) 919 | return nil 920 | } 921 | 922 | if strings.Contains(compare, "bad bot") || strings.Contains(compare, "damn bot") || 923 | strings.Contains(compare, "stupid bot") || strings.ToLower(p.Body) == "wrong" { 924 | myRepliesMu.Lock() 925 | comment, exists := myReplies[p.ParentID] 926 | myRepliesMu.Unlock() 927 | if exists && !comment.summoned { 928 | var err2 error 929 | target := mira.RedditOauth + "/api/del" 930 | _, err := r.r2.MiraRequest("POST", target, map[string]string{ 931 | "id": comment.commentID, 932 | "api_type": "json", 933 | }) 934 | 935 | if comment.additionalCommentID != "" { 936 | _, err2 = r.r2.MiraRequest("POST", target, map[string]string{ 937 | "id": comment.additionalCommentID, 938 | "api_type": "json", 939 | }) 940 | } 941 | if !capture(err) && !capture(err2) { 942 | myRepliesMu.Lock() 943 | delete(myReplies, p.ParentID) 944 | myRepliesMu.Unlock() 945 | } 946 | fmt.Println("got a bad bot comment", "https://reddit.com//comments/"+p.ParentID+"/"+p.ID) 947 | // capture(r.r.ReadMessage(p.Name)) 948 | } 949 | } 950 | return nil 951 | } 952 | func replaceSlice(s, new string, oldStrings ...string) string { 953 | for _, old := range oldStrings { 954 | s = strings.ReplaceAll(s, old, new) 955 | } 956 | return s 957 | } 958 | func getBodyToCompare(body string) string { 959 | return "\n" + strings.ReplaceAll(strings.ToLower(replaceSlice(body, "", "'", "’", "`")), "what is", "whats") + "?" 960 | } 961 | 962 | func distance(s, sub1, sub2 string) (int, bool) { 963 | i1 := strings.Index(s, sub1) 964 | i2 := strings.Index(s, sub2) 965 | if i1 == -1 || i2 == -1 { 966 | return 0, false 967 | } 968 | return i2 - i1, true 969 | } 970 | 971 | func minDistance(s, sub1 string, sub2 ...string) int { 972 | if !strings.Contains(s, sub1) { 973 | return 0 974 | } 975 | 976 | min := math.MaxInt16 977 | for i := range sub2 { 978 | d, e := distance(s, sub1, sub2[i]) 979 | if e && d < min && d > 0 { 980 | min = d 981 | } 982 | } 983 | if min == math.MaxInt16 { 984 | min = 0 985 | } 986 | return min 987 | } 988 | 989 | func (r *auddBot) Comment(p *models.Comment) { 990 | //fmt.Print("c") // why? to test the amount of new comments on Reddit! 991 | atomic.AddInt64(&commentsCounter, 1) 992 | //return nil 993 | compare := getBodyToCompare(p.Body) 994 | trigger := substringInSlice(compare, r.config.Triggers) 995 | if strings.Contains(compare, "bad bot") || strings.Contains(compare, "damn bot") || 996 | strings.Contains(compare, "stupid bot") { 997 | myRepliesMu.Lock() 998 | comment, exists := myReplies[string(p.ParentID)] 999 | myRepliesMu.Unlock() 1000 | if exists && !comment.summoned { 1001 | target := mira.RedditOauth + "/api/del" 1002 | _, err := r.r2.MiraRequest("POST", target, map[string]string{ 1003 | "id": comment.commentID, 1004 | "api_type": "json", 1005 | }) 1006 | capture(err) 1007 | if comment.additionalCommentID != "" { 1008 | _, err = r.r2.MiraRequest("POST", target, map[string]string{ 1009 | "id": comment.additionalCommentID, 1010 | "api_type": "json", 1011 | }) 1012 | } 1013 | capture(err) 1014 | fmt.Println("got a bad bot comment", "https://reddit.com"+p.Permalink) 1015 | return 1016 | } 1017 | } 1018 | if !trigger { 1019 | d := minDistance(compare, "what", "song", "music", "track") 1020 | if len(compare) < 100 && d != 0 && d < 10 { 1021 | fmt.Println("Skipping comment:", compare, "https://reddit.com"+p.Permalink) 1022 | } 1023 | return 1024 | } 1025 | myRepliesMu.Lock() 1026 | _, exists := myReplies[string(p.ParentID)] 1027 | myRepliesMu.Unlock() 1028 | if exists { 1029 | fmt.Println("Ignoring a comment that's a reply to ours:", "https://reddit.com"+p.Permalink) 1030 | return 1031 | } 1032 | if stringInSlice([]string{"auddbot", "RecognizeSong"}, p.Author) { 1033 | fmt.Println("Ignoring a comment from itself", p.Body) 1034 | return 1035 | } 1036 | if stringInSlice(r.config.IgnoreSubreddits, p.Subreddit) { 1037 | fmt.Println("Ignoring a comment from", p.Subreddit, "https://reddit.com"+p.Permalink) 1038 | return 1039 | } 1040 | if stringInSlice(r.config.SubredditsBannedOn, p.Subreddit) && 1041 | !strings.Contains(compare, "u/recognizesong") && strings.Contains(compare, "u/auddbot") { 1042 | avoidDuplicatesMu.Lock() 1043 | if _, exists := avoidDuplicates[string(p.ParentID)]; exists { 1044 | avoidDuplicatesMu.Unlock() 1045 | return 1046 | } 1047 | c := make(chan d, 1) 1048 | avoidDuplicates[string(p.ParentID)] = c 1049 | avoidDuplicatesMu.Unlock() 1050 | r.r2.Redditor(p.Author).Compose("Sorry, I'm banned on r/"+p.Subreddit, getBannedText(p.Subreddit)) 1051 | fmt.Println("Ignoring a mention from", p.Subreddit, "https://reddit.com"+p.Name) 1052 | return 1053 | } 1054 | if substringInSlice(compare, r.config.AntiTriggers) { 1055 | fmt.Println("Got an anti-trigger", p.Body, "https://reddit.com"+p.Permalink) 1056 | return 1057 | } 1058 | //j, _ := json.Marshal(p) 1059 | // fmt.Println("\n😻 Got a comment", "https://reddit.com"+p.Permalink, p.Body) 1060 | r.HandleQuery(nil, p, nil) 1061 | return 1062 | } 1063 | 1064 | func (r *auddBot) Post(p *models.Post) { 1065 | atomic.AddInt64(&postsCounter, 1) 1066 | compare := getBodyToCompare(p.Selftext) 1067 | trigger := substringInSlice(compare, r.config.Triggers) || 1068 | substringInSlice(strings.ToLower(p.Title), r.config.Triggers) 1069 | if !trigger { 1070 | return 1071 | } 1072 | if stringInSlice(r.config.IgnoreSubreddits, p.Subreddit) { 1073 | fmt.Println("Ignoring a post from", p.Subreddit, "https://reddit.com"+p.Permalink) 1074 | return 1075 | } 1076 | if stringInSlice(r.config.SubredditsBannedOn, p.Subreddit) && 1077 | !strings.Contains(compare, "u/recognizesong") && strings.Contains(compare, "u/auddbot") { 1078 | avoidDuplicatesMu.Lock() 1079 | if _, exists := avoidDuplicates[p.ID]; exists { 1080 | avoidDuplicatesMu.Unlock() 1081 | return 1082 | } 1083 | c := make(chan d, 1) 1084 | avoidDuplicates[p.ID] = c 1085 | avoidDuplicatesMu.Unlock() 1086 | r.r2.Redditor(p.Author).Compose("Sorry, I'm banned on r/"+p.Subreddit, getBannedText(p.Subreddit)) 1087 | fmt.Println("Ignoring a mention from", p.Subreddit, "https://reddit.com"+p.Name) 1088 | return 1089 | } 1090 | j, _ := json.Marshal(p) 1091 | fmt.Println("\n😻 Got a post", "https://reddit.com"+p.Permalink, string(j)) 1092 | r.HandleQuery(nil, nil, p) 1093 | return 1094 | } 1095 | 1096 | type auddBot struct { 1097 | config BotConfig 1098 | bot reddit1.Bot 1099 | bot2 reddit1.Bot 1100 | audd *audd.Client 1101 | r *mira.Reddit 1102 | r2 *mira.Reddit 1103 | } 1104 | 1105 | func (c *BotConfig) getReddit2Credentials(username string) *mira.Reddit { 1106 | r := mira.Init(mira.Credentials{ 1107 | ClientID: c.ClientID, 1108 | ClientSecret: c.ClientSecret, 1109 | Username: username, 1110 | Password: c.BotPasswords[username], 1111 | UserAgent: c.UserAgent, 1112 | }) 1113 | err := r.LoginAuth() 1114 | if err != nil { 1115 | return nil 1116 | } 1117 | if capture(err) { 1118 | return nil 1119 | } 1120 | rMeObj, err := r.Me().Info() 1121 | if err != nil { 1122 | return nil 1123 | } 1124 | rMe, _ := rMeObj.(*models.Me) 1125 | fmt.Printf("You are now logged in, /u/%s\n", rMe.Name) 1126 | return r 1127 | } 1128 | func (c *BotConfig) getReddit1Credentials(username string) reddit1.Bot { 1129 | bot, err := reddit1.NewBot( 1130 | reddit1.BotConfig{ 1131 | Agent: c.UserAgent, 1132 | App: reddit1.App{ 1133 | ID: c.ClientID, 1134 | Secret: c.ClientSecret, 1135 | Username: username, 1136 | Password: c.BotPasswords[username], 1137 | }, 1138 | Rate: time.Second * 10, 1139 | }, 1140 | ) 1141 | 1142 | if capture(err) { 1143 | return nil 1144 | } 1145 | return bot 1146 | } 1147 | 1148 | func loadConfig(file string) (*BotConfig, error) { 1149 | var cfg BotConfig 1150 | f, err := os.Open(file) 1151 | if err != nil { 1152 | return nil, err 1153 | } 1154 | j, err := ioutil.ReadAll(f) 1155 | if err != nil { 1156 | return nil, err 1157 | } 1158 | err = json.Unmarshal(j, &cfg) 1159 | if err != nil { 1160 | return nil, err 1161 | } 1162 | if stringInSlice(cfg.Triggers, "") { 1163 | return nil, fmt.Errorf("got a config with an empty string in the triggers") 1164 | } 1165 | if stringInSlice(cfg.AntiTriggers, "") { 1166 | return nil, fmt.Errorf("got a config with an empty string in the anti-triggers") 1167 | } 1168 | return &cfg, nil 1169 | } 1170 | 1171 | func WatchChanges(filename string, updated chan struct{}, l *rate.Limiter) { 1172 | watcher, err := fsnotify.NewWatcher() 1173 | capture(err) 1174 | defer captureFunc(watcher.Close) 1175 | done := make(chan bool) 1176 | go func() { 1177 | for { 1178 | select { 1179 | case event := <-watcher.Events: 1180 | fmt.Println(event) 1181 | if l != nil { 1182 | if l.Allow() { 1183 | updated <- struct{}{} 1184 | } else { 1185 | fmt.Println("skipping") 1186 | } 1187 | } 1188 | case err := <-watcher.Errors: 1189 | capture(err) 1190 | } 1191 | } 1192 | }() 1193 | if err := watcher.Add(filename); err != nil { 1194 | capture(err) 1195 | } 1196 | <-done 1197 | } 1198 | 1199 | func main() { 1200 | cfg, err := loadConfig(configFile) 1201 | if err != nil { 1202 | panic(err) 1203 | } 1204 | if err := raven.SetDSN(cfg.RavenDSN); err != nil { 1205 | panic(err) 1206 | } 1207 | reloadLimiter := rate.NewLimiter(1, 1) 1208 | configUpdated := make(chan struct{}, 1) 1209 | go WatchChanges(configFile, configUpdated, reloadLimiter) 1210 | for { 1211 | stop := make(chan struct{}, 2) 1212 | cfg, err := loadConfig(configFile) 1213 | if err != nil { 1214 | capture(err) 1215 | time.Sleep(15 * time.Second) 1216 | continue 1217 | } 1218 | var grawStopChan = make(chan func(), 2) 1219 | go func() { 1220 | select { 1221 | case <-configUpdated: 1222 | fmt.Println("Waiting 2 seconds, reloading config and restarting") 1223 | time.Sleep(time.Second * 2) 1224 | stop <- struct{}{} 1225 | select { 1226 | case grawStop := <-grawStopChan: 1227 | grawStop() 1228 | default: 1229 | } 1230 | select { 1231 | case grawStop := <-grawStopChan: 1232 | grawStop() 1233 | default: 1234 | } 1235 | return 1236 | case <-stop: 1237 | stop <- struct{}{} 1238 | return 1239 | } 1240 | }() 1241 | handler := &auddBot{ 1242 | config: *cfg, 1243 | bot: cfg.getReddit1Credentials(AudDBotUsername), 1244 | bot2: cfg.getReddit1Credentials(RecognizeSongBotUsername), 1245 | audd: audd.NewClient(cfg.AudDToken), 1246 | r: cfg.getReddit2Credentials(RecognizeSongBotUsername), 1247 | r2: cfg.getReddit2Credentials(AudDBotUsername), 1248 | } 1249 | if handler.bot == nil || handler.bot2 == nil || handler.r == nil || handler.r2 == nil { 1250 | time.Sleep(time.Second * 10) 1251 | continue 1252 | } 1253 | go func() { 1254 | t := time.NewTicker(time.Minute) 1255 | for { 1256 | select { 1257 | case <-stop: 1258 | stop <- struct{}{} 1259 | return 1260 | case <-t.C: 1261 | } 1262 | newComments, newPosts := atomic.LoadInt64(&commentsCounter), atomic.LoadInt64(&postsCounter) 1263 | fmt.Println("Comments:", newComments, "posts:", newPosts) 1264 | atomic.AddInt64(&commentsCounter, -1*newComments) 1265 | atomic.AddInt64(&postsCounter, -1*newPosts) 1266 | if newComments == 0 { 1267 | select { 1268 | case grawStop := <-grawStopChan: 1269 | grawStop() 1270 | default: 1271 | } 1272 | select { 1273 | case grawStop := <-grawStopChan: 1274 | grawStop() 1275 | default: 1276 | } 1277 | return 1278 | } 1279 | } 1280 | }() 1281 | handler.audd.SetEndpoint(audd.EnterpriseAPIEndpoint) // See https://docs.audd.io/enterprise 1282 | /*_, err := handler.r.ListUnreadMessages() 1283 | if capture(err) { 1284 | capture(err) 1285 | time.Sleep(15 * time.Second) 1286 | continue 1287 | } 1288 | for i := range m { 1289 | //go handler.Comment(m[i]) 1290 | fmt.Println(m[i].Permalink) 1291 | capture(handler.r.ReadMessage(m[i].ID)) 1292 | }*/ 1293 | grawCfg := graw.Config{Mentions: true} 1294 | grawStop, wait, err := graw.Run(handler, handler.bot, grawCfg) 1295 | if capture(err) { 1296 | capture(err) 1297 | time.Sleep(15 * time.Second) 1298 | continue 1299 | } 1300 | grawStop2, wait2, err := graw.Run(handler, handler.bot2, grawCfg) 1301 | if capture(err) { 1302 | capture(err) 1303 | time.Sleep(15 * time.Second) 1304 | continue 1305 | } 1306 | grawStopChan <- grawStop 1307 | grawStopChan <- grawStop2 1308 | 1309 | postsStream, err := streamSubredditPosts(handler.r, "all") 1310 | if err != nil { 1311 | capture(err) 1312 | time.Sleep(15 * time.Second) 1313 | continue 1314 | } 1315 | commentsStream, err := streamSubredditComments(handler.r2, "all") 1316 | if err != nil { 1317 | capture(err) 1318 | time.Sleep(15 * time.Second) 1319 | continue 1320 | } 1321 | go func() { 1322 | var s models.Submission 1323 | var p *models.Post 1324 | for s = range postsStream.C { 1325 | if s == nil { 1326 | fmt.Println("Stream was closed") 1327 | return 1328 | } 1329 | //go atomic.AddUint64(&postsCounter, 1) 1330 | p = s.(*models.Post) 1331 | go handler.Post(p) 1332 | } 1333 | }() 1334 | go func() { 1335 | var s models.Submission 1336 | for s = range commentsStream.C { 1337 | var c *models.Comment 1338 | if s == nil { 1339 | fmt.Println("Stream was closed") 1340 | return 1341 | } 1342 | //go atomic.AddUint64(&commentsCounter, 1) 1343 | c = s.(*models.Comment) 1344 | go handler.Comment(c) 1345 | } 1346 | }() 1347 | fmt.Println("started") 1348 | fmt.Println("graw run failed: ", wait(), wait2()) 1349 | go func() { 1350 | commentsStream.Close <- struct{}{} 1351 | postsStream.Close <- struct{}{} 1352 | stop <- struct{}{} 1353 | }() 1354 | } 1355 | 1356 | } 1357 | 1358 | func streamSubredditPosts(c *mira.Reddit, name string) (*mira.SubmissionStream, error) { 1359 | sendC := make(chan models.Submission, 100) 1360 | s := &mira.SubmissionStream{ 1361 | C: sendC, 1362 | Close: make(chan struct{}), 1363 | } 1364 | _, err := c.Subreddit(name).Posts("new", "all", 1) 1365 | if err != nil { 1366 | return nil, err 1367 | } 1368 | var last models.RedditID 1369 | go func() { 1370 | sent := ring.New(100) 1371 | for { 1372 | select { 1373 | case <-s.Close: 1374 | close(sendC) 1375 | return 1376 | default: 1377 | } 1378 | posts, err := c.Subreddit(name).PostsAfter(last, 100) 1379 | if err != nil { 1380 | close(sendC) 1381 | return 1382 | } 1383 | if len(posts) > 95 { 1384 | //fmt.Printf("%d new posts | ", len(posts)) 1385 | } 1386 | for i := len(posts) - 1; i >= 0; i-- { 1387 | if ringContains(sent, posts[i].GetID()) { 1388 | continue 1389 | } 1390 | sendC <- posts[i] 1391 | sent.Value = posts[i].GetID() 1392 | sent = sent.Next() 1393 | } 1394 | if len(posts) == 0 { 1395 | last = "" 1396 | } else if len(posts) > 2 { 1397 | last = posts[1].GetID() 1398 | } 1399 | time.Sleep(13 * time.Second) 1400 | } 1401 | }() 1402 | return s, nil 1403 | } 1404 | 1405 | func streamSubredditComments(c *mira.Reddit, name string) (*mira.SubmissionStream, error) { 1406 | sendC := make(chan models.Submission, 100) 1407 | s := &mira.SubmissionStream{ 1408 | C: sendC, 1409 | Close: make(chan struct{}), 1410 | } 1411 | _, err := c.Subreddit(name).Posts("new", "all", 1) 1412 | if err != nil { 1413 | return nil, err 1414 | } 1415 | var last models.RedditID 1416 | go func() { 1417 | sent := ring.New(100) 1418 | T := time.NewTicker(time.Millisecond * 1500) 1419 | for { 1420 | select { 1421 | case <-s.Close: 1422 | close(sendC) 1423 | return 1424 | default: 1425 | } 1426 | comments, err := c.Subreddit(name).CommentsAfter("new", last, 100) 1427 | if err != nil { 1428 | close(sendC) 1429 | return 1430 | } 1431 | if len(comments) > 95 { 1432 | //fmt.Printf("%d new comments | ", len(comments)) 1433 | } 1434 | for i := len(comments) - 1; i >= 0; i-- { 1435 | if ringContains(sent, comments[i].GetID()) { 1436 | continue 1437 | } 1438 | //fmt.Print("a") 1439 | sendC <- comments[i] 1440 | sent.Value = comments[i].GetID() 1441 | sent = sent.Next() 1442 | } 1443 | if len(comments) == 0 { 1444 | last = "" 1445 | } else if len(comments) > 2 { 1446 | last = comments[1].GetID() 1447 | } 1448 | <-T.C 1449 | } 1450 | }() 1451 | return s, nil 1452 | } 1453 | 1454 | func ringContains(r *ring.Ring, n models.RedditID) bool { 1455 | ret := false 1456 | r.Do(func(p interface{}) { 1457 | if p == nil { 1458 | return 1459 | } 1460 | i := p.(models.RedditID) 1461 | if i == n { 1462 | ret = true 1463 | } 1464 | }) 1465 | return ret 1466 | } 1467 | 1468 | func capture(err error) bool { 1469 | if err == nil { 1470 | return false 1471 | } 1472 | _, file, no, ok := runtime.Caller(1) 1473 | if ok { 1474 | err = fmt.Errorf("%v from %s#%d", err, file, no) 1475 | } 1476 | packet := raven.NewPacket(err.Error(), raven.NewException(err, raven.GetOrNewStacktrace(err, 1, 3, nil))) 1477 | go raven.Capture(packet, nil) 1478 | fmt.Println(err.Error()) 1479 | return true 1480 | } 1481 | 1482 | func captureFunc(f func() error) (r bool) { 1483 | err := f() 1484 | if r = err != nil; r { 1485 | _, file, no, ok := runtime.Caller(1) 1486 | if ok { 1487 | err = fmt.Errorf("%v from %s#%d", err, file, no) 1488 | } 1489 | go raven.CaptureError(err, nil) 1490 | } 1491 | return 1492 | } 1493 | 1494 | func commentToMessage(comment *models.Comment) *reddit1.Message { 1495 | if comment == nil { 1496 | return nil 1497 | } 1498 | return &reddit1.Message{ 1499 | ID: comment.ID, 1500 | Name: string(comment.GetID()), 1501 | CreatedUTC: uint64(comment.CreatedUTC), 1502 | Author: comment.Author, 1503 | Body: comment.Body, 1504 | BodyHTML: comment.BodyHTML, 1505 | Context: comment.Permalink, 1506 | ParentID: string(comment.ParentID), 1507 | Subreddit: comment.Subreddit, 1508 | WasComment: true, 1509 | } 1510 | } 1511 | 1512 | type RedditPageJSON []struct { 1513 | Data struct { 1514 | Children []struct { 1515 | Data struct { 1516 | MediaMetadata map[string]struct { 1517 | Status string `json:"status"` 1518 | E string `json:"e"` 1519 | DashURL string `json:"dashUrl"` 1520 | X int `json:"x"` 1521 | Y int `json:"y"` 1522 | HlsURL string `json:"hlsUrl"` 1523 | ID string `json:"id"` 1524 | IsGif bool `json:"isGif"` 1525 | } `json:"media_metadata"` 1526 | URL string `json:"url"` 1527 | RpanVideo struct { 1528 | HlsURL string `json:"hls_url"` 1529 | ScrubberMediaURL string `json:"scrubber_media_url"` 1530 | } `json:"rpan_video"` 1531 | } `json:"data"` 1532 | } `json:"children"` 1533 | After interface{} `json:"after"` 1534 | Before interface{} `json:"before"` 1535 | } `json:"data"` 1536 | } 1537 | 1538 | type RedditStreamJSON struct { 1539 | Data struct { 1540 | Stream struct { 1541 | StreamID string `json:"stream_id"` 1542 | HlsURL string `json:"hls_url"` 1543 | State string `json:"state"` 1544 | } `json:"stream"` 1545 | } `json:"data"` 1546 | } 1547 | --------------------------------------------------------------------------------