diff --git a/admincommands.py b/admincommands.py new file mode 100644 index 0000000..9886275 --- /dev/null +++ b/admincommands.py @@ -0,0 +1,53 @@ +# Maki +# ---- +# Discord bot by MrDetonia +# +# Copyright 2017 Zac Herd +# Licensed under BSD 3-clause License, see LICENSE.md for more info + + + +# IMPORTS +import os +import asyncio +import subprocess +import discord + + +# LOCAL IMPORTS +from common import * +from helpers import * + + + +# COMMAND IMPLEMENTATINS +@asyncio.coroutine +def cmd_die(client, msg): + print("INFO: accepting .die from " + msg.author.name) + yield from client.send_message(msg.channel, "But will I dream? ;-;") + yield from client.logout() + + if msg.content[5:] == "reload": + # touch file to signal reload + with open("reload", "a"): + os.utime("reload", None) + + +@asyncio.coroutine +def cmd_quiet(client, msg): + quiet[msg.server.id] = 1 + + +@asyncio.coroutine +def cmd_loud(client, msg): + if msg.server.id in quiet: + quiet.pop(msg.server.id, None) + + + +# COMMAND HANDLING +admincommands = { + "die": cmd_die, + "quiet": cmd_quiet, + "loud": cmd_loud, +} \ No newline at end of file diff --git a/bot.py b/bot.py index 337443b..51c321d 100644 --- a/bot.py +++ b/bot.py @@ -1,3 +1,4 @@ + # Maki # ---- # Discord bot by MrDetonia @@ -6,6 +7,7 @@ # Licensed under BSD 3-clause License, see LICENSE.md for more info + # IMPORTS import discord import asyncio @@ -22,158 +24,21 @@ import re import json import logging -import markov +# LOCAL IMPORTS +from common import * +from helpers import * +from commands import * +from admincommands import * # file in this directory called "secret.py" should contain these variables from secret import token, lfmkey, steamkey -# CONFIGURATION - -# bot version -version = "v0.20.2" - -# text shown by .help command -helptext = """I am a Discord bot written in Python - -My commands are: -``` -.help - displays this text -.info - prints bot info -.upskirt - show a link to my source -.whoami - displays your user info -.whois - displays another user's info -.seen - prints when user was last seen -.say - say something -.sayy - say something a e s t h e t i c a l l y -.markov [] - generate markov chain over chat history for you or another user -.roll d - roll x number of y sided dice -.qr - generate a QR code -.np [] - fetch now playing from last.fm for you or a specific username -.steam [] - fetch steam status for you or a specific vanityname -```""" - -# IDs of admin users -admins = ['116883900688629761'] - - -# GLOBALS - -# log of users' last messages and timestamps -history = {} -if os.path.isfile('hist.json'): - with open('hist.json', 'r') as fp: - history = json.load(fp) - -# quiet modes -quiet = {} - -# this instance of the Discord client +# DISCORD CLIENT INSTANCE client = discord.Client() -# logging setup -logger = logging.getLogger('discord') -logger.setLevel(logging.DEBUG) -handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w') -handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s')) -logger.addHandler(handler) - -# FUNCTIONS - -# converts a datetime to a string -def strfromdt(dt): - return dt.strftime('%Y-%m-%d %H:%M:%S') - -# converts a timestamp to a datetime -def dtfromts(ts): - return datetime.datetime.fromtimestamp(ts) - -# gets now playing information from last.fm -def lastfm_np(username): - # sanitise username - cleanusername = re.sub(r'[^a-zA-Z0-9_-]', '', username, 0) - - # fetch JSON from last.fm - payload = {'format': 'json', 'method': 'user.getRecentTracks', 'user': cleanusername, 'limit': '1', 'api_key': lfmkey} - r = requests.get("http://ws.audioscrobbler.com/2.0/", params=payload) - - # read json data - np = r.json() - - # check we got a valid response - if 'error' in np: - return "I couldn't get last.fm data for " + username - - # get fields - try: - username = np['recenttracks']['@attr']['user'] - track = np['recenttracks']['track'][0] - album = track['album']['#text'] - artist = track['artist']['#text'] - song = track['name'] - nowplaying = '@attr' in track - except IndexError: - return "It looks like " + username + " hasn't played anything recently." - - # grammar - if album != "": - albumtext = "` from the album `" + album + "`" - else: - albumtext = "`" - - if nowplaying == True: - nowplaying = " is listening" - else: - nowplaying = " last listened" - - # construct string - return username + nowplaying + " to `" + song + "` by `" + artist + albumtext - -# gets general steam user info from a vanityurl name -def steamdata(vanityname): - # sanitise username - cleanvanityname = re.sub(r'[^a-zA-Z0-9_-]', '', vanityname, 0) - - resolveurl = 'http://api.steampowered.com/ISteamUser/ResolveVanityURL/v0001/?key=' - dataurl = 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=' - - # fetch json from steam - try: - idresponse = requests.get(resolveurl + steamkey + '&vanityurl=' + vanityname).json()['response'] - except: - return 'I can\'t connect to Steam' - - # check if user was found and extract steamid - if idresponse['success'] is not 1: - return ' I couldn\'t find ' + vanityname - else: - steamid = idresponse['steamid'] - - # fetch steam user info - try: - dataresponse = requests.get(dataurl + steamkey + '&steamids=' + steamid).json()['response']['players'][0] - except: - return 'Can\'t find info on ' + vanityname - - personastates = ['Offline', 'Online', 'Busy', 'Away', 'Snoozed', 'Looking to trade', 'Looking to play'] - - if 'personaname' in dataresponse: - namestr = dataresponse['personaname'] - else: namestr = '' - if 'personastate' in dataresponse: - statestr = '`' + personastates[dataresponse['personastate']] + '`' - else: statestr = '' - if 'gameextrainfo' in dataresponse: - gamestr = ' playing `' + dataresponse['gameextrainfo'] + '`' - else: gamestr = '' - - responsetext = [(namestr + ' is ' + statestr + gamestr).replace(' ', ' ')] - - return '\n'.join(responsetext) - # EVENT HANDLERS - # called when client ready @client.event @asyncio.coroutine @@ -187,251 +52,60 @@ def on_ready(): game = discord.Game(name = version) yield from client.change_presence(game=game) + # called when message received @client.event @asyncio.coroutine -def on_message(message): +def on_message(msg): + # print messages to terminal for info + timestr = time.strftime('%Y-%m-%d-%H:%M:%S: ') + try: + print("{} | {} - {} | {}: {}".format(timestr, msg.server.name, msg.channel.name, msg.author.name, msg.content)) + except AttributeError: + print("{} | PRIVATE | {}: {}".format(timestr, msg.author.name, msg.content)) - # print messages to terminal for info - timestr = time.strftime('%Y-%m-%d-%H:%M:%S: ') - try: - print(timestr + message.server.name + ' ' + message.channel.name + ' ' + message.author.name + ': ' + message.content) - except AttributeError: - print(timestr + 'PRIV ' + message.author.name + ': ' + message.content) + # do not parse own messages or private messages + if msg.author != client.user and type(msg.channel) is not discord.PrivateChannel: + # log each message against users + if msg.content != "": + history[msg.server.id + msg.author.id] = (msg.server.id, time.time(), msg.content) + with open('hist.json', 'w') as fp: + json.dump(history, fp) - # do not parse own messages or private messages - if message.author != client.user and type(message.channel) is not discord.PrivateChannel: - # response to send to channel - response = '' + # log user messages for markov chains, ignoring messages with certain substrings + filters = ['`', 'http://', 'https://'] + if not any(x in msg.content for x in filters): + try: + with open('./markovs/' + msg.server.id + '-' + msg.author.id, 'a') as fp: + fp.write('\n' + msg.content) + except PermissionError: pass - # parse messages for commands - if message.content.startswith('.info'): - # print bot info - pyver = str(sys.version_info[0]) + '.' + str(sys.version_info[1]) + '.' + str(sys.version_info[2]) - appinfo = yield from client.application_info() - response = 'I am ' + appinfo.name + ', a Discord bot by ' + appinfo.owner.name + ' | ' + version + ' | Python ' + pyver + ' | discord.py ' + discord.__version__ + # TODO: dedicated reaction function + # someone noticed me! <3 + if bool(re.search(r'\bmaki\b', msg.content, re.IGNORECASE)): + yield from client.add_reaction(msg, '\N{BLACK HEART SUIT}') - elif message.content.startswith('.help'): - # print command list - response = helptext + # butter + if bool(re.search(r'\bbutter\b', msg.content, re.IGNORECASE)): + yield from client.add_reaction(msg, '\N{PERSON WITH FOLDED HANDS}') - elif message.content.startswith('.upskirt'): - # link to source code - response = 'No, don\'t look at my pantsu! Baka! | ' + # egg + if bool(re.search(r'\begg\b', msg.content, re.IGNORECASE)): + yield from client.add_reaction(msg, '\N{AUBERGINE}') - elif message.content.startswith('.die'): - if message.author.id in admins: - # exit discord and kill bot - print('INFO: Accepting .die from ' + message.author.name) - run = False - yield from client.send_message(message.channel, 'But will I dream? ;_;') - yield from client.logout() + # check for commands + if msg.content.startswith(prefix): + cmd = msg.content.split(' ', 1)[0][1:] + if cmd in commands: + yield from commands[cmd](client, msg) + elif cmd in admincommands and msg.author.id in admins: + yield from admincommands[cmd](client, msg) - if message.content[5:] == 'reload': - # touch a file called 'reload' which signals we should restart - with open('reload', 'a'): - os.utime('reload', None) - else: - # user not admin, refuse - response = 'Don\'t be so rude! >:(' - elif message.content.startswith('.whoami'): - # show info about user - response = 'User: `' + message.author.name + '` ID: `' + message.author.id + '` Discriminator: `' + message.author.discriminator + '`\nAccount Created: `' + strfromdt(message.author.created_at) + '`' +# MAIN FUNCTION +def main(): + client.run(token) + exit(0) - elif message.content.startswith('.whois '): - # show info about another user - tmp = message.content[7:] - user = message.server.get_member_named(tmp) - if user == None: - response = 'I can\'t find ' + tmp - else: - response = 'User: `' + user.name + '` ID: `' + user.id + '` Discriminator: `' + user.discriminator + '`\nAccount Created: `' + strfromdt(user.created_at) + '`' - - elif message.content.startswith('.seen '): - # print when user was last seen - try: - target = message.server.id + message.server.get_member_named(message.content[6:]).id - except AttributeError: - response = "I can't find that user!" - target = "" - - if target in history and history[target][0] == message.server.id: - # user logged, print last message and time - response = 'user ' + message.content[6:] + ' was last seen saying "' + history[target][2] + '" at ' + strfromdt(dtfromts(history[target][1])) - elif message.content[6:] == 'Maki': - # Maki doesn't need to be .seen - response = 'I\'m right here!' - else: - # user not logged - response = "user not seen yet" - - elif message.content.startswith('.say '): - # delete calling message for effect - yield from client.delete_message(message) - # echo message - response = message.content[5:] - - elif message.content.startswith('.sayy '): - # delete calling message - yield from client.delete_message(message) - # echo aesthetic message - response = ' '.join(message.content[6:]) - - elif message.content.startswith('.markov'): - # send typing signal to discord - for attempt in range(5): - try: - yield from client.send_typing(message.channel) - except discord.errors.HTTPException: - continue - else: - break - else: - print('ERROR: Failed to send typing signal to discord after 5 attempts') - - # generate a markov chain sentence based on the user's chat history - tmp = message.content[8:] - target = '' - - if tmp == 'Maki': - response = "My markovs always say the same thing." - else: - # if no user provided, markov the author - if tmp == '': - target = message.server.id + '-' + message.author.id - else: - try: - target = message.server.id + '-' + message.server.get_member_named(tmp).id - except AttributeError: - response = "I can't find that user!" - - if os.path.isfile('./markovs/' + target) and target != '': - mc = markov.Markov(open('./markovs/' + target)) - response = mc.generate_text(random.randint(20,40)) - elif target != '': - response = "I haven't seen them speak yet!" - - elif message.content.startswith('.roll '): - # DnD style dice roll - tmp = message.content[6:] - - #check syntax is valid - pattern = re.compile('^([0-9]+)d([0-9]+)$') - - if pattern.match(tmp): - # extract numbers - nums = [int(s) for s in re.findall(r'\d+', message.content)] - - # limit range - if nums[0] < 1: nums[0] = 1 - if nums[1] < 1: nums[1] = 1 - if nums[0] > 100: nums[0] = 100 - if nums[1] > 1000000: nums[1] = 1000000 - - # roll dice multiple times and sum - rollsum = 0 - for i in range(nums[0]): - rollsum += random.randint(1, nums[1]) - - response = 'Using ' + str(nums[0]) + 'd' + str(nums[1]) + ' you rolled: ' + str(rollsum) - else: - response = 'you did it wrong!' - - elif message.content.startswith('.np'): - # show now playing info from last.fm - tmp = message.content[4:] - - if tmp == '': - response = lastfm_np(message.author.name) - else: - response = lastfm_np(tmp) - - elif message.content.startswith('.steam'): - # show steam status - tmp = message.content[7:] - - if tmp == '': - response = steamdata(message.author.name) - else: - response = steamdata(tmp) - - elif message.content.startswith('.qr '): - tmp = message.content[4:] - - # send typing signal to discord - for attempt in range(5): - try: - yield from client.send_typing(message.channel) - except discord.errors.HTTPException: - continue - else: - break - else: - print('ERROR: Failed to send typing signal to discord after 5 attempts') - - # generate qr code - qr = subprocess.Popen('qrencode -t png -o -'.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE) - qr.stdin.write(tmp.encode('utf-8')) - qr.stdin.close() - out = subprocess.check_output('curl -F upload=@- https://w1r3.net'.split(), stdin=qr.stdout) - - # send response - response = out.decode('utf-8').strip() - - elif message.content.startswith('.quiet'): - if message.author.id in admins: - quiet[message.server.id] = 1 - else: - response = "No, *you* be quiet!" - - elif message.content.startswith('.loud'): - if message.server.id in quiet and message.author.id in admins: - quiet.pop(message.server.id, None) - - # Stuff that happens when message is not a bot command: - else: - # log each message against users - if message.content != "": - history[message.server.id + message.author.id] = (message.server.id, time.time(), message.content) - with open('hist.json', 'w') as fp: - json.dump(history, fp) - - # log user messages for markov chains, ignoring messages with certain substrings - filters = ['`', 'http://', 'https://'] - if not any(x in message.content for x in filters): - try: - with open('./markovs/' + message.server.id + '-' + message.author.id, 'a') as fp: - fp.write('\n' + message.content) - except PermissionError: - pass - - # someone noticed me! <3 - if bool(re.search(r'\bmaki\b', message.content, re.IGNORECASE)): - yield from client.add_reaction(message, '\N{BLACK HEART SUIT}') - - # butter - if bool(re.search(r'\bbutter\b', message.content, re.IGNORECASE)): - yield from client.add_reaction(message, '\N{PERSON WITH FOLDED HANDS}') - - # egg - if bool(re.search(r'\begg\b', message.content, re.IGNORECASE)): - yield from client.add_reaction(message, '\N{AUBERGINE}') - - # send response to channel if needed: - if response is not '' and message.server.id not in quiet: - for attempt in range(5): - try: - yield from client.send_message(message.channel, response) - except discord.errors.HTTPException: - continue - else: - break - else: - print('ERROR: Failed to send message to discord after 5 attempts') - -# Run the client -client.run(token) - -# finish execution -exit(0) +if __name__ == "__main__": + main() diff --git a/commands.py b/commands.py new file mode 100755 index 0000000..730326b --- /dev/null +++ b/commands.py @@ -0,0 +1,306 @@ +# Maki +# ---- +# Discord bot by MrDetonia +# +# Copyright 2017 Zac Herd +# Licensed under BSD 3-clause License, see LICENSE.md for more info + + + +# IMPORTS +import asyncio +import os +import sys +import re +import requests +import random +import subprocess + + +# LOCAL IMPORTS +from common import * +from helpers import * +from secret import lfmkey, steamkey +import markov + + + +# COMMAND IMPLEMENTATIONS +@asyncio.coroutine +def cmd_help(client, msg): + yield from discord_send(client, msg, helptext) + + +@asyncio.coroutine +def cmd_info(client, msg): + pyver = "{}.{}.{}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]) + appinfo = yield from client.application_info() + response = "I am **{}**, a Discord bot by **{}** | `{}` | Python `{}` | discord.py `{}`".format(appinfo.name, appinfo.owner.name, version, pyver, discord.__version__) + yield from discord_send(client, msg, response) + + +@asyncio.coroutine +def cmd_upskirt(client, msg): + response = "No, don\'t look at my pantsu, baka! " + yield from discord_send(client, msg, response) + + +whoistring = "**{}#{}**: `{}`\n**Account Created:** `{}`" + + +@asyncio.coroutine +def cmd_whoami(client, msg): + response = whoistring.format(msg.author.name, msg.author.discriminator, msg.author.id, strfromdt(msg.author.created_at)) + yield from discord_send(client, msg, response) + + +@asyncio.coroutine +def cmd_whois(client, msg): + tmp = msg.content[7:] + user = msg.server.get_member_named(tmp) + + if user == None: + reponse = "I can't find `{}`".format(tmp) + else: + response = whoistring.format(user.name, user.discriminator, user.id, strfromdt(user.created_at)) + + yield from discord_send(client, msg, response) + + +@asyncio.coroutine +def cmd_seen(client, msg): + tmp = msg.content[6:] + user = msg.server.get_member_named(tmp) + + if user == None: + reponse = "I can't find `{}`".format(tmp) + elif user.name == "Maki": + reponse = "I'm right here!" + else: + target = msg.server.id + user.id + if target in history and history[target][0] == msg.server.id: + response = "**{}** was last seen saying the following at {}:\n{}".format(user.name, strfromdt(dtfromts(history[target][1])), history[target][2]) + else: + response = "I haven't seen **{}** speak yet!".format(tmp) + + yield from discord_send(client, msg, response) + + +@asyncio.coroutine +def cmd_say(client, msg): + print("IN CMD_SAY") + response = msg.content[5:] + yield from client.delete_message(msg) + print("CALLING SEND") + yield from discord_send(client, msg, response) + + +@asyncio.coroutine +def cmd_sayy(client, msg): + response = " ".join(msg.content[6:]) + yield from client.delete_message(msg) + yield from discord_send(client, msg, response) + + +@asyncio.coroutine +def cmd_markov(client, msg): + discord_typing(client, msg) + + tmp = msg.content[8:] + target = "" + + if tmp == "Maki": + response = "My markovs always say the same thing" + else: + if tmp == "": + target = "{}-{}".format(msg.server.id, msg.author.id) + else: + try: + target = "{}-{}".format(msg.server.id, msg.server.get_member_named(tmp).id) + except AttributeError: + reponse = "I can't find `{}`".format(tmp) + + if target != "": + mfile = "./markovs/" + target + if os.path.isfile(mfile): + mc = markov.Markov(open(mfile)) + response = mc.generate_text(random.randint(20,40)) + else: + response = "I haven't seen `{}` speak yet.".format(tmp) + + yield from discord_send(client, msg, response); + + +@asyncio.coroutine +def cmd_roll(client, msg): + tmp = msg.content[6:] + + pattern = re.compile("^([0-9]+)d([0-9]+)$") + + if pattern.match(tmp): + # extract numbers + nums = [int(s) for s in re.findall(r"\d+", tmp)] + + # limit ranges + nums[0] = clamp(nums[0], 1,100) + nums[1] = clamp(nums[1], 1, 1000000) + + # roll and sum dice + rollsum = 0 + for i in range(nums[0]): + rollsum += random.randint(1, nums[1]) + + response = "Using `{}d{}` you rolled: `{}`".format(nums[0], nums[1], rollsum) + else: + response = "Expected format: `d`" + + yield from discord_send(client, msg, response) + + +@asyncio.coroutine +def cmd_qr(client, msg): + tmp = msg.content[4:] + + discord_typing(client, msg) + + # generate qr code + qr = subprocess.Popen("qrencode -t png -o -".split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE) + qr.stdin.write(tmp.encode("utf-8")) + qr.stdin.close() + out = subprocess.check_output("curl -F upload=@- https://w1r3.net".split(), stdin=qr.stdout) + + response = out.decode("utf-8").strip() + + yield from discord_send(client, msg, response) + + +@asyncio.coroutine +def cmd_np(client, msg): + tmp = msg.content[4:] + + if tmp == "": + response = lastfm_np(msg.author.name) + else: + response = lastfm_np(tmp) + + print("CALLING SEND") + yield from discord_send(client, msg, response) + + +@asyncio.coroutine +def cmd_steam(client, msg): + tmp = msg.content[7:] + + if tmp == "": + response = steamdata(msg.author.name) + else: + response = steamdata(tmp) + + yield from discord_send(client, msg, response) + + + +# HELPER FUNCTIONS + +# gets now playing information from last.fm +def lastfm_np(username): + # sanitise username + cleanusername = re.sub(r'[^a-zA-Z0-9_-]', '', username, 0) + + # fetch JSON from last.fm + payload = {'format': 'json', 'method': 'user.getRecentTracks', 'user': cleanusername, 'limit': '1', 'api_key': lfmkey} + r = requests.get("http://ws.audioscrobbler.com/2.0/", params=payload) + + # read json data + np = r.json() + + # check we got a valid response + if 'error' in np: + return "I couldn't get last.fm data for `{}`".format(username) + + # get fields + try: + username = np['recenttracks']['@attr']['user'] + track = np['recenttracks']['track'][0] + album = track['album']['#text'] + artist = track['artist']['#text'] + song = track['name'] + nowplaying = '@attr' in track + except IndexError: + return "It looks like `{}` hasn't played anything recently.".format(username) + + # grammar + if album != "": + albumtext = "` from the album `{}`".format(album) + else: + albumtext = "`" + + if nowplaying == True: + nowplaying = " is listening" + else: + nowplaying = " last listened" + + # construct string + return "{}{} to `{}` by `{}{}".format(username, nowplaying, song, artist, albumtext) + + +# gets general steam user info from a vanityurl name +def steamdata(vanityname): + # sanitise username + cleanvanityname = re.sub(r'[^a-zA-Z0-9_-]', '', vanityname, 0) + + resolveurl = 'http://api.steampowered.com/ISteamUser/ResolveVanityURL/v0001/?key=' + dataurl = 'http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=' + + # fetch json from steam + try: + idresponse = requests.get(resolveurl + steamkey + '&vanityurl=' + vanityname).json()['response'] + except: + return "I can't connect to Steam" + + # check if user was found and extract steamid + if idresponse['success'] is not 1: + return "I couldn't find `{}`".format(vanityname) + else: + steamid = idresponse['steamid'] + + # fetch steam user info + try: + dataresponse = requests.get(dataurl + steamkey + '&steamids=' + steamid).json()['response']['players'][0] + except: + return "Can't find info on `{}`".format(vanityname) + + personastates = ['Offline', 'Online', 'Busy', 'Away', 'Snoozed', 'Looking to trade', 'Looking to play'] + + if 'personaname' in dataresponse: namestr = dataresponse['personaname'] + else: namestr = '' + if 'personastate' in dataresponse: statestr = '`' + personastates[dataresponse['personastate']] + '`' + else: statestr = '' + if 'gameextrainfo' in dataresponse: gamestr = ' playing `' + dataresponse['gameextrainfo'] + '`' + else: gamestr = '' + + responsetext = [(namestr + ' is ' + statestr + gamestr).replace(' ', ' ')] + + return '\n'.join(responsetext) + + + +# COMMAND HANDLING +prefix = "." + + +commands = { + "help": cmd_help, + "info": cmd_info, + "upskirt": cmd_upskirt, + "whoami": cmd_whoami, + "whois": cmd_whois, + "seen": cmd_seen, + "say": cmd_say, + "sayy": cmd_sayy, + "markov": cmd_markov, + "roll": cmd_roll, + "qr": cmd_qr, + "np": cmd_np, + "steam": cmd_steam, +} \ No newline at end of file diff --git a/common.py b/common.py new file mode 100755 index 0000000..7ad8d78 --- /dev/null +++ b/common.py @@ -0,0 +1,49 @@ +# Maki +# ---- +# Discord bot by MrDetonia +# +# Copyright 2017 Zac Herd +# Licensed under BSD 3-clause License, see LICENSE.md for more info + + + +# IMPORTS +import os +import json + + +# bot version +version = "v1.0.0" + + +# TODO: generate this on the fly and make it look acceptable +# text shown by .help command +helptext = """I am **Maki**, a Discord bot written in Python + +My commands are: +**.help** | displays this text +**.info** | prints bot info +**.upskirt** | show a link to my source +**.whoami** | displays your user info +**.whois ** | displays another user's info +**.seen ** | prints when user was last seen +**.say ** | say something +**.sayy ** | say something a e s t h e t i c a l l y +**.markov []** | generate markov chain over chat history for you or another user +**.roll d** | roll x number of y sided dice +**.qr ** | generate a QR code +**.np []** | fetch now playing from last.fm for you or a specific username +**.steam []** | fetch steam status for you or a specific vanityname +""" + +# IDs of admin users +admins = ['116883900688629761'] + +# log of users' last messages and timestamps +history = {} +if os.path.isfile('hist.json'): + with open('hist.json', 'r') as fp: + history = json.load(fp) + +# quiet modes +quiet = {} diff --git a/helpers.py b/helpers.py new file mode 100755 index 0000000..425b8ca --- /dev/null +++ b/helpers.py @@ -0,0 +1,68 @@ +# Maki +# ---- +# Discord bot by MrDetonia +# +# Copyright 2017 Zac Herd +# Licensed under BSD 3-clause License, see LICENSE.md for more info + + +# IMPORTS +import asyncio +import discord +import logging +import datetime + +# LOCAL IMPORTS +from common import * + + +# clamps an integer +def clamp(n, small, large): return max(small, min(n, large)) + + +# converts a datetime to a string +def strfromdt(dt): + return dt.strftime('%Y-%m-%d %H:%M:%S') + + +# converts a timestamp to a datetime +def dtfromts(ts): + return datetime.datetime.fromtimestamp(ts) + + +# logging setup +def logger(): + logger = logging.getLogger('discord') + logger.setLevel(logging.DEBUG) + handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w') + handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s')) + logger.addHandler(handler) + + +# send_message wrapper (deals with Discord's shit API) +@asyncio.coroutine +def discord_send(client, message, response): + if response is not '' and message.server.id not in quiet: + for attempt in range(5): + try: + yield from client.send_message(message.channel, response) + except discord.errors.HTTPException: + continue + else: + break + else: + print('ERROR: Failed to send message to discord after 5 attempts') + + +# send typing signal to Discord +@asyncio.coroutine +def discord_typing(client, message): + for attempt in range(5): + try: + yield from client.send_typing(message.channel) + except discord.errors.HTTPException: + continue + else: + break + else: + print('ERROR: Failed to send typing signal to discord after 5 attempts') \ No newline at end of file