import asyncio import discord from discord.ext import commands import numpy as np import random from datetime import datetime import re class Chatbot(commands.Cog): """Een heuse chatbot. Under construction...""" def __init__(self, bot): self.bot = bot self.voice_states = {} self.END_OF_MESSAGE = "" self.START_OF_MESSAGE = "" self.pmLast = datetime(2018, 1, 1, 1, 1, 1) self.pmCount = 0 try: np.load('markov_dict.npy', allow_pickle=True) except FileNotFoundError: #If there's no initial file, make a new one: initFile = {'pairs':{self.START_OF_MESSAGE:{'Hoi':1},'Hoi':{self.END_OF_MESSAGE:1}}, 'trairs':{}, 'responsePairs':{}} np.save("markov_dict.npy", initFile) except: print("ERROR! - Could not open or create Markov chat file") #Do stuff when a message comes in: @self.bot.event @asyncio.coroutine def on_message(message): if message.author.bot: return #ignore your own messages, Stroop if message.content.strip() == "" or message.content.strip()[0] != '!': #don't respond to a command if message.content.lower() == "nihao": yield from message.channel.send("kankerlauw") return if message.content.lower() == "lol": yield from message.channel.send("laffing aut laut") return if self.bot.user.mentioned_in(message): yield from message.channel.send("Godve...Godver-fucking-domme! Wie de fuck heeft me zojuist gepingt? Wie de fuck heeft de ballen om mij te pingen?! Als ik jou vind hè, ik ram je helemaal de tyfus in. Ik ga je kapot maken. Welke breincel in jouw hoofd dacht dat het grappig was om mij te pingen? Noem jij dat 'grappig'? Ik zal je godverdomme iets laten zien wat grappig is, ja. Ik neuk je moeder zo hard dat ze van me gaat houden, godverdomme. Dat krijg je er van als je me pingt, vuile teringleijer. Jouw soort mag wat mij betreft uitsterven. Weet je, ik ga je helemaal geen aandacht meer geven. Je mag de tering krijgen, en daar blijft het bij. Kutjong.") return if re.match("^[0-9]+([,.]?[0-9]+)*?$", message.content.strip()) and random.randint(0,9) == 0: yield from message.channel.send("That's Numberwang!") # https://www.youtube.com/watch?v=0obMRztklqU return if "DMChannel" in type(message.channel).__name__: #respond to messages in PM channels if (self.isAllowedToLearnFromPm(message)): #avoid spam-learning self.pmCount += 1 self.pmLast = datetime.now() yield from self.learnFromMessagesInChannel(message.channel.id, 1) yield from self.respondToMessage(message) elif message.channel.id == self.bot.hdChannels["belangrijk"]: #don't abuse the "belangrijk" channel pass else: a = random.randint(0,999) # probablity if a < 40: yield from self.learnFromMessagesInChannel(message.channel.id, 10) if "stroop" in message.content.lower(): a = a / 20 # significantly increase the chance of responding if somebody talks about him. if message.channel.id == self.bot.hdChannels["shitpost"] and a < 40: #shitpost channel yield from self.respondToMessage(message) elif a < 10: #all other channels yield from self.respondToMessage(message) #process commands: yield from self.bot.process_commands(message) def __check(self, ctx): return True #The "Guus filter" #Don't allow to learn from very large personal messages #Only learn from 10 personal messages per hour at most def isAllowedToLearnFromPm(self, message): if len(message.content.strip()) > 150: return False diff = datetime.now() - self.pmLast if diff.days > 0 or diff.seconds > 3600: self.pmCount = 0 return True if self.pmCount > 10: return False return True #Make word pairs: def makePairs(self, corpus): for i in range(len(corpus)-1): yield (corpus[i], corpus[i+1]) #Make word trairs. Yes, I completely made up "trair" #It's like pairs, but with three, but not really, because there are two. OK? #[1 0 1] def makeTrairs(self, corpus): for i in range(1, len(corpus)-1): yield (corpus[i-1], corpus[i+1]) #Learn from a human message #Returns an updated dict def learnFrom(self, wordDictPairs, wordDictTrairs, message: str): newPairDict = wordDictPairs newTrairDict = wordDictTrairs #If the message is only a tag, don't learn from it. return if message.count(' ') == 0 and message[0:2] == "<@": return newPairDict, newTrairDict; message = self.START_OF_MESSAGE + " " + message + " " + self.END_OF_MESSAGE corpus = message.split() for n, word in enumerate(corpus): if word == "@everyone": corpus[n] = "@iedereen" pairs = self.makePairs(corpus) trairs = self.makeTrairs(corpus) #Add wordcounts to pair dict for word_1, word_2 in pairs: if word_1 in newPairDict.keys(): if word_2 in newPairDict[word_1].keys(): newPairDict[word_1][word_2] += 1 else: newPairDict[word_1][word_2] = 1 else: newPairDict[word_1] = {word_2 : 1} #Add wordcounts to trair dict for word_1, word_3 in trairs: if word_1 in newTrairDict.keys(): if word_3 in newTrairDict[word_1].keys(): newTrairDict[word_1][word_3] += 1 else: newTrairDict[word_1][word_3] = 1 else: newTrairDict[word_1] = {word_3 : 1} return newPairDict, newTrairDict; #Learn to respond to messages #Returns a new dict with response pairs def learnResponse(self, wordDictResp, message1: str, message2: str): STOP_WORDS = ["aan", "achter", "al", "ben", "dan", "dat", "de", "die", "dit", "een", "en", "er", "gehad", "had", "heb", "hen", "het", "hun", "in", "is", "kon", "met", "na", "of", "om", "ook", "op", "tot", "van", "was", "wat", "zo", "a", "an", "the", "am", "was", "were", "be", "not", "at"] newRespPairDict = wordDictResp #Split messages into words and remove stopwords: message1List = [] message2List = [] for word in message1.split(): if word not in STOP_WORDS: message1List.append(word) for word in message2.split(): if word not in STOP_WORDS: message2List.append(word) #link words of response to words of initial message for word_1 in message1List: for word_2 in message2List: if word_1 in newRespPairDict.keys(): if word_2 in newRespPairDict[word_1].keys(): newRespPairDict[word_1][word_2] += 1 else: newRespPairDict[word_1][word_2] = 1 else: newRespPairDict[word_1] = {word_2 : 1} return newRespPairDict @commands.command(pass_context=True, hidden=True) @asyncio.coroutine def trainmarkovchat(self, ctx, channelId=409751773595566081, nrOfMessages=50): """Train that chain, jermaine param channelId: use this channel to train param nrOfMessages: use the x most recent messages """ yield from self.learnFromMessagesInChannel(channelId, nrOfMessages) yield from ctx.channel.send("Ik heb weer wat nieuwe woordjes geleerd. :slight_smile:") #Learns from an x number of messages from a certain channel @asyncio.coroutine def learnFromMessagesInChannel(self, channelId, nrOfMessages): channel=self.bot.get_channel(channelId) #Open current chain: wordDict = np.load('markov_dict.npy', allow_pickle=True).item() #Iterate through the last x messages from the channel: messageIterator = channel.history(limit=nrOfMessages, before=None, after=None, oldest_first=False, around=None) allMessages = [] for i in range(nrOfMessages): msg = yield from messageIterator.next() allMessages.append(msg) i = 0 nextMessageIndex = -10 #Note: the loop goes from new to old for msg in allMessages: messageContent = msg.content.replace('(', '').replace(')', '').replace('"', '') messageContent = messageContent.strip() if messageContent == "": pass # Bericht is leeg. Negeer. elif msg.author.bot: pass # Dit is een bot chat. Negeer. elif messageContent[0] == "!": pass # Dit is een commando. Negeer. elif "```" in messageContent: pass # Daar zit een codeblok in. Negeer. elif messageContent[0:7] == "http://" or messageContent[0:8] == "https://": pass # Een link. Negeer. TODO: post af en toe een random link die geen repost is else: # Usable message! wordDict['pairs'], wordDict['trairs'] = self.learnFrom(wordDict['pairs'], wordDict['trairs'], messageContent) if (i-nextMessageIndex) == 1 and msg.author != nextMessage.author: #the previous usable messages was the one right after this one and was not from the same author timeDelta = nextMessage.created_at - msg.created_at #TODO check for different author if timeDelta.days == 0 and timeDelta.seconds < 3600: #less than an hour difference #this is counted as a response. learn from it wordDict['responsePairs'] = self.learnResponse(wordDict['responsePairs'], messageContent, nextMessageContent) #save for next iteration: nextMessage = msg nextMessageContent = messageContent nextMessageIndex = i i += 1 np.save("markov_dict.npy", wordDict) @commands.command(pass_context=True, hidden=False) @asyncio.coroutine def reageer(self, ctx): """Laat Stroop ergens op reageren. Stroop leert van onze gesprekken. Hij kan zich nu voordoen als een van ons. Soort van. """ #Get previous message to respond to messageIterator = ctx.channel.history(limit=3, before=None, after=None, oldest_first=False, around=None) messageToRespondTo = yield from messageIterator.next() #this will be the !reageer command. skip messageToRespondTo = yield from messageIterator.next() if messageToRespondTo.content.strip()[0] == '!' or messageToRespondTo.author.bot: #another chance messageToRespondTo = yield from messageIterator.next() #but it's not a huge problem if he responds to himself if messageToRespondTo.content.strip()[0] == '!': wordsToRespondTo = [] else: wordsToRespondTo = messageToRespondTo.content.strip().split() #print("respond to: ", wordsToRespondTo) #Delete command try: yield from ctx.message.delete() except: pass response = self.getResponseForMessage(wordsToRespondTo) yield from ctx.channel.send(response) #Respond to a message. #Assumes the message isn't a command @asyncio.coroutine def respondToMessage(self, message): wordsToRespondTo = message.content.strip().split() response = self.getResponseForMessage(wordsToRespondTo) yield from message.channel.send(response) #Gets a response def getResponseForMessage(self, wordsToRespondTo): MAX_N_WORDS = 100 #Don't use more words than this #Get dictionary/Markov chain wordDict = np.load('markov_dict.npy', allow_pickle=True).item() wordPairDict = wordDict['pairs'] wordTrairDict = wordDict['trairs'] responsePairDict = wordDict['responsePairs'] #TODO store in global variable to minimise IO? #Start with chain = [self.START_OF_MESSAGE] #Go through the chain: for i in range(1, MAX_N_WORDS): if chain[-1] == self.END_OF_MESSAGE: break #Find the possibilities: pairPossibilities = wordPairDict[chain[-1]].copy() #Increase score if it would be a good response: for userWord in wordsToRespondTo: if userWord in responsePairDict: for goodResponseWord in responsePairDict[userWord]: if goodResponseWord in pairPossibilities.keys(): if i == 1: pairPossibilities[goodResponseWord] += responsePairDict[userWord][goodResponseWord] * 2 #first word? try to respond else: pairPossibilities[goodResponseWord] += responsePairDict[userWord][goodResponseWord] * 0.1 #Increase score for trairs: if i > 1: #we can only compare trairs if we have at least two words already if chain[-2] in wordTrairDict: trairPossibilities = wordTrairDict[chain[-2]].copy() #If a trair matches, increase the frequency of the pair for word2 in pairPossibilities.keys(): if word2 in trairPossibilities.keys(): pairPossibilities[word2] += trairPossibilities[word2] #Calculate probabilities and pick a next word: #https://stackoverflow.com/questions/835092/python-dictionary-are-keys-and-values-always-the-same-order possible_words = list(pairPossibilities.keys()) weights = np.array(list(pairPossibilities.values())) probs = weights / weights.sum() chain.append(np.random.choice(possible_words, p=probs)) if chain[0] == self.START_OF_MESSAGE: chain = chain[1:-1] #remove and else: chain = chain[:-1] #remove #remove 90% of tags: for n, word in enumerate(chain): if word[0:2] == "<@": a = random.randint(1,10) if a != 1: chain[n] = "@jemoeder" #print(' '.join(chain)) #yield from ctx.channel.send(' '.join(chain)) return ' '.join(chain) #Try to count the number of syllables in a word #Used to make Haikus def countSyllables(self, word: str): KNOWN_ACRONYMS = ["aub", "svp", "ivm", "nr", "zsm", "mbt", "tmi", "fyi", "afk", "omg", "idk", "coc"] word = word.lower().strip() #for acryonyms, just count the letters: if word in KNOWN_ACRONYMS or re.match("^([a-z]{1}[.]{1}){2,}$", word) or re.match("^([bcdfghjklmnpqrstvwxz]){2,}$", word): return len(word.replace(".", "")) #count vowels surrounded by consonants count = len(re.findall("[bcdfghjklmnpqrstvwxyz]{1}[aeiouyëäïöüéèáà]{1,2}[bcdfghjklmnpqrstvwxz]{0,1}|[aeiouyëäïöüéèáà]{1,2}[bcdfghjklmnpqrstvwxz]{1}", word)) count += len(re.findall("[ëäï]", word)) #ideeën etc return max(1, count) #Append a message change with a numer of syllables #If it returns False, it couldn't find anything #It's recursive, yo def appendMessageWithNrOfSyllables(self, nrOfSyl: int, chain, wordPairDict): #Find the possibilities: if chain[-1] == "\n": pairPossibilities = wordPairDict[chain[-2]].copy() else: pairPossibilities = wordPairDict[chain[-1]].copy() for poss in list(pairPossibilities.keys()): if self.countSyllables(poss) > nrOfSyl or poss == self.END_OF_MESSAGE: #TODO change to constant del pairPossibilities[poss] if len(pairPossibilities) == 0: return False #Calculate probabilities and pick a next word: possible_words = list(pairPossibilities.keys()) weights = np.array(list(pairPossibilities.values())) probs = weights / weights.sum() chain.append(np.random.choice(possible_words, p=probs)) #TODO this is duplicate code. reuse the stuff from getResponseForMessage() if self.countSyllables(chain[-1]) == nrOfSyl: return chain #RECURSION! return self.appendMessageWithNrOfSyllables( (nrOfSyl-self.countSyllables(chain[-1])) , chain, wordPairDict) #Make a nice Haiku def getHaiku(self): MAX_N_TRIES = 50 #Get dictionary/Markov chain wordDict = np.load('markov_dict.npy', allow_pickle=True).item() wordPairDict = wordDict['pairs'] #try: for i in range(1, MAX_N_TRIES): chain = [self.START_OF_MESSAGE] chain = self.appendMessageWithNrOfSyllables(5, chain, wordPairDict) if chain is not False: chain.append("\n") chain = self.appendMessageWithNrOfSyllables(7, chain, wordPairDict) if chain is not False: chain.append("\n") chain = self.appendMessageWithNrOfSyllables(5, chain, wordPairDict) if chain is not False: break chain = chain[1:] #remove return ' '.join(chain) @commands.command(pass_context=True, hidden=False) @asyncio.coroutine def haiku(self, ctx): """Stroop is een poëet. Laat hem een Haiku maken. """ response = self.getHaiku() yield from ctx.channel.send(response) @commands.command(pass_context=True, hidden=False) @asyncio.coroutine def uitspraak(self, ctx, numPosts=1): """Random uitspraak uit de hall of fame. Optioneel kan je een nummer meegeven voor meerdere uitspraken 0 voor alle uitspraken.""" try: yield from ctx.message.delete() except: pass try: message = "" with open("uitspraken.txt", "r") as file: uitspraken = file.readlines() random.shuffle(uitspraken) if numPosts == 0: numPosts = len(uitspraken) for i in range(numPosts): message += uitspraken[i] yield from ctx.channel.send(message) except: pass