import discord from discord.ext import commands import numpy as np import random import requests import asyncio import os 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 async 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": await message.channel.send("kankerlauw") return if message.content.lower() == "lol": await message.channel.send("laffing aut laut") return if self.bot.user.mentioned_in(message) and "@everyone" not in message.content: MENTIONED_RESPONSES = [ "Hoi", "hmm?", "JA WAT?", "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.", self.getResponseForMessage("stroopwafel") ] try: MENTIONED_RESPONSES.append("Wat mot je, " + random.choice(self.bot.get_cog("Shitpost").insult) + "?") except: print("ERROR! - Could not import insults") await message.channel.send(random.choice(MENTIONED_RESPONSES)) return if re.match("^[0-9]+([,.]?[0-9]+)*?$", message.content.strip()) and random.randint(0,9) == 0: await 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() await self.learnFromMessagesInChannel(message.channel, 1) await self.respondToMessage(message) else: a = random.randint(0,999) # probablity if a < 40: await self.learnFromMessagesInChannel(message.channel, 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 == os.getenv('STROOP_CHANNEL_SHITPOST') and a < 40: #shitpost channel await self.respondToMessage(message) elif a < 10: #all other channels await self.respondToMessage(message) #process commands: await 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) async 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 """ channel=self.bot.get_channel(channelId) await self.learnFromMessagesInChannel(channel, nrOfMessages) await ctx.channel.send("Ik heb weer wat nieuwe woordjes geleerd. :slight_smile:") #Learns from an x number of messages from a certain channel async def learnFromMessagesInChannel(self, channel, nrOfMessages): #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 = await anext(messageIterator) 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) async 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 = await anext(messageIterator) #this will be the !reageer command. skip messageToRespondTo = await anext(messageIterator) if messageToRespondTo.content.strip()[0] == '!' or messageToRespondTo.author.bot: #another chance messageToRespondTo = await anext(messageIterator) #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: await ctx.message.delete() except: pass response = await self.getResponseForMessage(wordsToRespondTo) await ctx.channel.send(response) # Calculate probabilities and pick a next word from a word pair dict: # https://stackoverflow.com/questions/835092/python-dictionary-are-keys-and-values-always-the-same-order def pickNextWord(self, pairPossibilities): possible_words = list(pairPossibilities.keys()) weights = np.array(list(pairPossibilities.values())) probs = weights / weights.sum() return np.random.choice(possible_words, p=probs) #Respond to a message. #Assumes the message isn't a command async def respondToMessage(self, message): wordsToRespondTo = message.content.strip().split() response = await self.getResponseForMessage(wordsToRespondTo) await message.channel.send(response) #Gets a response async def getResponseForMessage(self, wordsToRespondTo): if random.random() > 0.9: try: # GPT request moet echt even in een non-blocking executortje, anders loopt de hele boel vast return await self.bot.loop.run_in_executor(None,self.getResponseForMessageOnline, wordsToRespondTo) except: pass else: return self.getResponseForMessageLocal(wordsToRespondTo) def translateTo(self, language, text): req = requests.post('http://gpt.hoekveen.net/translate', json={"text": text, "source_lang": "auto", "target_lang": language}) print(req.json()) return req.json()["translations"][0]["text"] def getResponseForMessageOnline(self, wordsToRespondTo): text = self.translateTo("en", " ".join(wordsToRespondTo)) req = requests.post('http://gpt.hoekveen.net/complete', json={"prompt": text, "max_tokens": 60}) print(req.json()) respons = req.json()["text"] respons = respons[0:respons.rfind(".")+1] # Alles weg na de laatste punt return self.translateTo("nl", respons) def getResponseForMessageLocal(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] #Pick a next word: chain.append(self.pickNextWord(pairPossibilities)) 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)) #await 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 #Pick a next word: chain.append(self.pickNextWord(pairPossibilities)) 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) async def haiku(self, ctx): """Stroop is een poëet. Laat hem een Haiku maken. """ response = self.getHaiku() await ctx.channel.send(response) @commands.command(pass_context=True, hidden=False) async 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: await 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] await ctx.channel.send(message) except: pass