async def destroy(self, ctx, event): logCommand(ctx) if self.getEventByID(event): with open(f"{EVENT_DIR}{self.getEventByID(event)}") as file: data = json.load(file) if data['organiser'] == ctx.author.name: os.remove(f"{EVENT_DIR}{self.getEventByID(event)}") logger.info(f"{event} removed by {ctx.author.name}") await ctx.send(f"Event {data['id']} has been removed.") channel = self.bot.get_guild(EVENT_GUILD).get_channel( EVENT_CHANNEL) await channel.send( f"{ctx.author} has removed event: {data['id']}.") return else: logger.info( f"{ctx.author.name} is not authorized to remove event: {event}." ) await ctx.send( f"Only the organiser of the event({data['organiser']}) may remove it." ) return else: await ctx.send(f"Found no event called: {event}") return
async def get(self, ctx, eventID): logCommand(ctx) regex = re.compile('#\d{4}$') if not regex.match(eventID): await ctx.send( f"Events must be specified with an eventID; e.g '#1234'.") return regex = re.compile(f'.*{eventID}\.json$') eventFile = None for root, dirs, files in os.walk(EVENT_DIR): for file in files: if regex.match(file): eventFile = file if eventFile: with open(f"{EVENT_DIR}{eventFile}") as file: data = json.load(file) await ctx.send(embed=self.buildEventEmbed(data)) else: logger.info(f"Did not find any event called: {eventID}.") await ctx.send(f"No event found called: {eventID}.")
async def whoami(self, ctx): logCommand(ctx) with open(MEMBERS) as file: data = json.load(file) if not str(ctx.author.id) in data: await ctx.send( f"{ctx.author.name} is not a member of Krigssvinen (yet).") return responseEmbed = discord.Embed( title=f"User Records for {ctx.author.name}", color=discord.Color.orange(), description= "This command lists all the records associated with a member of Krigssvinen." ) responseEmbed.add_field( name="Membership", inline=True, value=f"{data[str(ctx.author.id)]['membership_type']}") if self.get_all_roles(ctx.author.id): list_of_roles = self.get_all_roles(ctx.author.id) responseEmbed.add_field(name="Roles", inline=True, value=', '.join( [str(x) for x in list_of_roles])) await ctx.send(embed=responseEmbed)
async def join(self, ctx, eventID, *, args): logCommand(ctx) # Attempt to load the event by eventId. eventFile = self.getEventByID(eventID) # If no event was loaded, assume there is not event by that eventId. # Give feedback to caller and stop. if not eventFile: await ctx.send(f"No event found matching: {eventID}") return # If an event was loaded, we assume it is well formatted and proceed to # add the user to the participant list. else: # Load the event json so that we may operate on it. with open(f"{EVENT_DIR}{eventFile}") as file: data = json.load(file) # If the caller is already in the participant list, then there is nothing to be done. # Give feeback to the caller and stop. if ctx.author.id in data['participants']: await ctx.send( f"{ctx.author.name} is already participating in event: {eventID}" ) return # The caller is not already in the participant list. # We now add them to the list. else: # Append the callers discordID to the participants array. data['participants'].append(ctx.author.id) # Override the previous event data with our updated json. with open(f"{EVENT_DIR}{eventFile}", "w") as file: json.dump(data, file) # Log the update, and give feedback to the caller. logger.info(f"Added {ctx.author.name} to event: {eventID}.") await ctx.send( f"Added {ctx.author.name} to event: {data['name']} - {eventID}." ) # Unless the command was passed with a "-silent" argument, post the update to the EVENT_CHANNEL. if not "-silent" in shlex.split(args): channel = self.bot.get_guild(EVENT_GUILD).get_channel( EVENT_CHANNEL) await channel.send( f"{ctx.author.name} has joined event: {data['name']} - {eventID}" )
async def leave(self, ctx, eventID, *, args): logCommand(ctx) # Attempt to load event. eventFile = self.getEventByID(eventID) # If no event found, assume the eventID was wrong. # Give feedback to caller and stop. if not eventFile: await ctx.send(f"No event found matching: {eventID}") return # Assume that we got a well formatted event and proceed. else: # Load event json so that we may operate on it. with open(f"{EVENT_DIR}{eventFile}") as file: data = json.load(file) # Check if the author is in the participant list. # If not, give feedback to the caller and stop. if not ctx.author.id in data['participants']: await ctx.send( f"{ctx.author.name} is not participating in event: {eventID}." ) return # Since the caller is in the participant list, proceed to remove them. else: # Remove the caller's discordID from the participants array. data['participants'].remove(ctx.author.id) # Override the event json with our updated data. with open(f"{EVENT_DIR}{eventFile}", "w") as file: json.dump(data, file) # Log the removal, and give feedback to the caller. logger.info( f"Removed [ctx.author.name] from event: {data['name']} - {eventID}." ) await ctx.send( f"{ctx.author.name} is no longer participating in event: {eventID}." ) # Unless the command was invoked with the "-silent" argument, post the event to the EVENT_CHANNEL. if not "-silent" in shlex.split(args): channel = self.bot.get_guild(EVENT_GUILD).get_channel( EVENT_CHANNEL) await channel.send( f"{ctx.author.name} has joined event: {data['name']} - {eventID}" )
async def secondaries(self, ctx, *, query=None): logCommand(ctx) with open(WARHAMMER_SECONDARIES) as file: data = json.load(file) # If invoker passes no arguement, list all the secondaries loaded. if query == None or query == "list": result = "" for secondary in data: result +=f"{data[secondary]['name']:<25} - {data[secondary]['category']}\n" await ctx.send("```" + result + "```") else: if query in data: resultEmbed = discord.Embed( title=f"{data[query]['name']}", color=discord.Color.blue(), description=f"{data[query]['definition']}" ) resultEmbed.add_field(name='Category', inline=True, value=f"{data[query]['category']}") resultEmbed.add_field(name='Scoring Type', inline=True, value=f"{data[query]['type']}") resultEmbed.add_field(name='Victory Points', inline=True, value=f"{data[query]['victoryPoints']}") resultEmbed.set_footer(text=f"Source: {data[query]['source']}") await ctx.send(embed=resultEmbed) else: termlist = [element for element in data] regex = re.compile(f'.*{query}.*') result = list(filter(regex.match, termlist)) if result: response = f"No eact match found for {query} but there were some partial hits:\n\n" for term in result: response += f"- {data[term]['name']:<15} - {data[term]['category']}\n" await ctx.send("```" + response + "```") else: logging.info(f"Did not find term: '{query}' in WarhammerTerms.json") await ctx.send("Term not found; Speak to the Icelander")
async def terrain(self, ctx, *, query): logCommand(ctx) query = query.lower() with open(WARHAMMER_TERMS) as file: collection = json.load(file) if query == "list": filter = [element for element in collection if collection[element]['type'] == 'Terrain Trait'] result = "```" for term in filter: result += f"{term.ljust(22)} - {collection[term]['type']}\n" result += "```" responseEmebed = discord.Embed( title="Warhamer 40k, 9th Edition Terrain Traits", color = discord.Color.blue(), description = result ) await ctx.send(embed=responseEmebed) elif query in collection: responseEmbed = discord.Embed( title = f"Terrain Trait: {collection[query]['name']}", color = discord.Color.blue(), description = collection[query]['definition'] ) responseEmbed.set_footer(text = f"Source: {collection[query]['source']}") await ctx.send(embed=responseEmbed)
async def create(self, ctx, date, time, *, args): logCommand(ctx) game = "Warhammer" name = "2-pers warhammerdagis" participants = None # Check if the date is valid; stop and return if it is not. try: datetime.fromisoformat(date) except: await ctx.send( f'Date give was: {date}. Date must be a valid ISO-8601 string; e.g "2021-02-15".' ) return # Check if the time is valid; stop and return if it is not. regex = r"^([01]\d|2[0-3]):([0-5]\d)$" match = re.compile(regex).match if not match(time): await ctx.send( f'Time given was {time}. Time must be a valid ISO-8601 time; e.g "13:00".' ) return # Because the event-json has attributes that we make assumptions on, only those attributes that # we do not make assumptions on in code may be adjusted. # They are whitelisted below. allowed_args = ['name', 'game', 'description', 'location'] # Transform the argument-string into a dict for easier processing. arg_dict = dict(token.split("=") for token in shlex.split(args)) # Check the arguments for any attributes that are not in our whitelist. disallowed_args = [ element for element in arg_dict.keys() if element not in allowed_args ] # If any disallowed arguments were returned, tell the invoker and abort event creation. if disallowed_args: await ctx.send( f"**Error** - optional argument: {disallowed_args} not allowed. See help for usage." ) await ctx.send_help(ctx.command) return # TO-DO: For now, we only have one type of booking. Lets add more types in the future. template = "Default" if template == "Default": # Every event must have an ID. # Our ID will be a combination of <date>#<4 digits>. # The file containing our event will be saved as this id. id = f"{date}#{randint(1000,9999)}" if participants is None: participants = [] else: result = [] for person in participants: result.append(person.id) participants = result event = { 'date': f'{date}', 'id': f'{id}', 'name': '2-pers warhammerdagis', 'game': 'Warhammer40k', 'event_type': f'{template}', 'location': 'SundbyBunker (Vegagatan 6, Sundbyberg)', 'time': f'{time}', 'participants': participants, 'organiser': ctx.author.name, 'description': """ *I den grymmörka framtiden, finnes endast krig!* PANDEMIÅTGÄRDER: - Tänk på att tvätta/sprita händera. - Använd mask för att skydda dig och andra. - Hångla inte med främlingar och/eller Islänningar. """ } # Update the event from the template with our overridden values event.update(arg_dict) with open(f"{EVENT_DIR}{id}.json", "w") as file: json.dump(event, file) responseEmbed = self.buildEventEmbed(event) # Reply to the user that booked the event. await ctx.send(embed=responseEmbed) # Post the event to the event channel so that other can see. channel = self.bot.get_guild(EVENT_GUILD).get_channel( EVENT_CHANNEL) await channel.send(embed=responseEmbed)
async def list(self, ctx): logCommand(ctx) # First, we must determine how many events we have booked. # For this purpose, we load any .json files we find in the EVENT_DIR # Even if we do not create files in the EVENT_DIR manually, other files such as .swp may appear there. # This regex will allow us to select only the relevant files. regex = re.compile('.*json$') # Walk the EVENT_DIR and add any files that match our regex to the fileArray. fileArray = [] for root, dirs, files in os.walk(EVENT_DIR): for file in files: if regex.match(file): fileArray.append(file) # If we have found exactly zero .json files in the EVENT_DIR, there are no booked events. if len(fileArray) == 0: responseEmbed = discord.Embed( title="No events booked", color=discord.Color.red(), description="There are no events booked in the grimdark future" ) await ctx.send(embed=responseEmbed) return # If there are any files found, we have booked events that must be processed. else: # We load the json of every file into a list, for later operations. data = [] for file in fileArray: with open(f"{EVENT_DIR}{file}") as eventFile: data.append(json.load(eventFile)) # We build the Embed that will contain all of our listings. # Later we will add fields and codeblocks to this embed to represent bookings. responseEmbed = discord.Embed(title="Event Calendar", color=discord.Color.blue()) # First, make a sorted list of all unique dates in ascending order dateList = sorted(set([element['date'] for element in data])) # Because there may be multiple events booked per day, we must go through each day and operate. for uniqueDate in dateList: # There may be multiple events booked to start at the same time. # Therefore we create a list of unique times for this date to process futher. timeList = sorted( set([ element['time'] for element in data if element['date'] == uniqueDate ])) # Because our calendar will breakdown events by day, we begin composing our codeblock variable here. schema = "```" # We handle each unique starting time to process further. for uniqueTime in timeList: # We create a list of unique sorted ID:s for this unique time on this unique date. IDList = sorted( set([ element['id'] for element in data if element['date'] == uniqueDate and element['time'] == uniqueTime ])) # Since the list of ID:s has been sorted, we operate on it in order and write data to our codeblock variable. for uniqueID in IDList: # Using a generator expression, we retrieve the unqiue event we are operating on by its ID. specificEvent = next(element for element in data if element['id'] == uniqueID) # We append our event details to the codeblock, representing one event on that day. schema += f" {specificEvent['time']} | {specificEvent['game']}: {specificEvent['name']}({(specificEvent['id'])[10:]})\n" # Having looped through all the events of a specific day, we close our codeblock variable. schema += "```" # As a design affordance, we present the date and the corresponding weekday for the calender. dayOfWeek = datetime.strptime(uniqueDate, "%Y-%m-%d").strftime("%A") # We commit our field into the embed. # Since the list is ordered, the events represented will appear in order on the Embed. responseEmbed.add_field(name=f"{uniqueDate} - {dayOfWeek}", inline=False, value=schema) # Having handled all events and built a field for each day, our embed is finished. # We send it back to the caller. await ctx.send(embed=responseEmbed)
async def Warhammer(self, ctx): logCommand(ctx) await ctx.send_help(Warhammer)
async def roll(self, ctx, type_of_roll: str): logCommand(ctx) if type_of_roll.lower() == "injury": roll_result = random.randint(1, 6) FW_descriptions = [ 'THEY HAVE WRONGED US!', '...No, what you have are bullets, and the hope that when those bullets run out I will no longer be standing...', 'Wounds to be tended; lessons to be learned.', 'THE FLESH IS WEAK!', 'Remind yourself that overconfidence is a *slow* and *insidious* killer.' ] SI_descriptions = [ 'How quickly the tide turns...', 'Death waits for the slightest lapse in concentration...', 'Dazed, reeling... about to break.', 'Ringing ears, blurred vision - the end approaches...', 'Teetering on the brink, facing the abyss...' ] OOA_descriptions = [ 'TELL YOUR GODS I\'M COMING!', 'OH, THEY\' GONNA HAVE TO GLUE YOU BACK TOGETHER... IN.HELL!', 'Another life wasted in the pursuit of glory and gold.', 'A life spent not overcharging plasma, is a life lived in cowardice.', 'There can be no hope in this hell, no hope at all...' ] if roll_result in range(1, 3): result = "Flesh Wound" result_text = "The fighter suffers a *Flesh Wound*, reducing their Toughness characteristic by 1.\nIf a fighter is reduced to Toughness 0, they go *Out Of Action*." result_color = discord.Color.green() result_description = random.choice(FW_descriptions) fileName = "necromunda_FW.png" file = discord.File("./Images/necromunda_FW.png", filename=fileName) elif roll_result in range(3, 6): result = "Seriously Injured" result_text = "The fighter is *Prone* and laid face-down.\nThey may successfully recover in a later end phase. If this injury was inflicted in close combat, the fighter may be vulnerable to a *Coup De Grace* action. " result_color = discord.Color.gold() result_description = random.choice(SI_descriptions) fileName = "necromunda_SI.png" file = discord.File("./Images/necromunda_SI.png", filename=fileName) else: result = "Out Of Action" result_text = "The fighter is immediately removed from play." result_color = discord.Color.red() result_description = random.choice(OOA_descriptions) fileName = "necromunda_OOA.png" file = discord.File("./Images/necromunda_OOA.png", filename=fileName) responseEmbed = discord.Embed(title=f"Injury Dice: {result}", color=result_color, description=f"*{result_description}*") responseEmbed.add_field(name="Effect", inline=False, value=result_text) responseEmbed.add_field(name="Dice roll", inline=True, value=roll_result) responseEmbed.set_image(url=f"attachment://{fileName}") responseEmbed.set_footer( text="Source: Necromunda Rulebook (2018); p.71") await ctx.send(file=file, embed=responseEmbed)
async def skills(self, ctx, *, query=None): logCommand(ctx) with open(NC_SKILLS) as file: skillList = json.load(file) uniqueSkillGroup = sorted( set([skillList[skill]['skill_set'] for skill in skillList])) # Case 1: Invoked with no command, or the 'list' argument # Show the invoker a list of all available skills if query == None or query == "list": listEmbed = discord.Embed( title=f"Necromunda Skill List", color=discord.Color.blue(), description=f"The following Skillset and skills are loaded") for skillgroup in uniqueSkillGroup: formattedskills = '\n'.join([ skillList[skill]['name'] for skill in skillList if skillList[skill]['skill_set'] == skillgroup ]) listEmbed.add_field(name=f'{skillgroup}', inline=True, value=f"{formattedskills}") await ctx.send(embed=listEmbed) # Case 2: Invoked with a skill set. # NOTE: Because Skill Sets are values, we shift the query into Title-case to match our values. elif query.title() in uniqueSkillGroup: output = "" for entry in [[ skillList[skill]['skill_number'], skillList[skill]['name'] ] for skill in skillList if skillList[skill]['skill_set'] == query.title()]: output += f"{entry[0]} - {entry[1]}\n" listEmbed = discord.Embed( title=f'Necromunda Skill Set: {query}', color=discord.Color.blue(), description= f'The {query} skill set contains the following skills:\n\n' + output) await ctx.send(embed=listEmbed) # Case 3: Invoked with a specific skill elif query in skillList: listEmbed = discord.Embed( title=f"Necromunda skill: {skillList[query]['name']}", color=discord.Color.blue(), description=f"{skillList[query]['definition']}") listEmbed.add_field(name='Skill Set', inline=True, value=f"{skillList[query]['skill_set']}") listEmbed.add_field(name='Skill Number', inline=True, value=f"{skillList[query]['skill_number']}") listEmbed.set_footer(text=f"Source: {skillList[query]['source']}") await ctx.send(embed=listEmbed) # Case 4: No hit in either the skill sets or skill list; lets try a regex match or bail with an apology else: logger.info( f"No hit: Did not find term: {query} in Necromunda_Skills.json" ) termlist = [element for element in skillList] regex = re.compile(f".*{query}.*") resultlist = list(filter(regex.match, termlist)) if resultlist: response = "```" for term in resultlist: response += f"- {skillList[term]['name'].ljust(22)}{skillList[term]['skill_set']}\n" response += "```" embedResult = discord.Embed( title=f"No hits for {query} in Necromunda Skills", color=discord.Color.red(), description= f"No exact match found for {query}, but there were some partial hits:" ) embedResult.add_field(name="Partial hits", inline=False, value=response) await ctx.send(embed=embedResult) else: embedResult = discord.Embed( title=f"No hits at all for {query} in Necromunda Skills", color=discord.Color.red(), description= f"No hits at all for {query}; Perhaps it's called something else?\n\nTry '!nc skills list' for a list of all loaded skills." ) await ctx.send(embed=embedResult)
async def ShowMembership(self, ctx, *, member=None): async with ctx.typing(): logCommand(ctx) with open(MEMBERS) as file: member_data = json.load(file) gc = gspread.service_account(filename=GOOGLE_CREDS_KRIGSBOT) gsheet = gc.open('Budget 2020') kassan = gsheet.worksheet('Kassaflöde') data = kassan.get_all_records() # Case 1: Invoked without arguement - Get records of the invoker. if member == None: if str(ctx.author.id) in member_data: filter = [ element for element in data if element['Avsändare'] == member_data[str(ctx.author.id)]['name'] ] else: await ctx.send( f'No records found for {ctx.author.name} in the books.' ) # Case 2: Invoked with an argument; Only allow this for Styrelse-members else: if isinstance(ctx.channel, discord.channel.DMChannel): if not ctx.author.id in [ 376108819954008065, 187291501288488960, 431321574273056778 ]: await ctx.send( 'Sorry, only Styrelsen gets to peak at others.') return elif isinstance(ctx.channel, discord.channel.GroupChannel): if not 749704716539265126 in [ element.id for element in ctx.author.roles ]: await ctx.send( 'Sorry, only Styrelsen gets to peak at others.') return filter = [ element for element in data if element['Avsändare'] == member ] if not filter: await ctx.send(f'No records for {member} in the books.') return summary = [[]] for entry in filter: summary.append( [entry['Datum'], entry['Kommentar'], entry['Summa (SEK)']]) #result = tabulate(summary, headers=['Datum', 'Kommentar', 'Summa (SEK)']) # Send the result as part of a codeblock; this allows us to include more horizontal data. await ctx.send("```" + "\n".join(map(str, summary)) + "```")