1#! /usr/bin/env python3
+ 2# -*- coding: utf-8 -*-
+ 3
+ 4"""
+ 5Module for the common base class for all Bots
+ 6"""
+ 7
+ 8importre
+ 9
+10classBot():
+11"""Base class for things common between different protocols"""
+12def__init__(self):
+13self.CONFIG={}
+14self.ACTIONS=[]
+15self.GENERAL_ACTIONS=[]
+16
+17defgetConfig(self):
+18"""Return the current configuration"""
+19returnself.CONFIG
+20
+21defsetConfig(self,config):
+22"""Set the current configuration"""
+23self.CONFIG=config
+24
+25defregisterActions(self,actions):
+26"""Register actions to use"""
+27print("Adding actions:")
+28foractioninactions:
+29print(" - "+action.__name__)
+30self.ACTIONS.extend(actions)
+31
+32defregisterGeneralActions(self,actions):
+33"""Register general actions to use"""
+34print("Adding general actions:")
+35foractioninactions:
+36print(" - "+action.__name__)
+37self.GENERAL_ACTIONS.extend(actions)
+38
+39@staticmethod
+40deftokenize(message):
+41"""Split a message into normalized tokens"""
+42returnre.sub("[,.?:]"," ",message).strip().lower().split()
+
+
+
+
+
+
+
+
+ class
+ Bot:
+
+
+
+
+
+
11classBot():
+12"""Base class for things common between different protocols"""
+13def__init__(self):
+14self.CONFIG={}
+15self.ACTIONS=[]
+16self.GENERAL_ACTIONS=[]
+17
+18defgetConfig(self):
+19"""Return the current configuration"""
+20returnself.CONFIG
+21
+22defsetConfig(self,config):
+23"""Set the current configuration"""
+24self.CONFIG=config
+25
+26defregisterActions(self,actions):
+27"""Register actions to use"""
+28print("Adding actions:")
+29foractioninactions:
+30print(" - "+action.__name__)
+31self.ACTIONS.extend(actions)
+32
+33defregisterGeneralActions(self,actions):
+34"""Register general actions to use"""
+35print("Adding general actions:")
+36foractioninactions:
+37print(" - "+action.__name__)
+38self.GENERAL_ACTIONS.extend(actions)
+39
+40@staticmethod
+41deftokenize(message):
+42"""Split a message into normalized tokens"""
+43returnre.sub("[,.?:]"," ",message).strip().lower().split()
+
+
+
+
Base class for things common between different protocols
+
+
+
+
+
+ CONFIG
+
+
+
+
+
+
+
+
+
+
+ ACTIONS
+
+
+
+
+
+
+
+
+
+
+ GENERAL_ACTIONS
+
+
+
+
+
+
+
+
+
+
+
+
+ def
+ getConfig(self):
+
+
+
+
+
+
18defgetConfig(self):
+19"""Return the current configuration"""
+20returnself.CONFIG
+
+
+
+
Return the current configuration
+
+
+
+
+
+
+
+
+ def
+ setConfig(self, config):
+
+
+
+
+
+
22defsetConfig(self,config):
+23"""Set the current configuration"""
+24self.CONFIG=config
+
33defregisterGeneralActions(self,actions):
+34"""Register general actions to use"""
+35print("Adding general actions:")
+36foractioninactions:
+37print(" - "+action.__name__)
+38self.GENERAL_ACTIONS.extend(actions)
+
+
+
+
Register general actions to use
+
+
+
+
+
+
+
+
@staticmethod
+
+ def
+ tokenize(message):
+
+
+
+
+
+
40@staticmethod
+41deftokenize(message):
+42"""Split a message into normalized tokens"""
+43returnre.sub("[,.?:]"," ",message).strip().lower().split()
+
+
+
+
Split a message into normalized tokens
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/pdoc/discord_bot.html b/docs/pdoc/discord_bot.html
new file mode 100644
index 0000000..5a08113
--- /dev/null
+++ b/docs/pdoc/discord_bot.html
@@ -0,0 +1,548 @@
+
+
+
+
+
+
+ discord_bot API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+discord_bot
+
+
Module for the Discord bot.
+
+
Connecting, sending and receiving messages and doing custom actions.
+
+
+
+
+
+
+
1#! /usr/bin/env python3
+ 2# -*- coding: utf-8 -*-
+ 3
+ 4"""
+ 5Module for the Discord bot.
+ 6
+ 7Connecting, sending and receiving messages and doing custom actions.
+ 8"""
+ 9
+10importdiscord
+11
+12frombotimportBot
+13
+14classDiscordBot(discord.Client,Bot):
+15"""Bot implementing the discord protocol"""
+16def__init__(self):
+17Bot.__init__(self)
+18self.CONFIG={
+19"token":""
+20}
+21intents=discord.Intents.default()
+22intents.message_content=True
+23discord.Client.__init__(self,intents=intents)
+24
+25defbegin(self):
+26"""Start the bot"""
+27self.run(self.CONFIG.get("token"))
+28
+29asyncdefcheckMarvinActions(self,message):
+30"""Check if Marvin should perform any actions"""
+31words=self.tokenize(message.content)
+32ifself.user.name.lower()inwords:
+33foractioninself.ACTIONS:
+34response=action(words)
+35ifresponse:
+36awaitmessage.channel.send(response)
+37else:
+38foractioninself.GENERAL_ACTIONS:
+39response=action(words)
+40ifresponse:
+41awaitmessage.channel.send(response)
+42
+43asyncdefon_message(self,message):
+44"""Hook run on every message"""
+45print(f"#{message.channel.name} <{message.author}> {message.content}")
+46ifmessage.author.name==self.user.name:
+47# don't react to own messages
+48return
+49awaitself.checkMarvinActions(message)
+
+
+
+
+
+
+
+
+ class
+ DiscordBot(discord.client.Client, bot.Bot):
+
+
+
+
+
+
15classDiscordBot(discord.Client,Bot):
+16"""Bot implementing the discord protocol"""
+17def__init__(self):
+18Bot.__init__(self)
+19self.CONFIG={
+20"token":""
+21}
+22intents=discord.Intents.default()
+23intents.message_content=True
+24discord.Client.__init__(self,intents=intents)
+25
+26defbegin(self):
+27"""Start the bot"""
+28self.run(self.CONFIG.get("token"))
+29
+30asyncdefcheckMarvinActions(self,message):
+31"""Check if Marvin should perform any actions"""
+32words=self.tokenize(message.content)
+33ifself.user.name.lower()inwords:
+34foractioninself.ACTIONS:
+35response=action(words)
+36ifresponse:
+37awaitmessage.channel.send(response)
+38else:
+39foractioninself.GENERAL_ACTIONS:
+40response=action(words)
+41ifresponse:
+42awaitmessage.channel.send(response)
+43
+44asyncdefon_message(self,message):
+45"""Hook run on every message"""
+46print(f"#{message.channel.name} <{message.author}> {message.content}")
+47ifmessage.author.name==self.user.name:
+48# don't react to own messages
+49return
+50awaitself.checkMarvinActions(message)
+
+
+
+
Bot implementing the discord protocol
+
+
+
+
+
+ CONFIG
+
+
+
+
+
+
+
+
+
+
+
+
+ def
+ begin(self):
+
+
+
+
+
+
26defbegin(self):
+27"""Start the bot"""
+28self.run(self.CONFIG.get("token"))
+
44asyncdefon_message(self,message):
+45"""Hook run on every message"""
+46print(f"#{message.channel.name} <{message.author}> {message.content}")
+47ifmessage.author.name==self.user.name:
+48# don't react to own messages
+49return
+50awaitself.checkMarvinActions(message)
+
24classIrcBot(Bot):
+ 25"""Bot implementing the IRC protocol"""
+ 26def__init__(self):
+ 27super().__init__()
+ 28self.CONFIG={
+ 29"server":None,
+ 30"port":6667,
+ 31"channel":None,
+ 32"nick":"marvin",
+ 33"realname":"Marvin The All Mighty dbwebb-bot",
+ 34"ident":None,
+ 35"irclogfile":"irclog.txt",
+ 36"irclogmax":20,
+ 37"dirIncoming":"incoming",
+ 38"dirDone":"done",
+ 39"lastfm":None,
+ 40}
+ 41
+ 42# Socket for IRC server
+ 43self.SOCKET=None
+ 44
+ 45# Keep a log of the latest messages
+ 46self.IRCLOG=None
+ 47
+ 48
+ 49defconnectToServer(self):
+ 50"""Connect to the IRC Server"""
+ 51
+ 52# Create the socket & Connect to the server
+ 53server=self.CONFIG["server"]
+ 54port=self.CONFIG["port"]
+ 55
+ 56ifserverandport:
+ 57self.SOCKET=socket.socket()
+ 58print("Connecting: {SERVER}:{PORT}".format(SERVER=server,PORT=port))
+ 59self.SOCKET.connect((server,port))
+ 60else:
+ 61print("Failed to connect, missing server or port in configuration.")
+ 62return
+ 63
+ 64# Send the nick to server
+ 65nick=self.CONFIG["nick"]
+ 66ifnick:
+ 67msg='NICK {NICK}\r\n'.format(NICK=nick)
+ 68self.sendMsg(msg)
+ 69else:
+ 70print("Ignore sending nick, missing nick in configuration.")
+ 71
+ 72# Present yourself
+ 73realname=self.CONFIG["realname"]
+ 74self.sendMsg('USER {NICK} 0 * :{REALNAME}\r\n'.format(NICK=nick,REALNAME=realname))
+ 75
+ 76# This is my nick, i promise!
+ 77ident=self.CONFIG["ident"]
+ 78ifident:
+ 79self.sendMsg('PRIVMSG nick IDENTIFY {IDENT}\r\n'.format(IDENT=ident))
+ 80else:
+ 81print("Ignore identifying with password, ident is not set.")
+ 82
+ 83# Join a channel
+ 84channel=self.CONFIG["channel"]
+ 85ifchannel:
+ 86self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=channel))
+ 87else:
+ 88print("Ignore joining channel, missing channel name in configuration.")
+ 89
+ 90defsendPrivMsg(self,message,channel):
+ 91"""Send and log a PRIV message"""
+ 92ifchannel==self.CONFIG["channel"]:
+ 93self.ircLogAppend(user=self.CONFIG["nick"].ljust(8),message=message)
+ 94
+ 95msg="PRIVMSG {CHANNEL} :{MSG}\r\n".format(CHANNEL=channel,MSG=message)
+ 96self.sendMsg(msg)
+ 97
+ 98defsendMsg(self,msg):
+ 99"""Send and occasionally print the message sent"""
+100print("SEND: "+msg.rstrip('\r\n'))
+101self.SOCKET.send(msg.encode())
+102
+103defdecode_irc(self,raw,preferred_encs=None):
+104"""
+105 Do character detection.
+106 You can send preferred encodings as a list through preferred_encs.
+107 http://stackoverflow.com/questions/938870/python-irc-bot-and-encoding-issue
+108 """
+109ifpreferred_encsisNone:
+110preferred_encs=["UTF-8","CP1252","ISO-8859-1"]
+111
+112changed=False
+113enc=None
+114forencinpreferred_encs:
+115try:
+116res=raw.decode(enc)
+117changed=True
+118break
+119exceptException:
+120pass
+121
+122ifnotchanged:
+123try:
+124enc=chardet.detect(raw)['encoding']
+125res=raw.decode(enc)
+126exceptException:
+127res=raw.decode(enc,'ignore')
+128
+129returnres
+130
+131defreceive(self):
+132"""Read incoming message and guess encoding"""
+133try:
+134buf=self.SOCKET.recv(2048)
+135lines=self.decode_irc(buf)
+136lines=lines.split("\n")
+137buf=lines.pop()
+138exceptExceptionaserr:
+139print("Error reading incoming message. "+err)
+140
+141returnlines
+142
+143defircLogAppend(self,line=None,user=None,message=None):
+144"""Read incoming message and guess encoding"""
+145ifnotuser:
+146user=re.search(r"(?<=:)\w+",line[0]).group(0)
+147
+148ifnotmessage:
+149message=' '.join(line[3:]).lstrip(':')
+150
+151self.IRCLOG.append({
+152'time':datetime.now().strftime("%H:%M").rjust(5),
+153'user':user,
+154'msg':message
+155})
+156
+157defircLogWriteToFile(self):
+158"""Write IRClog to file"""
+159withopen(self.CONFIG["irclogfile"],'w',encoding="UTF-8")asf:
+160json.dump(list(self.IRCLOG),f,indent=2)
+161
+162defreadincoming(self):
+163"""
+164 Read all files in the directory incoming, send them as a message if
+165 they exists and then move the file to directory done.
+166 """
+167ifnotos.path.isdir(self.CONFIG["dirIncoming"]):
+168return
+169
+170listing=os.listdir(self.CONFIG["dirIncoming"])
+171
+172forinfileinlisting:
+173filename=os.path.join(self.CONFIG["dirIncoming"],infile)
+174
+175withopen(filename,"r",encoding="UTF-8")asf:
+176formsginf:
+177self.sendPrivMsg(msg,self.CONFIG["channel"])
+178
+179try:
+180shutil.move(filename,self.CONFIG["dirDone"])
+181exceptException:
+182os.remove(filename)
+183
+184defmainLoop(self):
+185"""For ever, listen and answer to incoming chats"""
+186self.IRCLOG=deque([],self.CONFIG["irclogmax"])
+187
+188while1:
+189# Write irclog
+190self.ircLogWriteToFile()
+191
+192# Check in any in the incoming directory
+193self.readincoming()
+194
+195forlineinself.receive():
+196print(line)
+197words=line.strip().split()
+198
+199ifnotwords:
+200continue
+201
+202self.checkIrcActions(words)
+203self.checkMarvinActions(words)
+204
+205defbegin(self):
+206"""Start the bot"""
+207self.connectToServer()
+208self.mainLoop()
+209
+210defcheckIrcActions(self,words):
+211"""
+212 Check if Marvin should take action on any messages defined in the
+213 IRC protocol.
+214 """
+215ifwords[0]=="PING":
+216self.sendMsg("PONG {ARG}\r\n".format(ARG=words[1]))
+217
+218ifwords[1]=='INVITE':
+219self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=words[3]))
+220
+221defcheckMarvinActions(self,words):
+222"""Check if Marvin should perform any actions"""
+223ifwords[1]=='PRIVMSG'andwords[2]==self.CONFIG["channel"]:
+224self.ircLogAppend(words)
+225
+226ifwords[1]=='PRIVMSG':
+227raw=' '.join(words[3:])
+228row=self.tokenize(raw)
+229
+230ifself.CONFIG["nick"]inrow:
+231foractioninself.ACTIONS:
+232msg=action(row)
+233ifmsg:
+234self.sendPrivMsg(msg,words[2])
+235break
+236else:
+237foractioninself.GENERAL_ACTIONS:
+238msg=action(row)
+239ifmsg:
+240self.sendPrivMsg(msg,words[2])
+241break
+
+
+
+
Bot implementing the IRC protocol
+
+
+
+
+
+ CONFIG
+
+
+
+
+
+
+
+
+
+
+ SOCKET
+
+
+
+
+
+
+
+
+
+
+ IRCLOG
+
+
+
+
+
+
+
+
+
+
+
+
+ def
+ connectToServer(self):
+
+
+
+
+
+
49defconnectToServer(self):
+50"""Connect to the IRC Server"""
+51
+52# Create the socket & Connect to the server
+53server=self.CONFIG["server"]
+54port=self.CONFIG["port"]
+55
+56ifserverandport:
+57self.SOCKET=socket.socket()
+58print("Connecting: {SERVER}:{PORT}".format(SERVER=server,PORT=port))
+59self.SOCKET.connect((server,port))
+60else:
+61print("Failed to connect, missing server or port in configuration.")
+62return
+63
+64# Send the nick to server
+65nick=self.CONFIG["nick"]
+66ifnick:
+67msg='NICK {NICK}\r\n'.format(NICK=nick)
+68self.sendMsg(msg)
+69else:
+70print("Ignore sending nick, missing nick in configuration.")
+71
+72# Present yourself
+73realname=self.CONFIG["realname"]
+74self.sendMsg('USER {NICK} 0 * :{REALNAME}\r\n'.format(NICK=nick,REALNAME=realname))
+75
+76# This is my nick, i promise!
+77ident=self.CONFIG["ident"]
+78ifident:
+79self.sendMsg('PRIVMSG nick IDENTIFY {IDENT}\r\n'.format(IDENT=ident))
+80else:
+81print("Ignore identifying with password, ident is not set.")
+82
+83# Join a channel
+84channel=self.CONFIG["channel"]
+85ifchannel:
+86self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=channel))
+87else:
+88print("Ignore joining channel, missing channel name in configuration.")
+
157defircLogWriteToFile(self):
+158"""Write IRClog to file"""
+159withopen(self.CONFIG["irclogfile"],'w',encoding="UTF-8")asf:
+160json.dump(list(self.IRCLOG),f,indent=2)
+
+
+
+
Write IRClog to file
+
+
+
+
+
+
+
+
+ def
+ readincoming(self):
+
+
+
+
+
+
162defreadincoming(self):
+163"""
+164 Read all files in the directory incoming, send them as a message if
+165 they exists and then move the file to directory done.
+166 """
+167ifnotos.path.isdir(self.CONFIG["dirIncoming"]):
+168return
+169
+170listing=os.listdir(self.CONFIG["dirIncoming"])
+171
+172forinfileinlisting:
+173filename=os.path.join(self.CONFIG["dirIncoming"],infile)
+174
+175withopen(filename,"r",encoding="UTF-8")asf:
+176formsginf:
+177self.sendPrivMsg(msg,self.CONFIG["channel"])
+178
+179try:
+180shutil.move(filename,self.CONFIG["dirDone"])
+181exceptException:
+182os.remove(filename)
+
+
+
+
Read all files in the directory incoming, send them as a message if
+they exists and then move the file to directory done.
+
+
+
+
+
+
+
+
+ def
+ mainLoop(self):
+
+
+
+
+
+
184defmainLoop(self):
+185"""For ever, listen and answer to incoming chats"""
+186self.IRCLOG=deque([],self.CONFIG["irclogmax"])
+187
+188while1:
+189# Write irclog
+190self.ircLogWriteToFile()
+191
+192# Check in any in the incoming directory
+193self.readincoming()
+194
+195forlineinself.receive():
+196print(line)
+197words=line.strip().split()
+198
+199ifnotwords:
+200continue
+201
+202self.checkIrcActions(words)
+203self.checkMarvinActions(words)
+
+
+
+
For ever, listen and answer to incoming chats
+
+
+
+
+
+
+
+
+ def
+ begin(self):
+
+
+
+
+
+
205defbegin(self):
+206"""Start the bot"""
+207self.connectToServer()
+208self.mainLoop()
+
+
+
+
Start the bot
+
+
+
+
+
+
+
+
+ def
+ checkIrcActions(self, words):
+
+
+
+
+
+
210defcheckIrcActions(self,words):
+211"""
+212 Check if Marvin should take action on any messages defined in the
+213 IRC protocol.
+214 """
+215ifwords[0]=="PING":
+216self.sendMsg("PONG {ARG}\r\n".format(ARG=words[1]))
+217
+218ifwords[1]=='INVITE':
+219self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=words[3]))
+
+
+
+
Check if Marvin should take action on any messages defined in the
+IRC protocol.
+
+
+
+
\ No newline at end of file
diff --git a/docs/pdoc/main.html b/docs/pdoc/main.html
new file mode 100644
index 0000000..50f44c4
--- /dev/null
+++ b/docs/pdoc/main.html
@@ -0,0 +1,745 @@
+
+
+
+
+
+
+ main API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+main
+
+
An IRC bot that answers random questions, keeps a log from the IRC-chat,
+easy to integrate in a webpage and montores a phpBB forum for latest topics
+by loggin in to the forum and checking the RSS-feed.
Check out the file 'marvin_config_default.json' on how to configure, instead
+of using cli-options. The default configfile is 'marvin_config.json' but you
+can change that using cli-options.
+
+
Make own actions
+
+
Check the file 'marvin_strings.json' for the file where most of the strings
+are defined and check out 'marvin_actions.py' to see how to write your own
+actions. Its just a small function.
+
+
Read from incoming
+
+
Marvin reads messages from the incoming/ directory, if it exists, and writes
+it out the the irc channel.
+
+
+
+
+
+
+
1#! /usr/bin/env python3
+ 2# -*- coding: utf-8 -*-
+ 3
+ 4"""
+ 5An IRC bot that answers random questions, keeps a log from the IRC-chat,
+ 6easy to integrate in a webpage and montores a phpBB forum for latest topics
+ 7by loggin in to the forum and checking the RSS-feed.
+ 8
+ 9You need to install additional modules.
+ 10
+ 11# Install needed modules in local directory
+ 12pip3 install --target modules/ feedparser beautifulsoup4 chardet
+ 13
+ 14Modules in modules/ will be loaded automatically. If you want to use a
+ 15different directory you can start the program like this instead:
+ 16
+ 17PYTHONPATH=modules python3 main.py
+ 18
+ 19# To get help
+ 20PYTHONPATH=modules python3 main.py --help
+ 21
+ 22# Example of using options
+ 23--server=irc.bsnet.se --channel=#db-o-webb
+ 24--server=irc.bsnet.se --port=6667 --channel=#db-o-webb
+ 25--nick=marvin --ident=secret
+ 26
+ 27# Configuration
+ 28Check out the file 'marvin_config_default.json' on how to configure, instead
+ 29of using cli-options. The default configfile is 'marvin_config.json' but you
+ 30can change that using cli-options.
+ 31
+ 32# Make own actions
+ 33Check the file 'marvin_strings.json' for the file where most of the strings
+ 34are defined and check out 'marvin_actions.py' to see how to write your own
+ 35actions. Its just a small function.
+ 36
+ 37# Read from incoming
+ 38Marvin reads messages from the incoming/ directory, if it exists, and writes
+ 39it out the the irc channel.
+ 40"""
+ 41
+ 42importargparse
+ 43importjson
+ 44importos
+ 45importsys
+ 46
+ 47fromdiscord_botimportDiscordBot
+ 48fromirc_botimportIrcBot
+ 49
+ 50importmarvin_actions
+ 51importmarvin_general_actions
+ 52
+ 53#
+ 54# General stuff about this program
+ 55#
+ 56PROGRAM="marvin"
+ 57AUTHOR="Mikael Roos"
+ 58EMAIL="mikael.t.h.roos@gmail.com"
+ 59VERSION="0.3.0"
+ 60MSG_VERSION="{program} version {version}.".format(program=PROGRAM,version=VERSION)
+ 61
+ 62
+ 63
+ 64defprintVersion():
+ 65"""
+ 66 Print version information and exit.
+ 67 """
+ 68print(MSG_VERSION)
+ 69sys.exit(0)
+ 70
+ 71
+ 72defmergeOptionsWithConfigFile(options,configFile):
+ 73"""
+ 74 Read information from config file.
+ 75 """
+ 76ifos.path.isfile(configFile):
+ 77withopen(configFile,encoding="UTF-8")asf:
+ 78data=json.load(f)
+ 79
+ 80options.update(data)
+ 81res=json.dumps(options,sort_keys=True,indent=4,separators=(',',': '))
+ 82
+ 83msg="Read configuration from config file '{file}'. Current configuration is:\n{config}"
+ 84print(msg.format(config=res,file=configFile))
+ 85
+ 86else:
+ 87print("Config file '{file}' is not readable, skipping.".format(file=configFile))
+ 88
+ 89returnoptions
+ 90
+ 91
+ 92defparseOptions(options):
+ 93"""
+ 94 Merge default options with incoming options and arguments and return them as a dictionary.
+ 95 """
+ 96
+ 97parser=argparse.ArgumentParser()
+ 98parser.add_argument("protocol",choices=["irc","discord"],nargs="?",default="irc")
+ 99parser.add_argument("-v","--version",action="store_true")
+100parser.add_argument("--config")
+101
+102forkey,valueinoptions.items():
+103parser.add_argument(f"--{key}",type=type(value))
+104
+105args=vars(parser.parse_args())
+106ifargs["version"]:
+107printVersion()
+108ifargs["config"]:
+109mergeOptionsWithConfigFile(options,args["config"])
+110
+111forparameterinoptions:
+112ifargs[parameter]:
+113options[parameter]=args[parameter]
+114
+115res=json.dumps(options,sort_keys=True,indent=4,separators=(',',': '))
+116print("Configuration updated after cli options:\n{config}".format(config=res))
+117
+118returnoptions
+119
+120
+121defdetermineProtocol():
+122"""Parse the argument to determine what protocol to use"""
+123parser=argparse.ArgumentParser()
+124parser.add_argument("protocol",choices=["irc","discord"],nargs="?",default="irc")
+125arg,_=parser.parse_known_args()
+126returnarg.protocol
+127
+128
+129defcreateBot(protocol):
+130"""Return an instance of a bot with the requested implementation"""
+131ifprotocol=="irc":
+132returnIrcBot()
+133ifprotocol=="discord":
+134returnDiscordBot()
+135raiseValueError(f"Unsupported protocol: {protocol}")
+136
+137
+138defmain():
+139"""
+140 Main function to carry out the work.
+141 """
+142protocol=determineProtocol()
+143bot=createBot(protocol)
+144options=bot.getConfig()
+145options.update(mergeOptionsWithConfigFile(options,"marvin_config.json"))
+146config=parseOptions(options)
+147bot.setConfig(config)
+148marvin_actions.setConfig(options)
+149marvin_general_actions.setConfig(options)
+150actions=marvin_actions.getAllActions()
+151general_actions=marvin_general_actions.getAllGeneralActions()
+152bot.registerActions(actions)
+153bot.registerGeneralActions(general_actions)
+154bot.begin()
+155
+156sys.exit(0)
+157
+158
+159if__name__=="__main__":
+160main()
+
+
+
+
+
+
+ PROGRAM =
+'marvin'
+
+
+
+
+
+
+
+
+
+
+ AUTHOR =
+'Mikael Roos'
+
+
+
+
+
+
+
+
+
+
+ EMAIL =
+'mikael.t.h.roos@gmail.com'
+
+
+
+
+
+
+
+
+
+
+ VERSION =
+'0.3.0'
+
+
+
+
+
+
+
+
+
+
+ MSG_VERSION =
+'marvin version 0.3.0.'
+
+
+
+
+
+
+
+
+
+
+
+
+ def
+ printVersion():
+
+
+
+
+
+
65defprintVersion():
+66"""
+67 Print version information and exit.
+68 """
+69print(MSG_VERSION)
+70sys.exit(0)
+
73defmergeOptionsWithConfigFile(options,configFile):
+74"""
+75 Read information from config file.
+76 """
+77ifos.path.isfile(configFile):
+78withopen(configFile,encoding="UTF-8")asf:
+79data=json.load(f)
+80
+81options.update(data)
+82res=json.dumps(options,sort_keys=True,indent=4,separators=(',',': '))
+83
+84msg="Read configuration from config file '{file}'. Current configuration is:\n{config}"
+85print(msg.format(config=res,file=configFile))
+86
+87else:
+88print("Config file '{file}' is not readable, skipping.".format(file=configFile))
+89
+90returnoptions
+
+
+
+
Read information from config file.
+
+
+
+
+
+
+
+
+ def
+ parseOptions(options):
+
+
+
+
+
+
93defparseOptions(options):
+ 94"""
+ 95 Merge default options with incoming options and arguments and return them as a dictionary.
+ 96 """
+ 97
+ 98parser=argparse.ArgumentParser()
+ 99parser.add_argument("protocol",choices=["irc","discord"],nargs="?",default="irc")
+100parser.add_argument("-v","--version",action="store_true")
+101parser.add_argument("--config")
+102
+103forkey,valueinoptions.items():
+104parser.add_argument(f"--{key}",type=type(value))
+105
+106args=vars(parser.parse_args())
+107ifargs["version"]:
+108printVersion()
+109ifargs["config"]:
+110mergeOptionsWithConfigFile(options,args["config"])
+111
+112forparameterinoptions:
+113ifargs[parameter]:
+114options[parameter]=args[parameter]
+115
+116res=json.dumps(options,sort_keys=True,indent=4,separators=(',',': '))
+117print("Configuration updated after cli options:\n{config}".format(config=res))
+118
+119returnoptions
+
+
+
+
Merge default options with incoming options and arguments and return them as a dictionary.
+
+
+
+
+
+
+
+
+ def
+ determineProtocol():
+
+
+
+
+
+
122defdetermineProtocol():
+123"""Parse the argument to determine what protocol to use"""
+124parser=argparse.ArgumentParser()
+125parser.add_argument("protocol",choices=["irc","discord"],nargs="?",default="irc")
+126arg,_=parser.parse_known_args()
+127returnarg.protocol
+
+
+
+
Parse the argument to determine what protocol to use
+
+
+
+
+
+
+
+
+ def
+ createBot(protocol):
+
+
+
+
+
+
130defcreateBot(protocol):
+131"""Return an instance of a bot with the requested implementation"""
+132ifprotocol=="irc":
+133returnIrcBot()
+134ifprotocol=="discord":
+135returnDiscordBot()
+136raiseValueError(f"Unsupported protocol: {protocol}")
+
+
+
+
Return an instance of a bot with the requested implementation
+
+
+
+
+
+
+
+
+ def
+ main():
+
+
+
+
+
+
139defmain():
+140"""
+141 Main function to carry out the work.
+142 """
+143protocol=determineProtocol()
+144bot=createBot(protocol)
+145options=bot.getConfig()
+146options.update(mergeOptionsWithConfigFile(options,"marvin_config.json"))
+147config=parseOptions(options)
+148bot.setConfig(config)
+149marvin_actions.setConfig(options)
+150marvin_general_actions.setConfig(options)
+151actions=marvin_actions.getAllActions()
+152general_actions=marvin_general_actions.getAllGeneralActions()
+153bot.registerActions(actions)
+154bot.registerGeneralActions(general_actions)
+155bot.begin()
+156
+157sys.exit(0)
+
+
+
+
Main function to carry out the work.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/pdoc/marvin_actions.html b/docs/pdoc/marvin_actions.html
new file mode 100644
index 0000000..85fdf98
--- /dev/null
+++ b/docs/pdoc/marvin_actions.html
@@ -0,0 +1,2115 @@
+
+
+
+
+
+
+ marvin_actions API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+marvin_actions
+
+
Make actions for Marvin, one function for each action.
+
+
+
+
+
+
+
1#! /usr/bin/env python3
+ 2# -*- coding: utf-8 -*-
+ 3
+ 4"""
+ 5Make actions for Marvin, one function for each action.
+ 6"""
+ 7fromurllib.parseimportquote_plus
+ 8fromurllib.requestimporturlopen
+ 9importcalendar
+ 10importdatetime
+ 11importjson
+ 12importrandom
+ 13importrequests
+ 14
+ 15frombs4importBeautifulSoup
+ 16
+ 17
+ 18defgetAllActions():
+ 19"""
+ 20 Return all actions in an array.
+ 21 """
+ 22return[
+ 23marvinExplainShell,
+ 24marvinGoogle,
+ 25marvinLunch,
+ 26marvinVideoOfToday,
+ 27marvinWhoIs,
+ 28marvinHelp,
+ 29marvinSource,
+ 30marvinBudord,
+ 31marvinQuote,
+ 32marvinStats,
+ 33marvinIrcLog,
+ 34marvinListen,
+ 35marvinWeather,
+ 36marvinSun,
+ 37marvinSayHi,
+ 38marvinSmile,
+ 39marvinStrip,
+ 40marvinTimeToBBQ,
+ 41marvinBirthday,
+ 42marvinNameday,
+ 43marvinUptime,
+ 44marvinStream,
+ 45marvinPrinciple,
+ 46marvinJoke,
+ 47marvinCommit
+ 48]
+ 49
+ 50
+ 51# Load all strings from file
+ 52withopen("marvin_strings.json",encoding="utf-8")asf:
+ 53STRINGS=json.load(f)
+ 54
+ 55# Configuration loaded
+ 56CONFIG=None
+ 57
+ 58defsetConfig(config):
+ 59"""
+ 60 Keep reference to the loaded configuration.
+ 61 """
+ 62globalCONFIG
+ 63CONFIG=config
+ 64
+ 65
+ 66defgetString(key,key1=None):
+ 67"""
+ 68 Get a string from the string database.
+ 69 """
+ 70data=STRINGS[key]
+ 71ifisinstance(data,list):
+ 72res=data[random.randint(0,len(data)-1)]
+ 73elifisinstance(data,dict):
+ 74ifkey1isNone:
+ 75res=data
+ 76else:
+ 77res=data[key1]
+ 78ifisinstance(res,list):
+ 79res=res[random.randint(0,len(res)-1)]
+ 80elifisinstance(data,str):
+ 81res=data
+ 82
+ 83returnres
+ 84
+ 85
+ 86defmarvinSmile(row):
+ 87"""
+ 88 Make Marvin smile.
+ 89 """
+ 90msg=None
+ 91ifany(rinrowforrin["smile","le","skratta","smilies"]):
+ 92smilie=getString("smile")
+ 93msg="{SMILE}".format(SMILE=smilie)
+ 94returnmsg
+ 95
+ 96
+ 97defwordsAfterKeyWords(words,keyWords):
+ 98"""
+ 99 Return all items in the words list after the first occurence
+100 of an item in the keyWords list.
+101 """
+102kwIndex=[]
+103forkwinkeyWords:
+104ifkwinwords:
+105kwIndex.append(words.index(kw))
+106
+107ifnotkwIndex:
+108returnNone
+109
+110returnwords[min(kwIndex)+1:]
+111
+112
+113defmarvinGoogle(row):
+114"""
+115 Let Marvin present an url to google.
+116 """
+117query=wordsAfterKeyWords(row,["google","googla"])
+118ifnotquery:
+119returnNone
+120
+121searchStr=" ".join(query)
+122url="https://www.google.se/search?q="
+123url+=quote_plus(searchStr)
+124msg=getString("google")
+125returnmsg.format(url)
+126
+127
+128defmarvinExplainShell(row):
+129"""
+130 Let Marvin present an url to the service explain shell to
+131 explain a shell command.
+132 """
+133query=wordsAfterKeyWords(row,["explain","förklara"])
+134ifnotquery:
+135returnNone
+136cmd=" ".join(query)
+137url="http://explainshell.com/explain?cmd="
+138url+=quote_plus(cmd,"/:")
+139msg=getString("explainShell")
+140returnmsg.format(url)
+141
+142
+143defmarvinSource(row):
+144"""
+145 State message about sourcecode.
+146 """
+147msg=None
+148ifany(rinrowforrin["källkod","source"]):
+149msg=getString("source")
+150
+151returnmsg
+152
+153
+154defmarvinBudord(row):
+155"""
+156 What are the budord for Marvin?
+157 """
+158msg=None
+159ifany(rinrowforrin["budord","stentavla"]):
+160ifany(rinrowforrin["#1","1"]):
+161msg=getString("budord","#1")
+162elifany(rinrowforrin["#2","2"]):
+163msg=getString("budord","#2")
+164elifany(rinrowforrin["#3","3"]):
+165msg=getString("budord","#3")
+166elifany(rinrowforrin["#4","4"]):
+167msg=getString("budord","#4")
+168elifany(rinrowforrin["#5","5"]):
+169msg=getString("budord","#5")
+170
+171returnmsg
+172
+173
+174defmarvinQuote(row):
+175"""
+176 Make a quote.
+177 """
+178msg=None
+179ifany(rinrowforrin["quote","citat","filosofi","filosofera"]):
+180msg=getString("hitchhiker")
+181
+182returnmsg
+183
+184
+185defvideoOfToday():
+186"""
+187 Check what day it is and provide a url to a suitable video together with a greeting.
+188 """
+189dayNum=datetime.date.weekday(datetime.date.today())+1
+190msg=getString("weekdays",str(dayNum))
+191video=getString("video-of-today",str(dayNum))
+192
+193ifvideo:
+194msg+=" En passande video är "+video
+195else:
+196msg+=" Jag har ännu ingen passande video för denna dagen."
+197
+198returnmsg
+199
+200
+201defmarvinVideoOfToday(row):
+202"""
+203 Show the video of today.
+204 """
+205msg=None
+206ifany(rinrowforrin["idag","dagens"]):
+207ifany(rinrowforrin["video","youtube","tube"]):
+208msg=videoOfToday()
+209
+210returnmsg
+211
+212
+213defmarvinWhoIs(row):
+214"""
+215 Who is Marvin.
+216 """
+217msg=None
+218ifall(rinrowforrin["vem","är"]):
+219msg=getString("whois")
+220
+221returnmsg
+222
+223
+224defmarvinHelp(row):
+225"""
+226 Provide a menu.
+227 """
+228msg=None
+229ifany(rinrowforrin["hjälp","help","menu","meny"]):
+230msg=getString("menu")
+231
+232returnmsg
+233
+234
+235defmarvinStats(row):
+236"""
+237 Provide a link to the stats.
+238 """
+239msg=None
+240ifany(rinrowforrin["stats","statistik","ircstats"]):
+241msg=getString("ircstats")
+242
+243returnmsg
+244
+245
+246defmarvinIrcLog(row):
+247"""
+248 Provide a link to the irclog
+249 """
+250msg=None
+251ifany(rinrowforrin["irc","irclog","log","irclogg","logg","historik"]):
+252msg=getString("irclog")
+253
+254returnmsg
+255
+256
+257defmarvinSayHi(row):
+258"""
+259 Say hi with a nice message.
+260 """
+261msg=None
+262ifany(rinrowforrin[
+263"snälla","hej","tjena","morsning","morrn","mår","hallå",
+264"halloj","läget","snäll","duktig","träna","träning",
+265"utbildning","tack","tacka","tackar","tacksam"
+266]):
+267smile=getString("smile")
+268hello=getString("hello")
+269friendly=getString("friendly")
+270msg="{}{}{}".format(smile,hello,friendly)
+271
+272returnmsg
+273
+274
+275defmarvinLunch(row):
+276"""
+277 Help decide where to eat.
+278 """
+279lunchOptions={
+280'stan centrum karlskrona kna':'lunch-karlskrona',
+281'ängelholm angelholm engelholm':'lunch-angelholm',
+282'hässleholm hassleholm':'lunch-hassleholm',
+283'malmö malmo malmoe':'lunch-malmo',
+284'göteborg goteborg gbg':'lunch-goteborg'
+285}
+286
+287ifany(rinrowforrin["lunch","mat","äta","luncha"]):
+288lunchStr=getString('lunch-message')
+289
+290forkeys,valueinlunchOptions.items():
+291ifany(rinrowforrinkeys.split(" ")):
+292returnlunchStr.format(getString(value))
+293
+294returnlunchStr.format(getString('lunch-bth'))
+295
+296returnNone
+297
+298
+299defmarvinListen(row):
+300"""
+301 Return music last listened to.
+302 """
+303msg=None
+304ifany(rinrowforrin["lyssna","lyssnar","musik"]):
+305
+306ifnotCONFIG["lastfm"]:
+307returngetString("listen","disabled")
+308
+309url="http://ws.audioscrobbler.com/2.0/"
+310
+311try:
+312params=dict(
+313method="user.getrecenttracks",
+314user=CONFIG["lastfm"]["user"],
+315api_key=CONFIG["lastfm"]["apikey"],
+316format="json",
+317limit="1"
+318)
+319
+320resp=requests.get(url=url,params=params,timeout=5)
+321data=json.loads(resp.text)
+322
+323artist=data["recenttracks"]["track"][0]["artist"]["#text"]
+324title=data["recenttracks"]["track"][0]["name"]
+325link=data["recenttracks"]["track"][0]["url"]
+326
+327msg=getString("listen","success").format(artist=artist,title=title,link=link)
+328
+329exceptException:
+330msg=getString("listen","failed")
+331
+332returnmsg
+333
+334
+335defmarvinSun(row):
+336"""
+337 Check when the sun goes up and down.
+338 """
+339msg=None
+340ifany(rinrowforrin["sol","solen","solnedgång","soluppgång"]):
+341try:
+342soup=BeautifulSoup(urlopen('http://www.timeanddate.com/sun/sweden/jonkoping'))
+343spans=soup.find_all("span",{"class":"three"})
+344sunrise=spans[0].text
+345sunset=spans[1].text
+346msg=getString("sun").format(sunrise,sunset)
+347
+348exceptException:
+349msg=getString("sun-no")
+350
+351returnmsg
+352
+353
+354defmarvinWeather(row):
+355"""
+356 Check what the weather prognosis looks like.
+357 """
+358msg=None
+359ifany(rinrowforrin["väder","vädret","prognos","prognosen","smhi"]):
+360url=getString("smhi","url")
+361try:
+362soup=BeautifulSoup(urlopen(url))
+363msg="{}. {}. {}".format(
+364soup.h1.text,
+365soup.h4.text,
+366soup.h4.findNextSibling("p").text
+367)
+368
+369exceptException:
+370msg=getString("smhi","failed")
+371
+372returnmsg
+373
+374
+375defmarvinStrip(row):
+376"""
+377 Get a comic strip.
+378 """
+379msg=None
+380ifany(rinrowforrin["strip","comic","nöje","paus"]):
+381msg=commitStrip(randomize=any(rinrowforrin["rand","random","slump","lucky"]))
+382returnmsg
+383
+384
+385defcommitStrip(randomize=False):
+386"""
+387 Latest or random comic strip from CommitStrip.
+388 """
+389msg=getString("commitstrip","message")
+390
+391ifrandomize:
+392first=getString("commitstrip","first")
+393last=getString("commitstrip","last")
+394rand=random.randint(first,last)
+395url=getString("commitstrip","urlPage")+str(rand)
+396else:
+397url=getString("commitstrip","url")
+398
+399returnmsg.format(url=url)
+400
+401
+402defmarvinTimeToBBQ(row):
+403"""
+404 Calcuate the time to next barbecue and print a appropriate msg
+405 """
+406msg=None
+407ifany(rinrowforrin["grilla","grill","grillcon","bbq"]):
+408url=getString("barbecue","url")
+409nextDate=nextBBQ()
+410today=datetime.date.today()
+411daysRemaining=(nextDate-today).days
+412
+413ifdaysRemaining==0:
+414msg=getString("barbecue","today")
+415elifdaysRemaining==1:
+416msg=getString("barbecue","tomorrow")
+417elif1<daysRemaining<14:
+418msg=getString("barbecue","week")%nextDate
+419elif14<daysRemaining<200:
+420msg=getString("barbecue","base")%nextDate
+421else:
+422msg=getString("barbecue","eternity")%nextDate
+423
+424msg=url+". "+msg
+425returnmsg
+426
+427defnextBBQ():
+428"""
+429 Calculate the next grillcon date after today
+430 """
+431
+432MAY=5
+433SEPTEMBER=9
+434
+435after=datetime.date.today()
+436spring=thirdFridayIn(after.year,MAY)
+437ifafter<=spring:
+438returnspring
+439
+440autumn=thirdFridayIn(after.year,SEPTEMBER)
+441ifafter<=autumn:
+442returnautumn
+443
+444returnthirdFridayIn(after.year+1,MAY)
+445
+446
+447defthirdFridayIn(y,m):
+448"""
+449 Get the third Friday in a given month and year
+450 """
+451THIRD=2
+452FRIDAY=-1
+453
+454# Start the weeks on saturday to prevent fridays from previous month
+455cal=calendar.Calendar(firstweekday=calendar.SATURDAY)
+456
+457# Return the friday in the third week
+458returncal.monthdatescalendar(y,m)[THIRD][FRIDAY]
+459
+460
+461defmarvinBirthday(row):
+462"""
+463 Check birthday info
+464 """
+465msg=None
+466ifany(rinrowforrin["birthday","födelsedag"]):
+467try:
+468url=getString("birthday","url")
+469soup=BeautifulSoup(urlopen(url),"html.parser")
+470my_list=list()
+471
+472foranainsoup.findAll('a'):
+473ifana.parent.name=='strong':
+474my_list.append(ana.getText())
+475
+476my_list.pop()
+477my_strings=', '.join(my_list)
+478ifnotmy_strings:
+479msg=getString("birthday","nobody")
+480else:
+481msg=getString("birthday","somebody").format(my_strings)
+482
+483exceptException:
+484msg=getString("birthday","error")
+485
+486returnmsg
+487
+488defmarvinNameday(row):
+489"""
+490 Check current nameday
+491 """
+492msg=None
+493ifany(rinrowforrin["nameday","namnsdag"]):
+494try:
+495now=datetime.datetime.now()
+496raw_url="http://api.dryg.net/dagar/v2.1/{year}/{month}/{day}"
+497url=raw_url.format(year=now.year,month=now.month,day=now.day)
+498r=requests.get(url,timeout=5)
+499nameday_data=r.json()
+500names=nameday_data["dagar"][0]["namnsdag"]
+501ifnames:
+502msg=getString("nameday","somebody").format(",".join(names))
+503else:
+504msg=getString("nameday","nobody")
+505exceptException:
+506msg=getString("nameday","error")
+507returnmsg
+508
+509defmarvinUptime(row):
+510"""
+511 Display info about uptime tournament
+512 """
+513msg=None
+514if"uptime"inrow:
+515msg=getString("uptime","info")
+516returnmsg
+517
+518defmarvinStream(row):
+519"""
+520 Display info about stream
+521 """
+522msg=None
+523ifany(rinrowforrin["stream","streama","ström","strömma"]):
+524msg=getString("stream","info")
+525returnmsg
+526
+527defmarvinPrinciple(row):
+528"""
+529 Display one selected software principle, or provide one as random
+530 """
+531msg=None
+532ifany(rinrowforrin["principle","princip","principer"]):
+533principles=getString("principle")
+534principleKeys=list(principles.keys())
+535matchedKeys=[kforkinrowifkinprincipleKeys]
+536ifmatchedKeys:
+537msg=principles[matchedKeys.pop()]
+538else:
+539msg=principles[random.choice(principleKeys)]
+540returnmsg
+541
+542defgetJoke():
+543"""
+544 Retrieves joke from api.chucknorris.io/jokes/random?category=dev
+545 """
+546try:
+547url=getString("joke","url")
+548r=requests.get(url,timeout=5)
+549joke_data=r.json()
+550returnjoke_data["value"]
+551exceptException:
+552returngetString("joke","error")
+553
+554defmarvinJoke(row):
+555"""
+556 Display a random Chuck Norris joke
+557 """
+558msg=None
+559ifany(rinrowforrin["joke","skämt","chuck norris","chuck","norris"]):
+560msg=getJoke()
+561returnmsg
+562
+563defgetCommit():
+564"""
+565 Retrieves random commit message from whatthecommit.com/index.html
+566 """
+567try:
+568url=getString("commit","url")
+569r=requests.get(url,timeout=5)
+570res=r.text.strip()
+571returnres
+572exceptException:
+573returngetString("commit","error")
+574
+575defmarvinCommit(row):
+576"""
+577 Display a random commit message
+578 """
+579msg=None
+580ifany(rinrowforrin["commit","-m"]):
+581commitMsg=getCommit()
+582msg="Använd detta meddelandet: '{}'".format(commitMsg)
+583returnmsg
+
98defwordsAfterKeyWords(words,keyWords):
+ 99"""
+100 Return all items in the words list after the first occurence
+101 of an item in the keyWords list.
+102 """
+103kwIndex=[]
+104forkwinkeyWords:
+105ifkwinwords:
+106kwIndex.append(words.index(kw))
+107
+108ifnotkwIndex:
+109returnNone
+110
+111returnwords[min(kwIndex)+1:]
+
+
+
+
Return all items in the words list after the first occurence
+of an item in the keyWords list.
+
+
+
+
+
+
+
+
+ def
+ marvinGoogle(row):
+
+
+
+
+
+
114defmarvinGoogle(row):
+115"""
+116 Let Marvin present an url to google.
+117 """
+118query=wordsAfterKeyWords(row,["google","googla"])
+119ifnotquery:
+120returnNone
+121
+122searchStr=" ".join(query)
+123url="https://www.google.se/search?q="
+124url+=quote_plus(searchStr)
+125msg=getString("google")
+126returnmsg.format(url)
+
+
+
+
Let Marvin present an url to google.
+
+
+
+
+
+
+
+
+ def
+ marvinExplainShell(row):
+
+
+
+
+
+
129defmarvinExplainShell(row):
+130"""
+131 Let Marvin present an url to the service explain shell to
+132 explain a shell command.
+133 """
+134query=wordsAfterKeyWords(row,["explain","förklara"])
+135ifnotquery:
+136returnNone
+137cmd=" ".join(query)
+138url="http://explainshell.com/explain?cmd="
+139url+=quote_plus(cmd,"/:")
+140msg=getString("explainShell")
+141returnmsg.format(url)
+
+
+
+
Let Marvin present an url to the service explain shell to
+explain a shell command.
+
+
+
+
+
+
+
+
+ def
+ marvinSource(row):
+
+
+
+
+
+
144defmarvinSource(row):
+145"""
+146 State message about sourcecode.
+147 """
+148msg=None
+149ifany(rinrowforrin["källkod","source"]):
+150msg=getString("source")
+151
+152returnmsg
+
+
+
+
State message about sourcecode.
+
+
+
+
+
+
+
+
+ def
+ marvinBudord(row):
+
+
+
+
+
+
155defmarvinBudord(row):
+156"""
+157 What are the budord for Marvin?
+158 """
+159msg=None
+160ifany(rinrowforrin["budord","stentavla"]):
+161ifany(rinrowforrin["#1","1"]):
+162msg=getString("budord","#1")
+163elifany(rinrowforrin["#2","2"]):
+164msg=getString("budord","#2")
+165elifany(rinrowforrin["#3","3"]):
+166msg=getString("budord","#3")
+167elifany(rinrowforrin["#4","4"]):
+168msg=getString("budord","#4")
+169elifany(rinrowforrin["#5","5"]):
+170msg=getString("budord","#5")
+171
+172returnmsg
+
+
+
+
What are the budord for Marvin?
+
+
+
+
+
+
+
+
+ def
+ marvinQuote(row):
+
+
+
+
+
+
175defmarvinQuote(row):
+176"""
+177 Make a quote.
+178 """
+179msg=None
+180ifany(rinrowforrin["quote","citat","filosofi","filosofera"]):
+181msg=getString("hitchhiker")
+182
+183returnmsg
+
+
+
+
Make a quote.
+
+
+
+
+
+
+
+
+ def
+ videoOfToday():
+
+
+
+
+
+
186defvideoOfToday():
+187"""
+188 Check what day it is and provide a url to a suitable video together with a greeting.
+189 """
+190dayNum=datetime.date.weekday(datetime.date.today())+1
+191msg=getString("weekdays",str(dayNum))
+192video=getString("video-of-today",str(dayNum))
+193
+194ifvideo:
+195msg+=" En passande video är "+video
+196else:
+197msg+=" Jag har ännu ingen passande video för denna dagen."
+198
+199returnmsg
+
+
+
+
Check what day it is and provide a url to a suitable video together with a greeting.
+
+
+
+
+
+
+
+
+ def
+ marvinVideoOfToday(row):
+
+
+
+
+
+
202defmarvinVideoOfToday(row):
+203"""
+204 Show the video of today.
+205 """
+206msg=None
+207ifany(rinrowforrin["idag","dagens"]):
+208ifany(rinrowforrin["video","youtube","tube"]):
+209msg=videoOfToday()
+210
+211returnmsg
+
+
+
+
Show the video of today.
+
+
+
+
+
+
+
+
+ def
+ marvinWhoIs(row):
+
+
+
+
+
+
214defmarvinWhoIs(row):
+215"""
+216 Who is Marvin.
+217 """
+218msg=None
+219ifall(rinrowforrin["vem","är"]):
+220msg=getString("whois")
+221
+222returnmsg
+
+
+
+
Who is Marvin.
+
+
+
+
+
+
+
+
+ def
+ marvinHelp(row):
+
+
+
+
+
+
225defmarvinHelp(row):
+226"""
+227 Provide a menu.
+228 """
+229msg=None
+230ifany(rinrowforrin["hjälp","help","menu","meny"]):
+231msg=getString("menu")
+232
+233returnmsg
+
+
+
+
Provide a menu.
+
+
+
+
+
+
+
+
+ def
+ marvinStats(row):
+
+
+
+
+
+
236defmarvinStats(row):
+237"""
+238 Provide a link to the stats.
+239 """
+240msg=None
+241ifany(rinrowforrin["stats","statistik","ircstats"]):
+242msg=getString("ircstats")
+243
+244returnmsg
+
+
+
+
Provide a link to the stats.
+
+
+
+
+
+
+
+
+ def
+ marvinIrcLog(row):
+
+
+
+
+
+
247defmarvinIrcLog(row):
+248"""
+249 Provide a link to the irclog
+250 """
+251msg=None
+252ifany(rinrowforrin["irc","irclog","log","irclogg","logg","historik"]):
+253msg=getString("irclog")
+254
+255returnmsg
+
+
+
+
Provide a link to the irclog
+
+
+
+
+
+
+
+
+ def
+ marvinSayHi(row):
+
+
+
+
+
+
258defmarvinSayHi(row):
+259"""
+260 Say hi with a nice message.
+261 """
+262msg=None
+263ifany(rinrowforrin[
+264"snälla","hej","tjena","morsning","morrn","mår","hallå",
+265"halloj","läget","snäll","duktig","träna","träning",
+266"utbildning","tack","tacka","tackar","tacksam"
+267]):
+268smile=getString("smile")
+269hello=getString("hello")
+270friendly=getString("friendly")
+271msg="{}{}{}".format(smile,hello,friendly)
+272
+273returnmsg
+
+
+
+
Say hi with a nice message.
+
+
+
+
+
+
+
+
+ def
+ marvinLunch(row):
+
+
+
+
+
+
276defmarvinLunch(row):
+277"""
+278 Help decide where to eat.
+279 """
+280lunchOptions={
+281'stan centrum karlskrona kna':'lunch-karlskrona',
+282'ängelholm angelholm engelholm':'lunch-angelholm',
+283'hässleholm hassleholm':'lunch-hassleholm',
+284'malmö malmo malmoe':'lunch-malmo',
+285'göteborg goteborg gbg':'lunch-goteborg'
+286}
+287
+288ifany(rinrowforrin["lunch","mat","äta","luncha"]):
+289lunchStr=getString('lunch-message')
+290
+291forkeys,valueinlunchOptions.items():
+292ifany(rinrowforrinkeys.split(" ")):
+293returnlunchStr.format(getString(value))
+294
+295returnlunchStr.format(getString('lunch-bth'))
+296
+297returnNone
+
376defmarvinStrip(row):
+377"""
+378 Get a comic strip.
+379 """
+380msg=None
+381ifany(rinrowforrin["strip","comic","nöje","paus"]):
+382msg=commitStrip(randomize=any(rinrowforrin["rand","random","slump","lucky"]))
+383returnmsg
+
+
+
+
Get a comic strip.
+
+
+
+
+
+
+
+
+ def
+ commitStrip(randomize=False):
+
+
+
+
+
+
386defcommitStrip(randomize=False):
+387"""
+388 Latest or random comic strip from CommitStrip.
+389 """
+390msg=getString("commitstrip","message")
+391
+392ifrandomize:
+393first=getString("commitstrip","first")
+394last=getString("commitstrip","last")
+395rand=random.randint(first,last)
+396url=getString("commitstrip","urlPage")+str(rand)
+397else:
+398url=getString("commitstrip","url")
+399
+400returnmsg.format(url=url)
+
+
+
+
Latest or random comic strip from CommitStrip.
+
+
+
+
+
+
+
+
+ def
+ marvinTimeToBBQ(row):
+
+
+
+
+
+
403defmarvinTimeToBBQ(row):
+404"""
+405 Calcuate the time to next barbecue and print a appropriate msg
+406 """
+407msg=None
+408ifany(rinrowforrin["grilla","grill","grillcon","bbq"]):
+409url=getString("barbecue","url")
+410nextDate=nextBBQ()
+411today=datetime.date.today()
+412daysRemaining=(nextDate-today).days
+413
+414ifdaysRemaining==0:
+415msg=getString("barbecue","today")
+416elifdaysRemaining==1:
+417msg=getString("barbecue","tomorrow")
+418elif1<daysRemaining<14:
+419msg=getString("barbecue","week")%nextDate
+420elif14<daysRemaining<200:
+421msg=getString("barbecue","base")%nextDate
+422else:
+423msg=getString("barbecue","eternity")%nextDate
+424
+425msg=url+". "+msg
+426returnmsg
+
+
+
+
Calcuate the time to next barbecue and print a appropriate msg
+
+
+
+
+
+
+
+
+ def
+ nextBBQ():
+
+
+
+
+
+
428defnextBBQ():
+429"""
+430 Calculate the next grillcon date after today
+431 """
+432
+433MAY=5
+434SEPTEMBER=9
+435
+436after=datetime.date.today()
+437spring=thirdFridayIn(after.year,MAY)
+438ifafter<=spring:
+439returnspring
+440
+441autumn=thirdFridayIn(after.year,SEPTEMBER)
+442ifafter<=autumn:
+443returnautumn
+444
+445returnthirdFridayIn(after.year+1,MAY)
+
+
+
+
Calculate the next grillcon date after today
+
+
+
+
+
+
+
+
+ def
+ thirdFridayIn(y, m):
+
+
+
+
+
+
448defthirdFridayIn(y,m):
+449"""
+450 Get the third Friday in a given month and year
+451 """
+452THIRD=2
+453FRIDAY=-1
+454
+455# Start the weeks on saturday to prevent fridays from previous month
+456cal=calendar.Calendar(firstweekday=calendar.SATURDAY)
+457
+458# Return the friday in the third week
+459returncal.monthdatescalendar(y,m)[THIRD][FRIDAY]
+
510defmarvinUptime(row):
+511"""
+512 Display info about uptime tournament
+513 """
+514msg=None
+515if"uptime"inrow:
+516msg=getString("uptime","info")
+517returnmsg
+
+
+
+
Display info about uptime tournament
+
+
+
+
+
+
+
+
+ def
+ marvinStream(row):
+
+
+
+
+
+
519defmarvinStream(row):
+520"""
+521 Display info about stream
+522 """
+523msg=None
+524ifany(rinrowforrin["stream","streama","ström","strömma"]):
+525msg=getString("stream","info")
+526returnmsg
+
+
+
+
Display info about stream
+
+
+
+
+
+
+
+
+ def
+ marvinPrinciple(row):
+
+
+
+
+
+
528defmarvinPrinciple(row):
+529"""
+530 Display one selected software principle, or provide one as random
+531 """
+532msg=None
+533ifany(rinrowforrin["principle","princip","principer"]):
+534principles=getString("principle")
+535principleKeys=list(principles.keys())
+536matchedKeys=[kforkinrowifkinprincipleKeys]
+537ifmatchedKeys:
+538msg=principles[matchedKeys.pop()]
+539else:
+540msg=principles[random.choice(principleKeys)]
+541returnmsg
+
+
+
+
Display one selected software principle, or provide one as random
Marvin says Good morning after someone else says it
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/pdoc/search.js b/docs/pdoc/search.js
new file mode 100644
index 0000000..013a072
--- /dev/null
+++ b/docs/pdoc/search.js
@@ -0,0 +1,46 @@
+window.pdocSearch = (function(){
+/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();oModule for the common base class for all Bots\n"}, "bot.Bot": {"fullname": "bot.Bot", "modulename": "bot", "qualname": "Bot", "kind": "class", "doc": "
Base class for things common between different protocols
An IRC bot that answers random questions, keeps a log from the IRC-chat,\neasy to integrate in a webpage and montores a phpBB forum for latest topics\nby loggin in to the forum and checking the RSS-feed.
Check out the file 'marvin_config_default.json' on how to configure, instead\nof using cli-options. The default configfile is 'marvin_config.json' but you\ncan change that using cli-options.
\n\n
Make own actions
\n\n
Check the file 'marvin_strings.json' for the file where most of the strings\nare defined and check out 'marvin_actions.py' to see how to write your own\nactions. Its just a small function.
\n\n
Read from incoming
\n\n
Marvin reads messages from the incoming/ directory, if it exists, and writes\nit out the the irc channel.
1#! /usr/bin/env python3
+ 2# -*- coding: utf-8 -*-
+ 3
+ 4"""
+ 5Tests for the main launcher
+ 6"""
+ 7
+ 8importargparse
+ 9importcontextlib
+ 10importio
+ 11importos
+ 12importsys
+ 13fromunittestimportTestCase
+ 14
+ 15frommainimportmergeOptionsWithConfigFile,parseOptions,determineProtocol,MSG_VERSION,createBot
+ 16fromirc_botimportIrcBot
+ 17fromdiscord_botimportDiscordBot
+ 18
+ 19
+ 20classConfigMergeTest(TestCase):
+ 21"""Test merging a config file with a dict"""
+ 22
+ 23defassertMergedConfig(self,config,fileName,expected):
+ 24"""Merge dict with file and assert the result matches expected"""
+ 25configFile=os.path.join("testConfigs",f"{fileName}.json")
+ 26actualConfig=mergeOptionsWithConfigFile(config,configFile)
+ 27self.assertEqual(actualConfig,expected)
+ 28
+ 29
+ 30deftestEmpty(self):
+ 31"""Empty into empty should equal empty"""
+ 32self.assertMergedConfig({},"empty",{})
+ 33
+ 34deftestAddSingleParameter(self):
+ 35"""Add a single parameter to an empty config"""
+ 36new={
+ 37"single":"test"
+ 38}
+ 39expected={
+ 40"single":"test"
+ 41}
+ 42self.assertMergedConfig(new,"empty",expected)
+ 43
+ 44deftestAddSingleParameterOverwrites(self):
+ 45"""Add a single parameter to a config that contains it already"""
+ 46new={
+ 47"single":"test"
+ 48}
+ 49expected={
+ 50"single":"original"
+ 51}
+ 52self.assertMergedConfig(new,"single",expected)
+ 53
+ 54deftestAddSingleParameterMerges(self):
+ 55"""Add a single parameter to a config that contains a different one"""
+ 56new={
+ 57"new":"test"
+ 58}
+ 59expected={
+ 60"new":"test",
+ 61"single":"original"
+ 62}
+ 63self.assertMergedConfig(new,"single",expected)
+ 64
+ 65classConfigParseTest(TestCase):
+ 66"""Test parsing options into a config"""
+ 67
+ 68SAMPLE_CONFIG={
+ 69"server":"localhost",
+ 70"port":6667,
+ 71"channel":"#dbwebb",
+ 72"nick":"marvin",
+ 73"realname":"Marvin The All Mighty dbwebb-bot",
+ 74"ident":"password"
+ 75}
+ 76
+ 77CHANGED_CONFIG={
+ 78"server":"remotehost",
+ 79"port":1234,
+ 80"channel":"#db-o-webb",
+ 81"nick":"imposter",
+ 82"realname":"where is marvin?",
+ 83"ident":"identify"
+ 84}
+ 85
+ 86deftestOverrideHardcodedParameters(self):
+ 87"""Test that all the hard coded parameters can be overridden from commandline"""
+ 88forparameterin["server","port","channel","nick","realname","ident"]:
+ 89sys.argv=["./main.py",f"--{parameter}",str(self.CHANGED_CONFIG.get(parameter))]
+ 90actual=parseOptions(self.SAMPLE_CONFIG)
+ 91self.assertEqual(actual.get(parameter),self.CHANGED_CONFIG.get(parameter))
+ 92
+ 93deftestOverrideMultipleParameters(self):
+ 94"""Test that multiple parameters can be overridden from commandline"""
+ 95sys.argv=["./main.py","--server","dbwebb.se","--port","5432"]
+ 96actual=parseOptions(self.SAMPLE_CONFIG)
+ 97self.assertEqual(actual.get("server"),"dbwebb.se")
+ 98self.assertEqual(actual.get("port"),5432)
+ 99
+100deftestOverrideWithFile(self):
+101"""Test that parameters can be overridden with the --config option"""
+102configFile=os.path.join("testConfigs","server.json")
+103sys.argv=["./main.py","--config",configFile]
+104actual=parseOptions(self.SAMPLE_CONFIG)
+105self.assertEqual(actual.get("server"),"irc.dbwebb.se")
+106
+107deftestOverridePrecedenceConfigFirst(self):
+108"""Test that proper precedence is considered. From most to least significant it should be:
+109 explicit parameter -> parameter in --config file -> default """
+110
+111configFile=os.path.join("testConfigs","server.json")
+112sys.argv=["./main.py","--config",configFile,"--server","important.com"]
+113actual=parseOptions(self.SAMPLE_CONFIG)
+114self.assertEqual(actual.get("server"),"important.com")
+115
+116deftestOverridePrecedenceParameterFirst(self):
+117"""Test that proper precedence is considered. From most to least significant it should be:
+118 explicit parameter -> parameter in --config file -> default """
+119
+120configFile=os.path.join("testConfigs","server.json")
+121sys.argv=["./main.py","--server","important.com","--config",configFile]
+122actual=parseOptions(self.SAMPLE_CONFIG)
+123self.assertEqual(actual.get("server"),"important.com")
+124
+125deftestBannedParameters(self):
+126"""Don't allow config, help and version as parameters, as those options are special"""
+127forbannedParameterin["config","help","version"]:
+128withself.assertRaises(argparse.ArgumentError):
+129parseOptions({bannedParameter:"test"})
+130
+131
+132classFormattingTest(TestCase):
+133"""Test the parameters that cause printouts"""
+134
+135USAGE=("usage: main.py [-h] [-v] [--config CONFIG] [--server SERVER] [--port PORT] "
+136"[--channel CHANNEL] [--nick NICK] [--realname REALNAME] [--ident IDENT]\n"
+137" [{irc,discord}]\n")
+138
+139OPTIONS=("positional arguments:\n {irc,discord}\n\n"
+140"options:\n"
+141" -h, --help show this help message and exit\n"
+142" -v, --version\n"
+143" --config CONFIG\n"
+144" --server SERVER\n"
+145" --port PORT\n"
+146" --channel CHANNEL\n"
+147" --nick NICK\n"
+148" --realname REALNAME\n"
+149" --ident IDENT")
+150
+151
+152@classmethod
+153defsetUpClass(cls):
+154"""Set the terminal width to 160 to prevent the tests from failing on small terminals"""
+155os.environ["COLUMNS"]="160"
+156
+157
+158defassertPrintOption(self,options,returnCode,output):
+159"""Assert that parseOptions returns a certain code and prints a certain output"""
+160withself.assertRaises(SystemExit)ase:
+161s=io.StringIO()
+162withcontextlib.redirect_stdout(s):
+163sys.argv=["./main.py"]+[options]
+164parseOptions(ConfigParseTest.SAMPLE_CONFIG)
+165self.assertEqual(e.exception.code,returnCode)
+166self.assertEqual(s.getvalue(),output+"\n")# extra newline added by print()
+167
+168
+169deftestHelpPrintout(self):
+170"""Test that a help is printed when providing the --help flag"""
+171self.assertPrintOption("--help",0,f"{self.USAGE}\n{self.OPTIONS}")
+172
+173deftestHelpPrintoutShort(self):
+174"""Test that a help is printed when providing the -h flag"""
+175self.assertPrintOption("-h",0,f"{self.USAGE}\n{self.OPTIONS}")
+176
+177deftestVersionPrintout(self):
+178"""Test that the version is printed when provided the --version flag"""
+179self.assertPrintOption("--version",0,MSG_VERSION)
+180
+181deftestVersionPrintoutShort(self):
+182"""Test that the version is printed when provided the -v flag"""
+183self.assertPrintOption("-v",0,MSG_VERSION)
+184
+185deftestUnhandledOption(self):
+186"""Test that unknown options gives an error"""
+187withself.assertRaises(SystemExit)ase:
+188s=io.StringIO()
+189expectedError=f"{self.USAGE}main.py: error: unrecognized arguments: -g\n"
+190withcontextlib.redirect_stderr(s):
+191sys.argv=["./main.py","-g"]
+192parseOptions(ConfigParseTest.SAMPLE_CONFIG)
+193self.assertEqual(e.exception.code,2)
+194self.assertEqual(s.getvalue(),expectedError)
+195
+196deftestUnhandledArgument(self):
+197"""Test that any argument gives an error"""
+198withself.assertRaises(SystemExit)ase:
+199s=io.StringIO()
+200expectedError=(f"{self.USAGE}main.py: error: argument protocol: "
+201"invalid choice: 'arg' (choose from 'irc', 'discord')\n")
+202withcontextlib.redirect_stderr(s):
+203sys.argv=["./main.py","arg"]
+204parseOptions(ConfigParseTest.SAMPLE_CONFIG)
+205self.assertEqual(e.exception.code,2)
+206self.assertEqual(s.getvalue(),expectedError)
+207
+208classTestArgumentParsing(TestCase):
+209"""Test parsing argument to determine whether to launch as irc or discord bot """
+210deftestDetermineDiscordProtocol(self):
+211"""Test that the it's possible to give argument to start the bot as a discord bot"""
+212sys.argv=["main.py","discord"]
+213protocol=determineProtocol()
+214self.assertEqual(protocol,"discord")
+215
+216deftestDetermineIRCProtocol(self):
+217"""Test that the it's possible to give argument to start the bot as an irc bot"""
+218sys.argv=["main.py","irc"]
+219protocol=determineProtocol()
+220self.assertEqual(protocol,"irc")
+221
+222deftestDetermineIRCProtocolisDefault(self):
+223"""Test that if no argument is given, irc is the default"""
+224sys.argv=["main.py"]
+225protocol=determineProtocol()
+226self.assertEqual(protocol,"irc")
+227
+228deftestDetermineConfigThrowsOnInvalidProto(self):
+229"""Test that determineProtocol throws error on unsupported protocols"""
+230sys.argv=["main.py","gopher"]
+231withself.assertRaises(SystemExit)ase:
+232determineProtocol()
+233self.assertEqual(e.exception.code,2)
+234
+235classTestBotFactoryMethod(TestCase):
+236"""Test that createBot returns expected instances of Bots"""
+237deftestCreateIRCBot(self):
+238"""Test that an irc bot can be created"""
+239bot=createBot("irc")
+240self.assertIsInstance(bot,IrcBot)
+241
+242deftestCreateDiscordBot(self):
+243"""Test that a discord bot can be created"""
+244bot=createBot("discord")
+245self.assertIsInstance(bot,DiscordBot)
+246
+247deftestCreateUnsupportedProtocolThrows(self):
+248"""Test that trying to create a bot with an unsupported protocol will throw exception"""
+249withself.assertRaises(ValueError)ase:
+250createBot("gopher")
+251self.assertEqual(str(e.exception),"Unsupported protocol: gopher")
+
+
+
+
+
+
+
+
+ class
+ ConfigMergeTest(unittest.case.TestCase):
+
+
+
+
+
+
21classConfigMergeTest(TestCase):
+22"""Test merging a config file with a dict"""
+23
+24defassertMergedConfig(self,config,fileName,expected):
+25"""Merge dict with file and assert the result matches expected"""
+26configFile=os.path.join("testConfigs",f"{fileName}.json")
+27actualConfig=mergeOptionsWithConfigFile(config,configFile)
+28self.assertEqual(actualConfig,expected)
+29
+30
+31deftestEmpty(self):
+32"""Empty into empty should equal empty"""
+33self.assertMergedConfig({},"empty",{})
+34
+35deftestAddSingleParameter(self):
+36"""Add a single parameter to an empty config"""
+37new={
+38"single":"test"
+39}
+40expected={
+41"single":"test"
+42}
+43self.assertMergedConfig(new,"empty",expected)
+44
+45deftestAddSingleParameterOverwrites(self):
+46"""Add a single parameter to a config that contains it already"""
+47new={
+48"single":"test"
+49}
+50expected={
+51"single":"original"
+52}
+53self.assertMergedConfig(new,"single",expected)
+54
+55deftestAddSingleParameterMerges(self):
+56"""Add a single parameter to a config that contains a different one"""
+57new={
+58"new":"test"
+59}
+60expected={
+61"new":"test",
+62"single":"original"
+63}
+64self.assertMergedConfig(new,"single",expected)
+
24defassertMergedConfig(self,config,fileName,expected):
+25"""Merge dict with file and assert the result matches expected"""
+26configFile=os.path.join("testConfigs",f"{fileName}.json")
+27actualConfig=mergeOptionsWithConfigFile(config,configFile)
+28self.assertEqual(actualConfig,expected)
+
+
+
+
Merge dict with file and assert the result matches expected
+
+
+
+
+
+
+
+
+ def
+ testEmpty(self):
+
+
+
+
+
+
31deftestEmpty(self):
+32"""Empty into empty should equal empty"""
+33self.assertMergedConfig({},"empty",{})
+
+
+
+
Empty into empty should equal empty
+
+
+
+
+
+
+
+
+ def
+ testAddSingleParameter(self):
+
+
+
+
+
+
35deftestAddSingleParameter(self):
+36"""Add a single parameter to an empty config"""
+37new={
+38"single":"test"
+39}
+40expected={
+41"single":"test"
+42}
+43self.assertMergedConfig(new,"empty",expected)
+
45deftestAddSingleParameterOverwrites(self):
+46"""Add a single parameter to a config that contains it already"""
+47new={
+48"single":"test"
+49}
+50expected={
+51"single":"original"
+52}
+53self.assertMergedConfig(new,"single",expected)
+
+
+
+
Add a single parameter to a config that contains it already
55deftestAddSingleParameterMerges(self):
+56"""Add a single parameter to a config that contains a different one"""
+57new={
+58"new":"test"
+59}
+60expected={
+61"new":"test",
+62"single":"original"
+63}
+64self.assertMergedConfig(new,"single",expected)
+
+
+
+
Add a single parameter to a config that contains a different one
+
+
+
+
+
+
Inherited Members
+
+
unittest.case.TestCase
+
TestCase
+
failureException
+
longMessage
+
maxDiff
+
addTypeEqualityFunc
+
addCleanup
+
enterContext
+
addClassCleanup
+
enterClassContext
+
setUp
+
tearDown
+
setUpClass
+
tearDownClass
+
countTestCases
+
defaultTestResult
+
shortDescription
+
id
+
subTest
+
run
+
doCleanups
+
doClassCleanups
+
debug
+
skipTest
+
fail
+
assertFalse
+
assertTrue
+
assertRaises
+
assertWarns
+
assertLogs
+
assertNoLogs
+
assertEqual
+
assertNotEqual
+
assertAlmostEqual
+
assertNotAlmostEqual
+
assertSequenceEqual
+
assertListEqual
+
assertTupleEqual
+
assertSetEqual
+
assertIn
+
assertNotIn
+
assertIs
+
assertIsNot
+
assertDictEqual
+
assertCountEqual
+
assertMultiLineEqual
+
assertLess
+
assertLessEqual
+
assertGreater
+
assertGreaterEqual
+
assertIsNone
+
assertIsNotNone
+
assertIsInstance
+
assertNotIsInstance
+
assertRaisesRegex
+
assertWarnsRegex
+
assertRegex
+
assertNotRegex
+
+
+
+
+
+
+
+
+
+ class
+ ConfigParseTest(unittest.case.TestCase):
+
+
+
+
+
+
66classConfigParseTest(TestCase):
+ 67"""Test parsing options into a config"""
+ 68
+ 69SAMPLE_CONFIG={
+ 70"server":"localhost",
+ 71"port":6667,
+ 72"channel":"#dbwebb",
+ 73"nick":"marvin",
+ 74"realname":"Marvin The All Mighty dbwebb-bot",
+ 75"ident":"password"
+ 76}
+ 77
+ 78CHANGED_CONFIG={
+ 79"server":"remotehost",
+ 80"port":1234,
+ 81"channel":"#db-o-webb",
+ 82"nick":"imposter",
+ 83"realname":"where is marvin?",
+ 84"ident":"identify"
+ 85}
+ 86
+ 87deftestOverrideHardcodedParameters(self):
+ 88"""Test that all the hard coded parameters can be overridden from commandline"""
+ 89forparameterin["server","port","channel","nick","realname","ident"]:
+ 90sys.argv=["./main.py",f"--{parameter}",str(self.CHANGED_CONFIG.get(parameter))]
+ 91actual=parseOptions(self.SAMPLE_CONFIG)
+ 92self.assertEqual(actual.get(parameter),self.CHANGED_CONFIG.get(parameter))
+ 93
+ 94deftestOverrideMultipleParameters(self):
+ 95"""Test that multiple parameters can be overridden from commandline"""
+ 96sys.argv=["./main.py","--server","dbwebb.se","--port","5432"]
+ 97actual=parseOptions(self.SAMPLE_CONFIG)
+ 98self.assertEqual(actual.get("server"),"dbwebb.se")
+ 99self.assertEqual(actual.get("port"),5432)
+100
+101deftestOverrideWithFile(self):
+102"""Test that parameters can be overridden with the --config option"""
+103configFile=os.path.join("testConfigs","server.json")
+104sys.argv=["./main.py","--config",configFile]
+105actual=parseOptions(self.SAMPLE_CONFIG)
+106self.assertEqual(actual.get("server"),"irc.dbwebb.se")
+107
+108deftestOverridePrecedenceConfigFirst(self):
+109"""Test that proper precedence is considered. From most to least significant it should be:
+110 explicit parameter -> parameter in --config file -> default """
+111
+112configFile=os.path.join("testConfigs","server.json")
+113sys.argv=["./main.py","--config",configFile,"--server","important.com"]
+114actual=parseOptions(self.SAMPLE_CONFIG)
+115self.assertEqual(actual.get("server"),"important.com")
+116
+117deftestOverridePrecedenceParameterFirst(self):
+118"""Test that proper precedence is considered. From most to least significant it should be:
+119 explicit parameter -> parameter in --config file -> default """
+120
+121configFile=os.path.join("testConfigs","server.json")
+122sys.argv=["./main.py","--server","important.com","--config",configFile]
+123actual=parseOptions(self.SAMPLE_CONFIG)
+124self.assertEqual(actual.get("server"),"important.com")
+125
+126deftestBannedParameters(self):
+127"""Don't allow config, help and version as parameters, as those options are special"""
+128forbannedParameterin["config","help","version"]:
+129withself.assertRaises(argparse.ArgumentError):
+130parseOptions({bannedParameter:"test"})
+
87deftestOverrideHardcodedParameters(self):
+88"""Test that all the hard coded parameters can be overridden from commandline"""
+89forparameterin["server","port","channel","nick","realname","ident"]:
+90sys.argv=["./main.py",f"--{parameter}",str(self.CHANGED_CONFIG.get(parameter))]
+91actual=parseOptions(self.SAMPLE_CONFIG)
+92self.assertEqual(actual.get(parameter),self.CHANGED_CONFIG.get(parameter))
+
+
+
+
Test that all the hard coded parameters can be overridden from commandline
94deftestOverrideMultipleParameters(self):
+95"""Test that multiple parameters can be overridden from commandline"""
+96sys.argv=["./main.py","--server","dbwebb.se","--port","5432"]
+97actual=parseOptions(self.SAMPLE_CONFIG)
+98self.assertEqual(actual.get("server"),"dbwebb.se")
+99self.assertEqual(actual.get("port"),5432)
+
+
+
+
Test that multiple parameters can be overridden from commandline
+
+
+
+
+
+
+
+
+ def
+ testOverrideWithFile(self):
+
+
+
+
+
+
101deftestOverrideWithFile(self):
+102"""Test that parameters can be overridden with the --config option"""
+103configFile=os.path.join("testConfigs","server.json")
+104sys.argv=["./main.py","--config",configFile]
+105actual=parseOptions(self.SAMPLE_CONFIG)
+106self.assertEqual(actual.get("server"),"irc.dbwebb.se")
+
+
+
+
Test that parameters can be overridden with the --config option
108deftestOverridePrecedenceConfigFirst(self):
+109"""Test that proper precedence is considered. From most to least significant it should be:
+110 explicit parameter -> parameter in --config file -> default """
+111
+112configFile=os.path.join("testConfigs","server.json")
+113sys.argv=["./main.py","--config",configFile,"--server","important.com"]
+114actual=parseOptions(self.SAMPLE_CONFIG)
+115self.assertEqual(actual.get("server"),"important.com")
+
+
+
+
Test that proper precedence is considered. From most to least significant it should be:
+explicit parameter -> parameter in --config file -> default
117deftestOverridePrecedenceParameterFirst(self):
+118"""Test that proper precedence is considered. From most to least significant it should be:
+119 explicit parameter -> parameter in --config file -> default """
+120
+121configFile=os.path.join("testConfigs","server.json")
+122sys.argv=["./main.py","--server","important.com","--config",configFile]
+123actual=parseOptions(self.SAMPLE_CONFIG)
+124self.assertEqual(actual.get("server"),"important.com")
+
+
+
+
Test that proper precedence is considered. From most to least significant it should be:
+explicit parameter -> parameter in --config file -> default
+
+
+
+
+
+
+
+
+ def
+ testBannedParameters(self):
+
+
+
+
+
+
126deftestBannedParameters(self):
+127"""Don't allow config, help and version as parameters, as those options are special"""
+128forbannedParameterin["config","help","version"]:
+129withself.assertRaises(argparse.ArgumentError):
+130parseOptions({bannedParameter:"test"})
+
+
+
+
Don't allow config, help and version as parameters, as those options are special
+
+
+
+
+
+
Inherited Members
+
+
unittest.case.TestCase
+
TestCase
+
failureException
+
longMessage
+
maxDiff
+
addTypeEqualityFunc
+
addCleanup
+
enterContext
+
addClassCleanup
+
enterClassContext
+
setUp
+
tearDown
+
setUpClass
+
tearDownClass
+
countTestCases
+
defaultTestResult
+
shortDescription
+
id
+
subTest
+
run
+
doCleanups
+
doClassCleanups
+
debug
+
skipTest
+
fail
+
assertFalse
+
assertTrue
+
assertRaises
+
assertWarns
+
assertLogs
+
assertNoLogs
+
assertEqual
+
assertNotEqual
+
assertAlmostEqual
+
assertNotAlmostEqual
+
assertSequenceEqual
+
assertListEqual
+
assertTupleEqual
+
assertSetEqual
+
assertIn
+
assertNotIn
+
assertIs
+
assertIsNot
+
assertDictEqual
+
assertCountEqual
+
assertMultiLineEqual
+
assertLess
+
assertLessEqual
+
assertGreater
+
assertGreaterEqual
+
assertIsNone
+
assertIsNotNone
+
assertIsInstance
+
assertNotIsInstance
+
assertRaisesRegex
+
assertWarnsRegex
+
assertRegex
+
assertNotRegex
+
+
+
+
+
+
+
+
+
+ class
+ FormattingTest(unittest.case.TestCase):
+
+
+
+
+
+
133classFormattingTest(TestCase):
+134"""Test the parameters that cause printouts"""
+135
+136USAGE=("usage: main.py [-h] [-v] [--config CONFIG] [--server SERVER] [--port PORT] "
+137"[--channel CHANNEL] [--nick NICK] [--realname REALNAME] [--ident IDENT]\n"
+138" [{irc,discord}]\n")
+139
+140OPTIONS=("positional arguments:\n {irc,discord}\n\n"
+141"options:\n"
+142" -h, --help show this help message and exit\n"
+143" -v, --version\n"
+144" --config CONFIG\n"
+145" --server SERVER\n"
+146" --port PORT\n"
+147" --channel CHANNEL\n"
+148" --nick NICK\n"
+149" --realname REALNAME\n"
+150" --ident IDENT")
+151
+152
+153@classmethod
+154defsetUpClass(cls):
+155"""Set the terminal width to 160 to prevent the tests from failing on small terminals"""
+156os.environ["COLUMNS"]="160"
+157
+158
+159defassertPrintOption(self,options,returnCode,output):
+160"""Assert that parseOptions returns a certain code and prints a certain output"""
+161withself.assertRaises(SystemExit)ase:
+162s=io.StringIO()
+163withcontextlib.redirect_stdout(s):
+164sys.argv=["./main.py"]+[options]
+165parseOptions(ConfigParseTest.SAMPLE_CONFIG)
+166self.assertEqual(e.exception.code,returnCode)
+167self.assertEqual(s.getvalue(),output+"\n")# extra newline added by print()
+168
+169
+170deftestHelpPrintout(self):
+171"""Test that a help is printed when providing the --help flag"""
+172self.assertPrintOption("--help",0,f"{self.USAGE}\n{self.OPTIONS}")
+173
+174deftestHelpPrintoutShort(self):
+175"""Test that a help is printed when providing the -h flag"""
+176self.assertPrintOption("-h",0,f"{self.USAGE}\n{self.OPTIONS}")
+177
+178deftestVersionPrintout(self):
+179"""Test that the version is printed when provided the --version flag"""
+180self.assertPrintOption("--version",0,MSG_VERSION)
+181
+182deftestVersionPrintoutShort(self):
+183"""Test that the version is printed when provided the -v flag"""
+184self.assertPrintOption("-v",0,MSG_VERSION)
+185
+186deftestUnhandledOption(self):
+187"""Test that unknown options gives an error"""
+188withself.assertRaises(SystemExit)ase:
+189s=io.StringIO()
+190expectedError=f"{self.USAGE}main.py: error: unrecognized arguments: -g\n"
+191withcontextlib.redirect_stderr(s):
+192sys.argv=["./main.py","-g"]
+193parseOptions(ConfigParseTest.SAMPLE_CONFIG)
+194self.assertEqual(e.exception.code,2)
+195self.assertEqual(s.getvalue(),expectedError)
+196
+197deftestUnhandledArgument(self):
+198"""Test that any argument gives an error"""
+199withself.assertRaises(SystemExit)ase:
+200s=io.StringIO()
+201expectedError=(f"{self.USAGE}main.py: error: argument protocol: "
+202"invalid choice: 'arg' (choose from 'irc', 'discord')\n")
+203withcontextlib.redirect_stderr(s):
+204sys.argv=["./main.py","arg"]
+205parseOptions(ConfigParseTest.SAMPLE_CONFIG)
+206self.assertEqual(e.exception.code,2)
+207self.assertEqual(s.getvalue(),expectedError)
+
+ OPTIONS =
+
+ 'positional arguments:\n {irc,discord}\n\noptions:\n -h, --help show this help message and exit\n -v, --version\n --config CONFIG\n --server SERVER\n --port PORT\n --channel CHANNEL\n --nick NICK\n --realname REALNAME\n --ident IDENT'
+
+
+
+
+
+
+
+
+
+
+
+
@classmethod
+
+ def
+ setUpClass(cls):
+
+
+
+
+
+
153@classmethod
+154defsetUpClass(cls):
+155"""Set the terminal width to 160 to prevent the tests from failing on small terminals"""
+156os.environ["COLUMNS"]="160"
+
+
+
+
Set the terminal width to 160 to prevent the tests from failing on small terminals
159defassertPrintOption(self,options,returnCode,output):
+160"""Assert that parseOptions returns a certain code and prints a certain output"""
+161withself.assertRaises(SystemExit)ase:
+162s=io.StringIO()
+163withcontextlib.redirect_stdout(s):
+164sys.argv=["./main.py"]+[options]
+165parseOptions(ConfigParseTest.SAMPLE_CONFIG)
+166self.assertEqual(e.exception.code,returnCode)
+167self.assertEqual(s.getvalue(),output+"\n")# extra newline added by print()
+
+
+
+
Assert that parseOptions returns a certain code and prints a certain output
+
+
+
+
+
+
+
+
+ def
+ testHelpPrintout(self):
+
+
+
+
+
+
170deftestHelpPrintout(self):
+171"""Test that a help is printed when providing the --help flag"""
+172self.assertPrintOption("--help",0,f"{self.USAGE}\n{self.OPTIONS}")
+
+
+
+
Test that a help is printed when providing the --help flag
+
+
+
+
+
+
+
+
+ def
+ testHelpPrintoutShort(self):
+
+
+
+
+
+
174deftestHelpPrintoutShort(self):
+175"""Test that a help is printed when providing the -h flag"""
+176self.assertPrintOption("-h",0,f"{self.USAGE}\n{self.OPTIONS}")
+
+
+
+
Test that a help is printed when providing the -h flag
+
+
+
+
+
+
+
+
+ def
+ testVersionPrintout(self):
+
+
+
+
+
+
178deftestVersionPrintout(self):
+179"""Test that the version is printed when provided the --version flag"""
+180self.assertPrintOption("--version",0,MSG_VERSION)
+
+
+
+
Test that the version is printed when provided the --version flag
+
+
+
+
+
+
+
+
+ def
+ testVersionPrintoutShort(self):
+
+
+
+
+
+
182deftestVersionPrintoutShort(self):
+183"""Test that the version is printed when provided the -v flag"""
+184self.assertPrintOption("-v",0,MSG_VERSION)
+
+
+
+
Test that the version is printed when provided the -v flag
197deftestUnhandledArgument(self):
+198"""Test that any argument gives an error"""
+199withself.assertRaises(SystemExit)ase:
+200s=io.StringIO()
+201expectedError=(f"{self.USAGE}main.py: error: argument protocol: "
+202"invalid choice: 'arg' (choose from 'irc', 'discord')\n")
+203withcontextlib.redirect_stderr(s):
+204sys.argv=["./main.py","arg"]
+205parseOptions(ConfigParseTest.SAMPLE_CONFIG)
+206self.assertEqual(e.exception.code,2)
+207self.assertEqual(s.getvalue(),expectedError)
+
+
+
+
Test that any argument gives an error
+
+
+
+
+
+
Inherited Members
+
+
unittest.case.TestCase
+
TestCase
+
failureException
+
longMessage
+
maxDiff
+
addTypeEqualityFunc
+
addCleanup
+
enterContext
+
addClassCleanup
+
enterClassContext
+
setUp
+
tearDown
+
tearDownClass
+
countTestCases
+
defaultTestResult
+
shortDescription
+
id
+
subTest
+
run
+
doCleanups
+
doClassCleanups
+
debug
+
skipTest
+
fail
+
assertFalse
+
assertTrue
+
assertRaises
+
assertWarns
+
assertLogs
+
assertNoLogs
+
assertEqual
+
assertNotEqual
+
assertAlmostEqual
+
assertNotAlmostEqual
+
assertSequenceEqual
+
assertListEqual
+
assertTupleEqual
+
assertSetEqual
+
assertIn
+
assertNotIn
+
assertIs
+
assertIsNot
+
assertDictEqual
+
assertCountEqual
+
assertMultiLineEqual
+
assertLess
+
assertLessEqual
+
assertGreater
+
assertGreaterEqual
+
assertIsNone
+
assertIsNotNone
+
assertIsInstance
+
assertNotIsInstance
+
assertRaisesRegex
+
assertWarnsRegex
+
assertRegex
+
assertNotRegex
+
+
+
+
+
+
+
+
+
+ class
+ TestArgumentParsing(unittest.case.TestCase):
+
+
+
+
+
+
209classTestArgumentParsing(TestCase):
+210"""Test parsing argument to determine whether to launch as irc or discord bot """
+211deftestDetermineDiscordProtocol(self):
+212"""Test that the it's possible to give argument to start the bot as a discord bot"""
+213sys.argv=["main.py","discord"]
+214protocol=determineProtocol()
+215self.assertEqual(protocol,"discord")
+216
+217deftestDetermineIRCProtocol(self):
+218"""Test that the it's possible to give argument to start the bot as an irc bot"""
+219sys.argv=["main.py","irc"]
+220protocol=determineProtocol()
+221self.assertEqual(protocol,"irc")
+222
+223deftestDetermineIRCProtocolisDefault(self):
+224"""Test that if no argument is given, irc is the default"""
+225sys.argv=["main.py"]
+226protocol=determineProtocol()
+227self.assertEqual(protocol,"irc")
+228
+229deftestDetermineConfigThrowsOnInvalidProto(self):
+230"""Test that determineProtocol throws error on unsupported protocols"""
+231sys.argv=["main.py","gopher"]
+232withself.assertRaises(SystemExit)ase:
+233determineProtocol()
+234self.assertEqual(e.exception.code,2)
+
+
+
+
Test parsing argument to determine whether to launch as irc or discord bot
211deftestDetermineDiscordProtocol(self):
+212"""Test that the it's possible to give argument to start the bot as a discord bot"""
+213sys.argv=["main.py","discord"]
+214protocol=determineProtocol()
+215self.assertEqual(protocol,"discord")
+
+
+
+
Test that the it's possible to give argument to start the bot as a discord bot
+
+
+
+
+
+
+
+
+ def
+ testDetermineIRCProtocol(self):
+
+
+
+
+
+
217deftestDetermineIRCProtocol(self):
+218"""Test that the it's possible to give argument to start the bot as an irc bot"""
+219sys.argv=["main.py","irc"]
+220protocol=determineProtocol()
+221self.assertEqual(protocol,"irc")
+
+
+
+
Test that the it's possible to give argument to start the bot as an irc bot
223deftestDetermineIRCProtocolisDefault(self):
+224"""Test that if no argument is given, irc is the default"""
+225sys.argv=["main.py"]
+226protocol=determineProtocol()
+227self.assertEqual(protocol,"irc")
+
+
+
+
Test that if no argument is given, irc is the default
229deftestDetermineConfigThrowsOnInvalidProto(self):
+230"""Test that determineProtocol throws error on unsupported protocols"""
+231sys.argv=["main.py","gopher"]
+232withself.assertRaises(SystemExit)ase:
+233determineProtocol()
+234self.assertEqual(e.exception.code,2)
+
+
+
+
Test that determineProtocol throws error on unsupported protocols
+
+
+
+
+
+
Inherited Members
+
+
unittest.case.TestCase
+
TestCase
+
failureException
+
longMessage
+
maxDiff
+
addTypeEqualityFunc
+
addCleanup
+
enterContext
+
addClassCleanup
+
enterClassContext
+
setUp
+
tearDown
+
setUpClass
+
tearDownClass
+
countTestCases
+
defaultTestResult
+
shortDescription
+
id
+
subTest
+
run
+
doCleanups
+
doClassCleanups
+
debug
+
skipTest
+
fail
+
assertFalse
+
assertTrue
+
assertRaises
+
assertWarns
+
assertLogs
+
assertNoLogs
+
assertEqual
+
assertNotEqual
+
assertAlmostEqual
+
assertNotAlmostEqual
+
assertSequenceEqual
+
assertListEqual
+
assertTupleEqual
+
assertSetEqual
+
assertIn
+
assertNotIn
+
assertIs
+
assertIsNot
+
assertDictEqual
+
assertCountEqual
+
assertMultiLineEqual
+
assertLess
+
assertLessEqual
+
assertGreater
+
assertGreaterEqual
+
assertIsNone
+
assertIsNotNone
+
assertIsInstance
+
assertNotIsInstance
+
assertRaisesRegex
+
assertWarnsRegex
+
assertRegex
+
assertNotRegex
+
+
+
+
+
+
+
+
+
+ class
+ TestBotFactoryMethod(unittest.case.TestCase):
+
+
+
+
+
+
236classTestBotFactoryMethod(TestCase):
+237"""Test that createBot returns expected instances of Bots"""
+238deftestCreateIRCBot(self):
+239"""Test that an irc bot can be created"""
+240bot=createBot("irc")
+241self.assertIsInstance(bot,IrcBot)
+242
+243deftestCreateDiscordBot(self):
+244"""Test that a discord bot can be created"""
+245bot=createBot("discord")
+246self.assertIsInstance(bot,DiscordBot)
+247
+248deftestCreateUnsupportedProtocolThrows(self):
+249"""Test that trying to create a bot with an unsupported protocol will throw exception"""
+250withself.assertRaises(ValueError)ase:
+251createBot("gopher")
+252self.assertEqual(str(e.exception),"Unsupported protocol: gopher")
+
+
+
+
Test that createBot returns expected instances of Bots
+
+
+
+
+
+
+
+ def
+ testCreateIRCBot(self):
+
+
+
+
+
+
238deftestCreateIRCBot(self):
+239"""Test that an irc bot can be created"""
+240bot=createBot("irc")
+241self.assertIsInstance(bot,IrcBot)
+
+
+
+
Test that an irc bot can be created
+
+
+
+
+
+
+
+
+ def
+ testCreateDiscordBot(self):
+
+
+
+
+
+
243deftestCreateDiscordBot(self):
+244"""Test that a discord bot can be created"""
+245bot=createBot("discord")
+246self.assertIsInstance(bot,DiscordBot)
+
248deftestCreateUnsupportedProtocolThrows(self):
+249"""Test that trying to create a bot with an unsupported protocol will throw exception"""
+250withself.assertRaises(ValueError)ase:
+251createBot("gopher")
+252self.assertEqual(str(e.exception),"Unsupported protocol: gopher")
+
+
+
+
Test that trying to create a bot with an unsupported protocol will throw exception
+
+
+
+
+
+
Inherited Members
+
+
unittest.case.TestCase
+
TestCase
+
failureException
+
longMessage
+
maxDiff
+
addTypeEqualityFunc
+
addCleanup
+
enterContext
+
addClassCleanup
+
enterClassContext
+
setUp
+
tearDown
+
setUpClass
+
tearDownClass
+
countTestCases
+
defaultTestResult
+
shortDescription
+
id
+
subTest
+
run
+
doCleanups
+
doClassCleanups
+
debug
+
skipTest
+
fail
+
assertFalse
+
assertTrue
+
assertRaises
+
assertWarns
+
assertLogs
+
assertNoLogs
+
assertEqual
+
assertNotEqual
+
assertAlmostEqual
+
assertNotAlmostEqual
+
assertSequenceEqual
+
assertListEqual
+
assertTupleEqual
+
assertSetEqual
+
assertIn
+
assertNotIn
+
assertIs
+
assertIsNot
+
assertDictEqual
+
assertCountEqual
+
assertMultiLineEqual
+
assertLess
+
assertLessEqual
+
assertGreater
+
assertGreaterEqual
+
assertIsNone
+
assertIsNotNone
+
assertIsInstance
+
assertNotIsInstance
+
assertRaisesRegex
+
assertWarnsRegex
+
assertRegex
+
assertNotRegex
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/pdoc/test_marvin_actions.html b/docs/pdoc/test_marvin_actions.html
new file mode 100644
index 0000000..b02ab7b
--- /dev/null
+++ b/docs/pdoc/test_marvin_actions.html
@@ -0,0 +1,1976 @@
+
+
+
+
+
+
+ test_marvin_actions API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+test_marvin_actions
+
+
Tests for all Marvin actions
+
+
+
+
+
+
+
1#! /usr/bin/env python3
+ 2# -*- coding: utf-8 -*-
+ 3
+ 4"""
+ 5Tests for all Marvin actions
+ 6"""
+ 7
+ 8importjson
+ 9
+ 10fromdatetimeimportdate
+ 11fromunittestimportmock,TestCase
+ 12
+ 13importrequests
+ 14
+ 15frombotimportBot
+ 16importmarvin_actions
+ 17importmarvin_general_actions
+ 18
+ 19classActionTest(TestCase):
+ 20"""Test Marvin actions"""
+ 21strings={}
+ 22
+ 23@classmethod
+ 24defsetUpClass(cls):
+ 25withopen("marvin_strings.json",encoding="utf-8")asf:
+ 26cls.strings=json.load(f)
+ 27
+ 28
+ 29defexecuteAction(self,action,message):
+ 30"""Execute an action for a message and return the response"""
+ 31returnaction(Bot.tokenize(message))
+ 32
+ 33
+ 34defassertActionOutput(self,action,message,expectedOutput):
+ 35"""Call an action on message and assert expected output"""
+ 36actualOutput=self.executeAction(action,message)
+ 37
+ 38self.assertEqual(actualOutput,expectedOutput)
+ 39
+ 40
+ 41defassertActionSilent(self,action,message):
+ 42"""Call an action with provided message and assert no output"""
+ 43self.assertActionOutput(action,message,None)
+ 44
+ 45
+ 46defassertStringsOutput(self,action,message,expectedoutputKey,subkey=None):
+ 47"""Call an action with provided message and assert the output is equal to DB"""
+ 48expectedOutput=self.strings.get(expectedoutputKey)
+ 49ifsubkeyisnotNone:
+ 50ifisinstance(expectedOutput,list):
+ 51expectedOutput=expectedOutput[subkey]
+ 52else:
+ 53expectedOutput=expectedOutput.get(subkey)
+ 54self.assertActionOutput(action,message,expectedOutput)
+ 55
+ 56
+ 57defassertBBQResponse(self,todaysDate,bbqDate,expectedMessageKey):
+ 58"""Assert that the proper bbq message is returned, given a date"""
+ 59url=self.strings.get("barbecue").get("url")
+ 60message=self.strings.get("barbecue").get(expectedMessageKey)
+ 61ifisinstance(message,list):
+ 62message=message[1]
+ 63ifexpectedMessageKeyin["base","week","eternity"]:
+ 64message=message%bbqDate
+ 65
+ 66withmock.patch("marvin_actions.datetime")asd:
+ 67d.date.today.return_value=todaysDate
+ 68withmock.patch("marvin_actions.random")asr:
+ 69r.randint.return_value=1
+ 70expected=f"{url}. {message}"
+ 71self.assertActionOutput(marvin_actions.marvinTimeToBBQ,"dags att grilla",expected)
+ 72
+ 73
+ 74defassertNameDayOutput(self,exampleFile,expectedOutput):
+ 75"""Assert that the proper nameday message is returned, given an inputfile"""
+ 76withopen(f"namedayFiles/{exampleFile}.json","r",encoding="UTF-8")asf:
+ 77response=requests.models.Response()
+ 78response._content=str.encode(json.dumps(json.load(f)))
+ 79withmock.patch("marvin_actions.requests")asr:
+ 80r.get.return_value=response
+ 81self.assertActionOutput(marvin_actions.marvinNameday,"nameday",expectedOutput)
+ 82
+ 83defassertJokeOutput(self,exampleFile,expectedOutput):
+ 84"""Assert that a joke is returned, given an input file"""
+ 85withopen(f"jokeFiles/{exampleFile}.json","r",encoding="UTF-8")asf:
+ 86response=requests.models.Response()
+ 87response._content=str.encode(json.dumps(json.load(f)))
+ 88withmock.patch("marvin_actions.requests")asr:
+ 89r.get.return_value=response
+ 90self.assertActionOutput(marvin_actions.marvinJoke,"joke",expectedOutput)
+ 91
+ 92deftestSmile(self):
+ 93"""Test that marvin can smile"""
+ 94withmock.patch("marvin_actions.random")asr:
+ 95r.randint.return_value=1
+ 96self.assertStringsOutput(marvin_actions.marvinSmile,"le lite?","smile",1)
+ 97self.assertActionSilent(marvin_actions.marvinSmile,"sur idag?")
+ 98
+ 99deftestWhois(self):
+100"""Test that marvin responds to whois"""
+101self.assertStringsOutput(marvin_actions.marvinWhoIs,"vem är marvin?","whois")
+102self.assertActionSilent(marvin_actions.marvinWhoIs,"vemär")
+103
+104deftestGoogle(self):
+105"""Test that marvin can help google stuff"""
+106withmock.patch("marvin_actions.random")asr:
+107r.randint.return_value=1
+108self.assertActionOutput(
+109marvin_actions.marvinGoogle,
+110"kan du googla mos",
+111"LMGTFY https://www.google.se/search?q=mos")
+112self.assertActionOutput(
+113marvin_actions.marvinGoogle,
+114"kan du googla google mos",
+115"LMGTFY https://www.google.se/search?q=google+mos")
+116self.assertActionSilent(marvin_actions.marvinGoogle,"du kan googla")
+117self.assertActionSilent(marvin_actions.marvinGoogle,"gogool")
+118
+119deftestExplainShell(self):
+120"""Test that marvin can explain shell commands"""
+121url="http://explainshell.com/explain?cmd=pwd"
+122self.assertActionOutput(marvin_actions.marvinExplainShell,"explain pwd",url)
+123self.assertActionOutput(marvin_actions.marvinExplainShell,"can you explain pwd",url)
+124self.assertActionOutput(
+125marvin_actions.marvinExplainShell,
+126"förklara pwd|grep -o $user",
+127f"{url}%7Cgrep+-o+%24user")
+128
+129self.assertActionSilent(marvin_actions.marvinExplainShell,"explains")
+130
+131deftestSource(self):
+132"""Test that marvin responds to questions about source code"""
+133self.assertStringsOutput(marvin_actions.marvinSource,"source","source")
+134self.assertStringsOutput(marvin_actions.marvinSource,"källkod","source")
+135self.assertActionSilent(marvin_actions.marvinSource,"opensource")
+136
+137deftestBudord(self):
+138"""Test that marvin knows all the commandments"""
+139forninrange(1,5):
+140self.assertStringsOutput(marvin_actions.marvinBudord,f"budord #{n}","budord",f"#{n}")
+141
+142self.assertStringsOutput(marvin_actions.marvinBudord,"visa stentavla 1","budord","#1")
+143self.assertActionSilent(marvin_actions.marvinBudord,"var är stentavlan?")
+144
+145deftestQuote(self):
+146"""Test that marvin can quote The Hitchhikers Guide to the Galaxy"""
+147withmock.patch("marvin_actions.random")asr:
+148r.randint.return_value=1
+149self.assertStringsOutput(marvin_actions.marvinQuote,"ge os ett citat","hitchhiker",1)
+150self.assertStringsOutput(marvin_actions.marvinQuote,"filosofi","hitchhiker",1)
+151self.assertStringsOutput(marvin_actions.marvinQuote,"filosofera","hitchhiker",1)
+152self.assertActionSilent(marvin_actions.marvinQuote,"noquote")
+153
+154fori,_inenumerate(self.strings.get("hitchhiker")):
+155r.randint.return_value=i
+156self.assertStringsOutput(marvin_actions.marvinQuote,"quote","hitchhiker",i)
+157
+158deftestVideoOfToday(self):
+159"""Test that marvin can link to a different video each day of the week"""
+160withmock.patch("marvin_actions.datetime")asdt:
+161fordinrange(1,8):
+162dt.date.weekday.return_value=d-1
+163day=self.strings.get("weekdays").get(str(d))
+164video=self.strings.get("video-of-today").get(str(d))
+165response=f"{day} En passande video är {video}"
+166self.assertActionOutput(marvin_actions.marvinVideoOfToday,"dagens video",response)
+167self.assertActionSilent(marvin_actions.marvinVideoOfToday,"videoidag")
+168
+169deftestHelp(self):
+170"""Test that marvin can provide a help menu"""
+171self.assertStringsOutput(marvin_actions.marvinHelp,"help","menu")
+172self.assertActionSilent(marvin_actions.marvinHelp,"halp")
+173
+174deftestStats(self):
+175"""Test that marvin can provide a link to the IRC stats page"""
+176self.assertStringsOutput(marvin_actions.marvinStats,"stats","ircstats")
+177self.assertActionSilent(marvin_actions.marvinStats,"statistics")
+178
+179deftestIRCLog(self):
+180"""Test that marvin can provide a link to the IRC log"""
+181self.assertStringsOutput(marvin_actions.marvinIrcLog,"irc","irclog")
+182self.assertActionSilent(marvin_actions.marvinIrcLog,"ircstats")
+183
+184deftestSayHi(self):
+185"""Test that marvin responds to greetings"""
+186withmock.patch("marvin_actions.random")asr:
+187forskey,sinenumerate(self.strings.get("smile")):
+188forhkey,hinenumerate(self.strings.get("hello")):
+189forfkey,finenumerate(self.strings.get("friendly")):
+190r.randint.side_effect=[skey,hkey,fkey]
+191self.assertActionOutput(marvin_actions.marvinSayHi,"hej",f"{s}{h}{f}")
+192self.assertActionSilent(marvin_actions.marvinSayHi,"korsning")
+193
+194deftestLunchLocations(self):
+195"""Test that marvin can provide lunch suggestions for certain places"""
+196locations=["karlskrona","goteborg","angelholm","hassleholm","malmo"]
+197withmock.patch("marvin_actions.random")asr:
+198forlocationinlocations:
+199forindex,placeinenumerate(self.strings.get(f"lunch-{location}")):
+200r.randint.side_effect=[0,index]
+201self.assertActionOutput(
+202marvin_actions.marvinLunch,f"mat {location}",f"Ska vi ta {place}?")
+203r.randint.side_effect=[1,2]
+204self.assertActionOutput(
+205marvin_actions.marvinLunch,"dags att luncha","Jag är lite sugen på Indiska?")
+206self.assertActionSilent(marvin_actions.marvinLunch,"matdags")
+207
+208deftestStrip(self):
+209"""Test that marvin can recommend comics"""
+210messageFormat=self.strings.get("commitstrip").get("message")
+211expected=messageFormat.format(url=self.strings.get("commitstrip").get("url"))
+212self.assertActionOutput(marvin_actions.marvinStrip,"lite strip kanske?",expected)
+213self.assertActionSilent(marvin_actions.marvinStrip,"nostrip")
+214
+215deftestRandomStrip(self):
+216"""Test that marvin can recommend random comics"""
+217messageFormat=self.strings.get("commitstrip").get("message")
+218expected=messageFormat.format(url=self.strings.get("commitstrip").get("urlPage")+"123")
+219withmock.patch("marvin_actions.random")asr:
+220r.randint.return_value=123
+221self.assertActionOutput(marvin_actions.marvinStrip,"random strip kanske?",expected)
+222
+223deftestTimeToBBQ(self):
+224"""Test that marvin knows when the next BBQ is"""
+225self.assertBBQResponse(date(2024,5,17),date(2024,5,17),"today")
+226self.assertBBQResponse(date(2024,5,16),date(2024,5,17),"tomorrow")
+227self.assertBBQResponse(date(2024,5,10),date(2024,5,17),"week")
+228self.assertBBQResponse(date(2024,5,1),date(2024,5,17),"base")
+229self.assertBBQResponse(date(2023,10,17),date(2024,5,17),"eternity")
+230
+231self.assertBBQResponse(date(2024,9,20),date(2024,9,20),"today")
+232self.assertBBQResponse(date(2024,9,19),date(2024,9,20),"tomorrow")
+233self.assertBBQResponse(date(2024,9,13),date(2024,9,20),"week")
+234self.assertBBQResponse(date(2024,9,4),date(2024,9,20),"base")
+235
+236deftestNameDayReaction(self):
+237"""Test that marvin only responds to nameday when asked"""
+238self.assertActionSilent(marvin_actions.marvinNameday,"anything")
+239
+240deftestNameDayRequest(self):
+241"""Test that marvin sends a proper request for nameday info"""
+242withmock.patch("marvin_actions.requests")asr:
+243withmock.patch("marvin_actions.datetime")asd:
+244d.datetime.now.return_value=date(2024,1,2)
+245self.executeAction(marvin_actions.marvinNameday,"namnsdag")
+246self.assertEqual(r.get.call_args.args[0],"http://api.dryg.net/dagar/v2.1/2024/1/2")
+247
+248deftestNameDayResponse(self):
+249"""Test that marvin properly parses nameday responses"""
+250self.assertNameDayOutput("single","Idag har Svea namnsdag")
+251self.assertNameDayOutput("double","Idag har Alfred,Alfrida namnsdag")
+252self.assertNameDayOutput("nobody","Ingen har namnsdag idag")
+253
+254deftestJokeRequest(self):
+255"""Test that marvin sends a proper request for a joke"""
+256withmock.patch("marvin_actions.requests")asr:
+257self.executeAction(marvin_actions.marvinJoke,"joke")
+258self.assertEqual(r.get.call_args.args[0],"https://api.chucknorris.io/jokes/random?category=dev")
+259
+260deftestJoke(self):
+261"""Test that marvin sends a joke when requested"""
+262self.assertJokeOutput("joke","There is no Esc key on Chuck Norris' keyboard, because no one escapes Chuck Norris.")
+263
+264deftestUptime(self):
+265"""Test that marvin can provide the link to the uptime tournament"""
+266self.assertStringsOutput(marvin_actions.marvinUptime,"visa lite uptime","uptime","info")
+267self.assertActionSilent(marvin_actions.marvinUptime,"uptimetävling")
+268
+269deftestStream(self):
+270"""Test that marvin can provide the link to the stream"""
+271self.assertStringsOutput(marvin_actions.marvinStream,"ska mos streama?","stream","info")
+272self.assertActionSilent(marvin_actions.marvinStream,"är mos en streamer?")
+273
+274deftestPrinciple(self):
+275"""Test that marvin can recite some software principles"""
+276principles=self.strings.get("principle")
+277forkey,valueinprinciples.items():
+278self.assertActionOutput(marvin_actions.marvinPrinciple,f"princip {key}",value)
+279withmock.patch("marvin_actions.random")asr:
+280r.choice.return_value="dry"
+281self.assertStringsOutput(marvin_actions.marvinPrinciple,"princip","principle","dry")
+282self.assertActionSilent(marvin_actions.marvinPrinciple,"principlös")
+283
+284deftestCommitRequest(self):
+285"""Test that marvin sends proper requests when generating commit messages"""
+286withmock.patch("marvin_actions.requests")asr:
+287self.executeAction(marvin_actions.marvinCommit,"vad skriver man efter commit -m?")
+288self.assertEqual(r.get.call_args.args[0],"http://whatthecommit.com/index.txt")
+289
+290deftestCommitResponse(self):
+291"""Test that marvin properly handles responses when generating commit messages"""
+292message="Secret sauce #9"
+293response=requests.models.Response()
+294response._content=str.encode(message)
+295withmock.patch("marvin_actions.requests")asr:
+296r.get.return_value=response
+297expected=f"Använd detta meddelandet: '{message}'"
+298self.assertActionOutput(marvin_actions.marvinCommit,"commit",expected)
+299
+300deftestMorning(self):
+301"""Test that marvin wishes good morning, at most once per day"""
+302marvin_general_actions.lastDateGreeted=None
+303withmock.patch("marvin_general_actions.datetime")asd:
+304d.date.today.return_value=date(2024,5,17)
+305withmock.patch("marvin_general_actions.random")asr:
+306r.choice.return_value="Morgon"
+307self.assertActionOutput(marvin_general_actions.marvinMorning,"morrn","Morgon")
+308# Should only greet once per day
+309self.assertActionSilent(marvin_general_actions.marvinMorning,"morgon")
+310# Should greet again tomorrow
+311d.date.today.return_value=date(2024,5,18)
+312self.assertActionOutput(marvin_general_actions.marvinMorning,"godmorgon","Morgon")
+
+
+
+
+
+
+
+
+ class
+ ActionTest(unittest.case.TestCase):
+
+
+
+
+
+
20classActionTest(TestCase):
+ 21"""Test Marvin actions"""
+ 22strings={}
+ 23
+ 24@classmethod
+ 25defsetUpClass(cls):
+ 26withopen("marvin_strings.json",encoding="utf-8")asf:
+ 27cls.strings=json.load(f)
+ 28
+ 29
+ 30defexecuteAction(self,action,message):
+ 31"""Execute an action for a message and return the response"""
+ 32returnaction(Bot.tokenize(message))
+ 33
+ 34
+ 35defassertActionOutput(self,action,message,expectedOutput):
+ 36"""Call an action on message and assert expected output"""
+ 37actualOutput=self.executeAction(action,message)
+ 38
+ 39self.assertEqual(actualOutput,expectedOutput)
+ 40
+ 41
+ 42defassertActionSilent(self,action,message):
+ 43"""Call an action with provided message and assert no output"""
+ 44self.assertActionOutput(action,message,None)
+ 45
+ 46
+ 47defassertStringsOutput(self,action,message,expectedoutputKey,subkey=None):
+ 48"""Call an action with provided message and assert the output is equal to DB"""
+ 49expectedOutput=self.strings.get(expectedoutputKey)
+ 50ifsubkeyisnotNone:
+ 51ifisinstance(expectedOutput,list):
+ 52expectedOutput=expectedOutput[subkey]
+ 53else:
+ 54expectedOutput=expectedOutput.get(subkey)
+ 55self.assertActionOutput(action,message,expectedOutput)
+ 56
+ 57
+ 58defassertBBQResponse(self,todaysDate,bbqDate,expectedMessageKey):
+ 59"""Assert that the proper bbq message is returned, given a date"""
+ 60url=self.strings.get("barbecue").get("url")
+ 61message=self.strings.get("barbecue").get(expectedMessageKey)
+ 62ifisinstance(message,list):
+ 63message=message[1]
+ 64ifexpectedMessageKeyin["base","week","eternity"]:
+ 65message=message%bbqDate
+ 66
+ 67withmock.patch("marvin_actions.datetime")asd:
+ 68d.date.today.return_value=todaysDate
+ 69withmock.patch("marvin_actions.random")asr:
+ 70r.randint.return_value=1
+ 71expected=f"{url}. {message}"
+ 72self.assertActionOutput(marvin_actions.marvinTimeToBBQ,"dags att grilla",expected)
+ 73
+ 74
+ 75defassertNameDayOutput(self,exampleFile,expectedOutput):
+ 76"""Assert that the proper nameday message is returned, given an inputfile"""
+ 77withopen(f"namedayFiles/{exampleFile}.json","r",encoding="UTF-8")asf:
+ 78response=requests.models.Response()
+ 79response._content=str.encode(json.dumps(json.load(f)))
+ 80withmock.patch("marvin_actions.requests")asr:
+ 81r.get.return_value=response
+ 82self.assertActionOutput(marvin_actions.marvinNameday,"nameday",expectedOutput)
+ 83
+ 84defassertJokeOutput(self,exampleFile,expectedOutput):
+ 85"""Assert that a joke is returned, given an input file"""
+ 86withopen(f"jokeFiles/{exampleFile}.json","r",encoding="UTF-8")asf:
+ 87response=requests.models.Response()
+ 88response._content=str.encode(json.dumps(json.load(f)))
+ 89withmock.patch("marvin_actions.requests")asr:
+ 90r.get.return_value=response
+ 91self.assertActionOutput(marvin_actions.marvinJoke,"joke",expectedOutput)
+ 92
+ 93deftestSmile(self):
+ 94"""Test that marvin can smile"""
+ 95withmock.patch("marvin_actions.random")asr:
+ 96r.randint.return_value=1
+ 97self.assertStringsOutput(marvin_actions.marvinSmile,"le lite?","smile",1)
+ 98self.assertActionSilent(marvin_actions.marvinSmile,"sur idag?")
+ 99
+100deftestWhois(self):
+101"""Test that marvin responds to whois"""
+102self.assertStringsOutput(marvin_actions.marvinWhoIs,"vem är marvin?","whois")
+103self.assertActionSilent(marvin_actions.marvinWhoIs,"vemär")
+104
+105deftestGoogle(self):
+106"""Test that marvin can help google stuff"""
+107withmock.patch("marvin_actions.random")asr:
+108r.randint.return_value=1
+109self.assertActionOutput(
+110marvin_actions.marvinGoogle,
+111"kan du googla mos",
+112"LMGTFY https://www.google.se/search?q=mos")
+113self.assertActionOutput(
+114marvin_actions.marvinGoogle,
+115"kan du googla google mos",
+116"LMGTFY https://www.google.se/search?q=google+mos")
+117self.assertActionSilent(marvin_actions.marvinGoogle,"du kan googla")
+118self.assertActionSilent(marvin_actions.marvinGoogle,"gogool")
+119
+120deftestExplainShell(self):
+121"""Test that marvin can explain shell commands"""
+122url="http://explainshell.com/explain?cmd=pwd"
+123self.assertActionOutput(marvin_actions.marvinExplainShell,"explain pwd",url)
+124self.assertActionOutput(marvin_actions.marvinExplainShell,"can you explain pwd",url)
+125self.assertActionOutput(
+126marvin_actions.marvinExplainShell,
+127"förklara pwd|grep -o $user",
+128f"{url}%7Cgrep+-o+%24user")
+129
+130self.assertActionSilent(marvin_actions.marvinExplainShell,"explains")
+131
+132deftestSource(self):
+133"""Test that marvin responds to questions about source code"""
+134self.assertStringsOutput(marvin_actions.marvinSource,"source","source")
+135self.assertStringsOutput(marvin_actions.marvinSource,"källkod","source")
+136self.assertActionSilent(marvin_actions.marvinSource,"opensource")
+137
+138deftestBudord(self):
+139"""Test that marvin knows all the commandments"""
+140forninrange(1,5):
+141self.assertStringsOutput(marvin_actions.marvinBudord,f"budord #{n}","budord",f"#{n}")
+142
+143self.assertStringsOutput(marvin_actions.marvinBudord,"visa stentavla 1","budord","#1")
+144self.assertActionSilent(marvin_actions.marvinBudord,"var är stentavlan?")
+145
+146deftestQuote(self):
+147"""Test that marvin can quote The Hitchhikers Guide to the Galaxy"""
+148withmock.patch("marvin_actions.random")asr:
+149r.randint.return_value=1
+150self.assertStringsOutput(marvin_actions.marvinQuote,"ge os ett citat","hitchhiker",1)
+151self.assertStringsOutput(marvin_actions.marvinQuote,"filosofi","hitchhiker",1)
+152self.assertStringsOutput(marvin_actions.marvinQuote,"filosofera","hitchhiker",1)
+153self.assertActionSilent(marvin_actions.marvinQuote,"noquote")
+154
+155fori,_inenumerate(self.strings.get("hitchhiker")):
+156r.randint.return_value=i
+157self.assertStringsOutput(marvin_actions.marvinQuote,"quote","hitchhiker",i)
+158
+159deftestVideoOfToday(self):
+160"""Test that marvin can link to a different video each day of the week"""
+161withmock.patch("marvin_actions.datetime")asdt:
+162fordinrange(1,8):
+163dt.date.weekday.return_value=d-1
+164day=self.strings.get("weekdays").get(str(d))
+165video=self.strings.get("video-of-today").get(str(d))
+166response=f"{day} En passande video är {video}"
+167self.assertActionOutput(marvin_actions.marvinVideoOfToday,"dagens video",response)
+168self.assertActionSilent(marvin_actions.marvinVideoOfToday,"videoidag")
+169
+170deftestHelp(self):
+171"""Test that marvin can provide a help menu"""
+172self.assertStringsOutput(marvin_actions.marvinHelp,"help","menu")
+173self.assertActionSilent(marvin_actions.marvinHelp,"halp")
+174
+175deftestStats(self):
+176"""Test that marvin can provide a link to the IRC stats page"""
+177self.assertStringsOutput(marvin_actions.marvinStats,"stats","ircstats")
+178self.assertActionSilent(marvin_actions.marvinStats,"statistics")
+179
+180deftestIRCLog(self):
+181"""Test that marvin can provide a link to the IRC log"""
+182self.assertStringsOutput(marvin_actions.marvinIrcLog,"irc","irclog")
+183self.assertActionSilent(marvin_actions.marvinIrcLog,"ircstats")
+184
+185deftestSayHi(self):
+186"""Test that marvin responds to greetings"""
+187withmock.patch("marvin_actions.random")asr:
+188forskey,sinenumerate(self.strings.get("smile")):
+189forhkey,hinenumerate(self.strings.get("hello")):
+190forfkey,finenumerate(self.strings.get("friendly")):
+191r.randint.side_effect=[skey,hkey,fkey]
+192self.assertActionOutput(marvin_actions.marvinSayHi,"hej",f"{s}{h}{f}")
+193self.assertActionSilent(marvin_actions.marvinSayHi,"korsning")
+194
+195deftestLunchLocations(self):
+196"""Test that marvin can provide lunch suggestions for certain places"""
+197locations=["karlskrona","goteborg","angelholm","hassleholm","malmo"]
+198withmock.patch("marvin_actions.random")asr:
+199forlocationinlocations:
+200forindex,placeinenumerate(self.strings.get(f"lunch-{location}")):
+201r.randint.side_effect=[0,index]
+202self.assertActionOutput(
+203marvin_actions.marvinLunch,f"mat {location}",f"Ska vi ta {place}?")
+204r.randint.side_effect=[1,2]
+205self.assertActionOutput(
+206marvin_actions.marvinLunch,"dags att luncha","Jag är lite sugen på Indiska?")
+207self.assertActionSilent(marvin_actions.marvinLunch,"matdags")
+208
+209deftestStrip(self):
+210"""Test that marvin can recommend comics"""
+211messageFormat=self.strings.get("commitstrip").get("message")
+212expected=messageFormat.format(url=self.strings.get("commitstrip").get("url"))
+213self.assertActionOutput(marvin_actions.marvinStrip,"lite strip kanske?",expected)
+214self.assertActionSilent(marvin_actions.marvinStrip,"nostrip")
+215
+216deftestRandomStrip(self):
+217"""Test that marvin can recommend random comics"""
+218messageFormat=self.strings.get("commitstrip").get("message")
+219expected=messageFormat.format(url=self.strings.get("commitstrip").get("urlPage")+"123")
+220withmock.patch("marvin_actions.random")asr:
+221r.randint.return_value=123
+222self.assertActionOutput(marvin_actions.marvinStrip,"random strip kanske?",expected)
+223
+224deftestTimeToBBQ(self):
+225"""Test that marvin knows when the next BBQ is"""
+226self.assertBBQResponse(date(2024,5,17),date(2024,5,17),"today")
+227self.assertBBQResponse(date(2024,5,16),date(2024,5,17),"tomorrow")
+228self.assertBBQResponse(date(2024,5,10),date(2024,5,17),"week")
+229self.assertBBQResponse(date(2024,5,1),date(2024,5,17),"base")
+230self.assertBBQResponse(date(2023,10,17),date(2024,5,17),"eternity")
+231
+232self.assertBBQResponse(date(2024,9,20),date(2024,9,20),"today")
+233self.assertBBQResponse(date(2024,9,19),date(2024,9,20),"tomorrow")
+234self.assertBBQResponse(date(2024,9,13),date(2024,9,20),"week")
+235self.assertBBQResponse(date(2024,9,4),date(2024,9,20),"base")
+236
+237deftestNameDayReaction(self):
+238"""Test that marvin only responds to nameday when asked"""
+239self.assertActionSilent(marvin_actions.marvinNameday,"anything")
+240
+241deftestNameDayRequest(self):
+242"""Test that marvin sends a proper request for nameday info"""
+243withmock.patch("marvin_actions.requests")asr:
+244withmock.patch("marvin_actions.datetime")asd:
+245d.datetime.now.return_value=date(2024,1,2)
+246self.executeAction(marvin_actions.marvinNameday,"namnsdag")
+247self.assertEqual(r.get.call_args.args[0],"http://api.dryg.net/dagar/v2.1/2024/1/2")
+248
+249deftestNameDayResponse(self):
+250"""Test that marvin properly parses nameday responses"""
+251self.assertNameDayOutput("single","Idag har Svea namnsdag")
+252self.assertNameDayOutput("double","Idag har Alfred,Alfrida namnsdag")
+253self.assertNameDayOutput("nobody","Ingen har namnsdag idag")
+254
+255deftestJokeRequest(self):
+256"""Test that marvin sends a proper request for a joke"""
+257withmock.patch("marvin_actions.requests")asr:
+258self.executeAction(marvin_actions.marvinJoke,"joke")
+259self.assertEqual(r.get.call_args.args[0],"https://api.chucknorris.io/jokes/random?category=dev")
+260
+261deftestJoke(self):
+262"""Test that marvin sends a joke when requested"""
+263self.assertJokeOutput("joke","There is no Esc key on Chuck Norris' keyboard, because no one escapes Chuck Norris.")
+264
+265deftestUptime(self):
+266"""Test that marvin can provide the link to the uptime tournament"""
+267self.assertStringsOutput(marvin_actions.marvinUptime,"visa lite uptime","uptime","info")
+268self.assertActionSilent(marvin_actions.marvinUptime,"uptimetävling")
+269
+270deftestStream(self):
+271"""Test that marvin can provide the link to the stream"""
+272self.assertStringsOutput(marvin_actions.marvinStream,"ska mos streama?","stream","info")
+273self.assertActionSilent(marvin_actions.marvinStream,"är mos en streamer?")
+274
+275deftestPrinciple(self):
+276"""Test that marvin can recite some software principles"""
+277principles=self.strings.get("principle")
+278forkey,valueinprinciples.items():
+279self.assertActionOutput(marvin_actions.marvinPrinciple,f"princip {key}",value)
+280withmock.patch("marvin_actions.random")asr:
+281r.choice.return_value="dry"
+282self.assertStringsOutput(marvin_actions.marvinPrinciple,"princip","principle","dry")
+283self.assertActionSilent(marvin_actions.marvinPrinciple,"principlös")
+284
+285deftestCommitRequest(self):
+286"""Test that marvin sends proper requests when generating commit messages"""
+287withmock.patch("marvin_actions.requests")asr:
+288self.executeAction(marvin_actions.marvinCommit,"vad skriver man efter commit -m?")
+289self.assertEqual(r.get.call_args.args[0],"http://whatthecommit.com/index.txt")
+290
+291deftestCommitResponse(self):
+292"""Test that marvin properly handles responses when generating commit messages"""
+293message="Secret sauce #9"
+294response=requests.models.Response()
+295response._content=str.encode(message)
+296withmock.patch("marvin_actions.requests")asr:
+297r.get.return_value=response
+298expected=f"Använd detta meddelandet: '{message}'"
+299self.assertActionOutput(marvin_actions.marvinCommit,"commit",expected)
+300
+301deftestMorning(self):
+302"""Test that marvin wishes good morning, at most once per day"""
+303marvin_general_actions.lastDateGreeted=None
+304withmock.patch("marvin_general_actions.datetime")asd:
+305d.date.today.return_value=date(2024,5,17)
+306withmock.patch("marvin_general_actions.random")asr:
+307r.choice.return_value="Morgon"
+308self.assertActionOutput(marvin_general_actions.marvinMorning,"morrn","Morgon")
+309# Should only greet once per day
+310self.assertActionSilent(marvin_general_actions.marvinMorning,"morgon")
+311# Should greet again tomorrow
+312d.date.today.return_value=date(2024,5,18)
+313self.assertActionOutput(marvin_general_actions.marvinMorning,"godmorgon","Morgon")
+
35defassertActionOutput(self,action,message,expectedOutput):
+36"""Call an action on message and assert expected output"""
+37actualOutput=self.executeAction(action,message)
+38
+39self.assertEqual(actualOutput,expectedOutput)
+
+
+
+
Call an action on message and assert expected output
42defassertActionSilent(self,action,message):
+43"""Call an action with provided message and assert no output"""
+44self.assertActionOutput(action,message,None)
+
+
+
+
Call an action with provided message and assert no output
47defassertStringsOutput(self,action,message,expectedoutputKey,subkey=None):
+48"""Call an action with provided message and assert the output is equal to DB"""
+49expectedOutput=self.strings.get(expectedoutputKey)
+50ifsubkeyisnotNone:
+51ifisinstance(expectedOutput,list):
+52expectedOutput=expectedOutput[subkey]
+53else:
+54expectedOutput=expectedOutput.get(subkey)
+55self.assertActionOutput(action,message,expectedOutput)
+
+
+
+
Call an action with provided message and assert the output is equal to DB
58defassertBBQResponse(self,todaysDate,bbqDate,expectedMessageKey):
+59"""Assert that the proper bbq message is returned, given a date"""
+60url=self.strings.get("barbecue").get("url")
+61message=self.strings.get("barbecue").get(expectedMessageKey)
+62ifisinstance(message,list):
+63message=message[1]
+64ifexpectedMessageKeyin["base","week","eternity"]:
+65message=message%bbqDate
+66
+67withmock.patch("marvin_actions.datetime")asd:
+68d.date.today.return_value=todaysDate
+69withmock.patch("marvin_actions.random")asr:
+70r.randint.return_value=1
+71expected=f"{url}. {message}"
+72self.assertActionOutput(marvin_actions.marvinTimeToBBQ,"dags att grilla",expected)
+
+
+
+
Assert that the proper bbq message is returned, given a date
75defassertNameDayOutput(self,exampleFile,expectedOutput):
+76"""Assert that the proper nameday message is returned, given an inputfile"""
+77withopen(f"namedayFiles/{exampleFile}.json","r",encoding="UTF-8")asf:
+78response=requests.models.Response()
+79response._content=str.encode(json.dumps(json.load(f)))
+80withmock.patch("marvin_actions.requests")asr:
+81r.get.return_value=response
+82self.assertActionOutput(marvin_actions.marvinNameday,"nameday",expectedOutput)
+
+
+
+
Assert that the proper nameday message is returned, given an inputfile
84defassertJokeOutput(self,exampleFile,expectedOutput):
+85"""Assert that a joke is returned, given an input file"""
+86withopen(f"jokeFiles/{exampleFile}.json","r",encoding="UTF-8")asf:
+87response=requests.models.Response()
+88response._content=str.encode(json.dumps(json.load(f)))
+89withmock.patch("marvin_actions.requests")asr:
+90r.get.return_value=response
+91self.assertActionOutput(marvin_actions.marvinJoke,"joke",expectedOutput)
+
+
+
+
Assert that a joke is returned, given an input file
+
+
+
+
+
+
+
+
+ def
+ testSmile(self):
+
+
+
+
+
+
93deftestSmile(self):
+94"""Test that marvin can smile"""
+95withmock.patch("marvin_actions.random")asr:
+96r.randint.return_value=1
+97self.assertStringsOutput(marvin_actions.marvinSmile,"le lite?","smile",1)
+98self.assertActionSilent(marvin_actions.marvinSmile,"sur idag?")
+
+
+
+
Test that marvin can smile
+
+
+
+
+
+
+
+
+ def
+ testWhois(self):
+
+
+
+
+
+
100deftestWhois(self):
+101"""Test that marvin responds to whois"""
+102self.assertStringsOutput(marvin_actions.marvinWhoIs,"vem är marvin?","whois")
+103self.assertActionSilent(marvin_actions.marvinWhoIs,"vemär")
+
+
+
+
Test that marvin responds to whois
+
+
+
+
+
+
+
+
+ def
+ testGoogle(self):
+
+
+
+
+
+
105deftestGoogle(self):
+106"""Test that marvin can help google stuff"""
+107withmock.patch("marvin_actions.random")asr:
+108r.randint.return_value=1
+109self.assertActionOutput(
+110marvin_actions.marvinGoogle,
+111"kan du googla mos",
+112"LMGTFY https://www.google.se/search?q=mos")
+113self.assertActionOutput(
+114marvin_actions.marvinGoogle,
+115"kan du googla google mos",
+116"LMGTFY https://www.google.se/search?q=google+mos")
+117self.assertActionSilent(marvin_actions.marvinGoogle,"du kan googla")
+118self.assertActionSilent(marvin_actions.marvinGoogle,"gogool")
+
+
+
+
Test that marvin can help google stuff
+
+
+
+
+
+
+
+
+ def
+ testExplainShell(self):
+
+
+
+
+
+
120deftestExplainShell(self):
+121"""Test that marvin can explain shell commands"""
+122url="http://explainshell.com/explain?cmd=pwd"
+123self.assertActionOutput(marvin_actions.marvinExplainShell,"explain pwd",url)
+124self.assertActionOutput(marvin_actions.marvinExplainShell,"can you explain pwd",url)
+125self.assertActionOutput(
+126marvin_actions.marvinExplainShell,
+127"förklara pwd|grep -o $user",
+128f"{url}%7Cgrep+-o+%24user")
+129
+130self.assertActionSilent(marvin_actions.marvinExplainShell,"explains")
+
+
+
+
Test that marvin can explain shell commands
+
+
+
+
+
+
+
+
+ def
+ testSource(self):
+
+
+
+
+
+
132deftestSource(self):
+133"""Test that marvin responds to questions about source code"""
+134self.assertStringsOutput(marvin_actions.marvinSource,"source","source")
+135self.assertStringsOutput(marvin_actions.marvinSource,"källkod","source")
+136self.assertActionSilent(marvin_actions.marvinSource,"opensource")
+
+
+
+
Test that marvin responds to questions about source code
+
+
+
+
+
+
+
+
+ def
+ testBudord(self):
+
+
+
+
+
+
138deftestBudord(self):
+139"""Test that marvin knows all the commandments"""
+140forninrange(1,5):
+141self.assertStringsOutput(marvin_actions.marvinBudord,f"budord #{n}","budord",f"#{n}")
+142
+143self.assertStringsOutput(marvin_actions.marvinBudord,"visa stentavla 1","budord","#1")
+144self.assertActionSilent(marvin_actions.marvinBudord,"var är stentavlan?")
+
+
+
+
Test that marvin knows all the commandments
+
+
+
+
+
+
+
+
+ def
+ testQuote(self):
+
+
+
+
+
+
146deftestQuote(self):
+147"""Test that marvin can quote The Hitchhikers Guide to the Galaxy"""
+148withmock.patch("marvin_actions.random")asr:
+149r.randint.return_value=1
+150self.assertStringsOutput(marvin_actions.marvinQuote,"ge os ett citat","hitchhiker",1)
+151self.assertStringsOutput(marvin_actions.marvinQuote,"filosofi","hitchhiker",1)
+152self.assertStringsOutput(marvin_actions.marvinQuote,"filosofera","hitchhiker",1)
+153self.assertActionSilent(marvin_actions.marvinQuote,"noquote")
+154
+155fori,_inenumerate(self.strings.get("hitchhiker")):
+156r.randint.return_value=i
+157self.assertStringsOutput(marvin_actions.marvinQuote,"quote","hitchhiker",i)
+
+
+
+
Test that marvin can quote The Hitchhikers Guide to the Galaxy
+
+
+
+
+
+
+
+
+ def
+ testVideoOfToday(self):
+
+
+
+
+
+
159deftestVideoOfToday(self):
+160"""Test that marvin can link to a different video each day of the week"""
+161withmock.patch("marvin_actions.datetime")asdt:
+162fordinrange(1,8):
+163dt.date.weekday.return_value=d-1
+164day=self.strings.get("weekdays").get(str(d))
+165video=self.strings.get("video-of-today").get(str(d))
+166response=f"{day} En passande video är {video}"
+167self.assertActionOutput(marvin_actions.marvinVideoOfToday,"dagens video",response)
+168self.assertActionSilent(marvin_actions.marvinVideoOfToday,"videoidag")
+
+
+
+
Test that marvin can link to a different video each day of the week
+
+
+
+
+
+
+
+
+ def
+ testHelp(self):
+
+
+
+
+
+
170deftestHelp(self):
+171"""Test that marvin can provide a help menu"""
+172self.assertStringsOutput(marvin_actions.marvinHelp,"help","menu")
+173self.assertActionSilent(marvin_actions.marvinHelp,"halp")
+
+
+
+
Test that marvin can provide a help menu
+
+
+
+
+
+
+
+
+ def
+ testStats(self):
+
+
+
+
+
+
175deftestStats(self):
+176"""Test that marvin can provide a link to the IRC stats page"""
+177self.assertStringsOutput(marvin_actions.marvinStats,"stats","ircstats")
+178self.assertActionSilent(marvin_actions.marvinStats,"statistics")
+
+
+
+
Test that marvin can provide a link to the IRC stats page
+
+
+
+
+
+
+
+
+ def
+ testIRCLog(self):
+
+
+
+
+
+
180deftestIRCLog(self):
+181"""Test that marvin can provide a link to the IRC log"""
+182self.assertStringsOutput(marvin_actions.marvinIrcLog,"irc","irclog")
+183self.assertActionSilent(marvin_actions.marvinIrcLog,"ircstats")
+
+
+
+
Test that marvin can provide a link to the IRC log
+
+
+
+
+
+
+
+
+ def
+ testSayHi(self):
+
+
+
+
+
+
185deftestSayHi(self):
+186"""Test that marvin responds to greetings"""
+187withmock.patch("marvin_actions.random")asr:
+188forskey,sinenumerate(self.strings.get("smile")):
+189forhkey,hinenumerate(self.strings.get("hello")):
+190forfkey,finenumerate(self.strings.get("friendly")):
+191r.randint.side_effect=[skey,hkey,fkey]
+192self.assertActionOutput(marvin_actions.marvinSayHi,"hej",f"{s}{h}{f}")
+193self.assertActionSilent(marvin_actions.marvinSayHi,"korsning")
+
+
+
+
Test that marvin responds to greetings
+
+
+
+
+
+
+
+
+ def
+ testLunchLocations(self):
+
+
+
+
+
+
195deftestLunchLocations(self):
+196"""Test that marvin can provide lunch suggestions for certain places"""
+197locations=["karlskrona","goteborg","angelholm","hassleholm","malmo"]
+198withmock.patch("marvin_actions.random")asr:
+199forlocationinlocations:
+200forindex,placeinenumerate(self.strings.get(f"lunch-{location}")):
+201r.randint.side_effect=[0,index]
+202self.assertActionOutput(
+203marvin_actions.marvinLunch,f"mat {location}",f"Ska vi ta {place}?")
+204r.randint.side_effect=[1,2]
+205self.assertActionOutput(
+206marvin_actions.marvinLunch,"dags att luncha","Jag är lite sugen på Indiska?")
+207self.assertActionSilent(marvin_actions.marvinLunch,"matdags")
+
+
+
+
Test that marvin can provide lunch suggestions for certain places
+
+
+
+
+
+
+
+
+ def
+ testStrip(self):
+
+
+
+
+
+
209deftestStrip(self):
+210"""Test that marvin can recommend comics"""
+211messageFormat=self.strings.get("commitstrip").get("message")
+212expected=messageFormat.format(url=self.strings.get("commitstrip").get("url"))
+213self.assertActionOutput(marvin_actions.marvinStrip,"lite strip kanske?",expected)
+214self.assertActionSilent(marvin_actions.marvinStrip,"nostrip")
+
+
+
+
Test that marvin can recommend comics
+
+
+
+
+
+
+
+
+ def
+ testRandomStrip(self):
+
+
+
+
+
+
216deftestRandomStrip(self):
+217"""Test that marvin can recommend random comics"""
+218messageFormat=self.strings.get("commitstrip").get("message")
+219expected=messageFormat.format(url=self.strings.get("commitstrip").get("urlPage")+"123")
+220withmock.patch("marvin_actions.random")asr:
+221r.randint.return_value=123
+222self.assertActionOutput(marvin_actions.marvinStrip,"random strip kanske?",expected)
+
+
+
+
Test that marvin can recommend random comics
+
+
+
+
+
+
+
+
+ def
+ testTimeToBBQ(self):
+
+
+
+
+
+
224deftestTimeToBBQ(self):
+225"""Test that marvin knows when the next BBQ is"""
+226self.assertBBQResponse(date(2024,5,17),date(2024,5,17),"today")
+227self.assertBBQResponse(date(2024,5,16),date(2024,5,17),"tomorrow")
+228self.assertBBQResponse(date(2024,5,10),date(2024,5,17),"week")
+229self.assertBBQResponse(date(2024,5,1),date(2024,5,17),"base")
+230self.assertBBQResponse(date(2023,10,17),date(2024,5,17),"eternity")
+231
+232self.assertBBQResponse(date(2024,9,20),date(2024,9,20),"today")
+233self.assertBBQResponse(date(2024,9,19),date(2024,9,20),"tomorrow")
+234self.assertBBQResponse(date(2024,9,13),date(2024,9,20),"week")
+235self.assertBBQResponse(date(2024,9,4),date(2024,9,20),"base")
+
+
+
+
Test that marvin knows when the next BBQ is
+
+
+
+
+
+
+
+
+ def
+ testNameDayReaction(self):
+
+
+
+
+
+
237deftestNameDayReaction(self):
+238"""Test that marvin only responds to nameday when asked"""
+239self.assertActionSilent(marvin_actions.marvinNameday,"anything")
+
+
+
+
Test that marvin only responds to nameday when asked
+
+
+
+
+
+
+
+
+ def
+ testNameDayRequest(self):
+
+
+
+
+
+
241deftestNameDayRequest(self):
+242"""Test that marvin sends a proper request for nameday info"""
+243withmock.patch("marvin_actions.requests")asr:
+244withmock.patch("marvin_actions.datetime")asd:
+245d.datetime.now.return_value=date(2024,1,2)
+246self.executeAction(marvin_actions.marvinNameday,"namnsdag")
+247self.assertEqual(r.get.call_args.args[0],"http://api.dryg.net/dagar/v2.1/2024/1/2")
+
+
+
+
Test that marvin sends a proper request for nameday info
+
+
+
+
+
+
+
+
+ def
+ testNameDayResponse(self):
+
+
+
+
+
+
249deftestNameDayResponse(self):
+250"""Test that marvin properly parses nameday responses"""
+251self.assertNameDayOutput("single","Idag har Svea namnsdag")
+252self.assertNameDayOutput("double","Idag har Alfred,Alfrida namnsdag")
+253self.assertNameDayOutput("nobody","Ingen har namnsdag idag")
+
+
+
+
Test that marvin properly parses nameday responses
+
+
+
+
+
+
+
+
+ def
+ testJokeRequest(self):
+
+
+
+
+
+
255deftestJokeRequest(self):
+256"""Test that marvin sends a proper request for a joke"""
+257withmock.patch("marvin_actions.requests")asr:
+258self.executeAction(marvin_actions.marvinJoke,"joke")
+259self.assertEqual(r.get.call_args.args[0],"https://api.chucknorris.io/jokes/random?category=dev")
+
+
+
+
Test that marvin sends a proper request for a joke
+
+
+
+
+
+
+
+
+ def
+ testJoke(self):
+
+
+
+
+
+
261deftestJoke(self):
+262"""Test that marvin sends a joke when requested"""
+263self.assertJokeOutput("joke","There is no Esc key on Chuck Norris' keyboard, because no one escapes Chuck Norris.")
+
+
+
+
Test that marvin sends a joke when requested
+
+
+
+
+
+
+
+
+ def
+ testUptime(self):
+
+
+
+
+
+
265deftestUptime(self):
+266"""Test that marvin can provide the link to the uptime tournament"""
+267self.assertStringsOutput(marvin_actions.marvinUptime,"visa lite uptime","uptime","info")
+268self.assertActionSilent(marvin_actions.marvinUptime,"uptimetävling")
+
+
+
+
Test that marvin can provide the link to the uptime tournament
+
+
+
+
+
+
+
+
+ def
+ testStream(self):
+
+
+
+
+
+
270deftestStream(self):
+271"""Test that marvin can provide the link to the stream"""
+272self.assertStringsOutput(marvin_actions.marvinStream,"ska mos streama?","stream","info")
+273self.assertActionSilent(marvin_actions.marvinStream,"är mos en streamer?")
+
+
+
+
Test that marvin can provide the link to the stream
+
+
+
+
+
+
+
+
+ def
+ testPrinciple(self):
+
+
+
+
+
+
275deftestPrinciple(self):
+276"""Test that marvin can recite some software principles"""
+277principles=self.strings.get("principle")
+278forkey,valueinprinciples.items():
+279self.assertActionOutput(marvin_actions.marvinPrinciple,f"princip {key}",value)
+280withmock.patch("marvin_actions.random")asr:
+281r.choice.return_value="dry"
+282self.assertStringsOutput(marvin_actions.marvinPrinciple,"princip","principle","dry")
+283self.assertActionSilent(marvin_actions.marvinPrinciple,"principlös")
+
+
+
+
Test that marvin can recite some software principles
+
+
+
+
+
+
+
+
+ def
+ testCommitRequest(self):
+
+
+
+
+
+
285deftestCommitRequest(self):
+286"""Test that marvin sends proper requests when generating commit messages"""
+287withmock.patch("marvin_actions.requests")asr:
+288self.executeAction(marvin_actions.marvinCommit,"vad skriver man efter commit -m?")
+289self.assertEqual(r.get.call_args.args[0],"http://whatthecommit.com/index.txt")
+
+
+
+
Test that marvin sends proper requests when generating commit messages
+
+
+
+
+
+
+
+
+ def
+ testCommitResponse(self):
+
+
+
+
+
+
291deftestCommitResponse(self):
+292"""Test that marvin properly handles responses when generating commit messages"""
+293message="Secret sauce #9"
+294response=requests.models.Response()
+295response._content=str.encode(message)
+296withmock.patch("marvin_actions.requests")asr:
+297r.get.return_value=response
+298expected=f"Använd detta meddelandet: '{message}'"
+299self.assertActionOutput(marvin_actions.marvinCommit,"commit",expected)
+
+
+
+
Test that marvin properly handles responses when generating commit messages
+
+
+
+
+
+
+
+
+ def
+ testMorning(self):
+
+
+
+
+
+
301deftestMorning(self):
+302"""Test that marvin wishes good morning, at most once per day"""
+303marvin_general_actions.lastDateGreeted=None
+304withmock.patch("marvin_general_actions.datetime")asd:
+305d.date.today.return_value=date(2024,5,17)
+306withmock.patch("marvin_general_actions.random")asr:
+307r.choice.return_value="Morgon"
+308self.assertActionOutput(marvin_general_actions.marvinMorning,"morrn","Morgon")
+309# Should only greet once per day
+310self.assertActionSilent(marvin_general_actions.marvinMorning,"morgon")
+311# Should greet again tomorrow
+312d.date.today.return_value=date(2024,5,18)
+313self.assertActionOutput(marvin_general_actions.marvinMorning,"godmorgon","Morgon")
+
+
+
+
Test that marvin wishes good morning, at most once per day