Something I cooked up for someone, it went way out of the scope and I spent way too much time on it to not share it with other people. Still a WIP and there are some things I still need to fix/add/remove, especially the recovery feature. Lmk how atrocious my code is, and don't tell me that I need more comments, because I know I do xD

You can probably presume what is censored.

Features:
  • Type -ticket to create private channel for author and admins
  • Send a custom intro to each new ticket
  • Customer can send information to be stored locally on bot host's computer (and off of *******)
  • Retrieve ticket data through the bot console, private message, or post it back into the ticket channel
  • Send an invoice for bitcoin, and automatically detect when paid and close the ticket, deleting channel and any ticket information from host computer
  • Generate a list of all member ******* names



Code:
"""A bot to list all members of a server as well as make ticket channels"""
import csv
import sys
import time
import re
import *******
import datetime
import asyncio
import requests
import os
import threading
import platform
import sqlite3
import urllib.request
from *******.ext import commands
from *******.ex*****mmands import Bot
from electrum import bitcoin
from electrum import keystore
if platform.system() == "Windows":
    from win32com.client import Dispatch


# Global Constants
token = '0'
command_prefix = '-'
bot_name = "Duke's Bot"
seeds = {}
progress_bar = None
bot = None
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
conn = None
c = None


# Create files and directories
if not os.path.isdir('database/'):
    os.mkdir('database/')
if not os.path.isfile('database/data.db'):
    conn = sqlite3.connect('database/data.db')
    c = conn.cursor()
    c.execute('''CREATE TABLE servers
                 (serverid TEXT, servername TEXT, prefix TEXT, ticketnumber INT, intro TEXT)''')
    c.execute('''CREATE TABLE IF NOT EXISTS Addresses 
    (serverid TEXT, address TEXT, balance REAL, ticket INT)''')
    conn.commit()
else:
    conn = sqlite3.connect('database/data.db')
    c = conn.cursor()
if not os.path.isdir('settings/'):
    os.mkdir('settings/')
if not os.path.isfile('settings/vers.txt'):
    ticket_create = open("settings/vers.txt", "w+")
    ticket_create.write('0')
    ticket_create.close()
if not os.path.isfile('settings/settings.txt'):
    conf_create = open("settings/settings.txt", "w+")
    command_prefix = input('What would you like your prefix to be? Leave blank for default. ')
    if (command_prefix.isalpha() and len(command_prefix) == 1) or command_prefix.isdigit():
        print("That prefix is too dangerous. Reverting to default")
        command_prefix = "-"
    conf_create.write('#Your bot token. Please follow TO GET A token directions in README.txt to get a token\n' + token
                      + '#Your bot prefix\n' + command_prefix)
    conf_create.close()
    input('Please follow TO GET A token directions in README.txt to get a token')
    sys.exit(0)


# Get all constants
num_lines = 0
with open('settings/settings.txt') as fp:
    line = fp.readline()
    while line:
        if line[0] is not '' and line[0] is not '#':
            num_lines += 1
        line = fp.readline()
    fp.seek(0)
    line = fp.readline()
    while line:
        if token is '0' and line[0] is not '#' and line is not '':
            token = line[:-1]
        elif command_prefix is '' and line[0] is not '#' and line is not '':
            command_prefix = line
        line = fp.readline()
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)
if len(token) < 14:
    input('Please follow TO GET A token directions in README.txt to get a token')
    sys.exit(0)
bot = Bot(command_prefix=command_prefix)


   @bot.event
async def on_ready():
    print('Logged in as')
    print(bot.user.name + ' displayed as ' + bot_name)
    print(bot.user.id)
    print("Prefix: " + command_prefix)
    print('------')
    print('Ready to work')
    for server in bot.servers:
        c.execute('''SELECT serverid FROM servers WHERE serverid=?''', (server.id,))
        got = c.fetchone()
        if got is None:
            c.execute('''INSERT INTO servers(servername, serverid, prefix, ticketnumber, intro)
                                  VALUES(?,?,?,?,?)''', (server.name, server.id, '-', 0, None))
        query = '''CREATE TABLE IF NOT EXISTS {} (ticket INT PRIMARY KEY, channelid TEXT, userid TEXT, username 
        TEXT, content TEXT, address TEXT, requiredbtc REAL, dollaramnt REAL, 
        senttime TEXT, messageid TEXT)'''.format(combine_server_string(server))
        c.execute(query)
        c.execute('''CREATE TABLE IF NOT EXISTS Addresses (serverid TEXT, 
        address TEXT, balance REAL, ticket INT)''')
        conn.commit()
    await bot.change_presence(game=*******.Game(name="you heathens writhe", type=3))
    await bot.edit_profile(username=bot_name)
    if platform.system() == "Windows":
        user_name = os.getenv('username')
        desktop = 'C:\\Users\\' + user_name + '\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\'
        if not os.path.isfile(desktop + 'mbot.ink'):
            path = os.path.join(desktop, "mbot.lnk")
            target = os.getcwd() + "\\main.exe"
            working_dir = os.getcwd()
            shell = Dispatch('WScript.Shell')
            shortcut = shell.CreateShortCut(path)
            shortcut.Targetpath = target
            shortcut.WorkingDirectory = working_dir
            shortcut.save()
    threading.Timer(10, check_payments).start()


   @bot.event
async def on_server_join(server):
    c.execute('''INSERT INTO servers(servername, serverid, prefix, ticketnumber, intro)
                                      VALUES(?,?,?,?,?)''', (server.name, server.id, '-', 0, None))
    query = '''CREATE TABLE IF NOT EXISTS {} (ticket INT PRIMARY KEY, channelid TEXT, userid TEXT,
            username TEXT, content TEXT, address TEXT, requiredbtc REAL, dollaramnt REAL, 
        senttime TEXT, messageid TEXT)'''.format(combine_server_string(server))
    c.execute(query)
    conn.commit()


   @bot.event
async def on_command_error(error, ctx):
    print("Error on: " + ctx.message.content)
    if isinstance(error, commands.CommandNotFound):
        return
    else:
        print(error)


   @bot.command(pass_context=True)
async def ticket(ctx):
    """Opens up a new ticket channel"""
    if ctx.message.channel.name == "tickets":
        await bot.delete_message(ctx.message)
        server = ctx.message.server
        author_id = ctx.message.author.id
        author_name = str(await bot.get_user_info(author_id))
        c.execute('''SELECT ticketnumber FROM servers WHERE serverid=?''', (server.id,))
        number = c.fetchone()[0]
        c.execute('''SELECT intro FROM servers WHERE serverid=?''', (server.id,))
        server_intro = c.fetchone()[0]
        everyone = *******.PermissionOverwrite(read_messages=False)
        mine = *******.PermissionOverwrite(read_messages=True)
        test = await bot.create_channel(server, str(number), (server.default_role, everyone),
                                        (ctx.message.author, mine))
        if server_intro is not None:
            try:
                await bot.send_message(test, "Hello. Please copy, paste and fill out the following form.\n"
                                       + "```" + command_prefix + "send " + server_intro + "```")
            except:
                print("I couldn't send the intro to channel, sending to author")
                await bot.send_message(ctx.message.author,
                                       "Hello. Please copy, paste and fill out the following form in the ticket\n"
                                       + "```" + command_prefix + "send " + server_intro + "```")
        c.execute('''UPDATE servers SET ticketnumber = ? WHERE serverid=? ''', (str(number+1), server.id,))
        cleaned_name = re.sub("[^0-9a-zA-Z# '.!?]+", '', author_name)
        query = '''INSERT INTO {} (ticket, channelid, userid, username)
                                      VALUES(?,?,?,?)'''.format(combine_server_string(server))
        c.execute(query, (str(number), str(test.id), str(author_id), cleaned_name,))
        conn.commit()
    else:
        await bot.send_message(ctx.message.author, "Please use the correct channel labeled 'tickets'")


   @bot.command(pass_context=True)
async def intro(ctx):
    """Adds/ updates the ticket channel welcome message"""
    if ctx.message.author.server_permissions.administrator:
        message_content = ctx.message.content[len(command_prefix)+6:]
        c.execute('''UPDATE servers SET intro = ? WHERE serverid = ? ''',
                  (message_content, ctx.message.server.id,))
        conn.commit()
        await bot.say("Updated.")
    await bot.delete_message(ctx.message)


   @bot.command(pass_context=True)
async def prefix(ctx):
    """Changes the prefix to enact the bot"""
    if ctx.message.author.server_permissions.administrator:
        new_prefix = (ctx.message.content[len(command_prefix)+7:])
        server_id = ctx.message.server.id
        if (new_prefix.isalpha() and len(new_prefix) == 1) or new_prefix.isdigit():
            await bot.send_message(ctx.message.channel, "That prefix is too dangerous. Please choose another.")
        else:
            c.execute('''UPDATE servers SET ticketnumber = ? WHERE serverid = ? ''', (new_prefix, server_id,))
            conn.commit()
            await bot.send_message(ctx.message.channel, "My future prefix will be: " + new_prefix)
    else:
        await bot.say("You do not have permissions for that.")
    await bot.delete_message(ctx.message)


   @bot.command(pass_context=True)
async def send(ctx):
    """Stores local copy of ticket data. Can also append new data to an old ticket"""
    await bot.delete_message(ctx.message)
    server = ctx.message.server
    query = '''SELECT content FROM {} WHERE channelid = ?'''.format(combine_server_string(server))
    c.execute(query, (ctx.message.channel.id,))
    old_content = c.fetchone()
    if old_content is None:
        await bot.say("Channel is not in my database.")
        return
    else:
        if old_content[0] is not None:
            new_content = old_content[0] + "Update at: " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "\n" \
                          + ctx.message.content[len(command_prefix)+5:] + "\n"
        else:
            new_content = ctx.message.content[len(command_prefix) + 5:] + '\n'
        query = '''UPDATE {} SET content = ? WHERE channelid = ?'''.format(combine_server_string(server))
        c.execute(query, (new_content, ctx.message.channel.id,))
        conn.commit()
        await bot.say(ctx.message.author.display_name + " I have received your message.")


   @bot.command(pass_context=True)
async def close(ctx):
    """Deletes the channel and local ticket data"""
    if ctx.message.author.server_permissions.administrator:
        query = '''SELECT FROM {} WHERE channelid = ?'''.format(combine_server_string(ctx.message.server))
        c.execute(query, (ctx.message.channel.id,))
        check_exist = c.fetchone()
        if check_exist is not None:
            query = ''''DELETE FROM {} WHERE channelid=?'''.format(combine_server_string(ctx.message.server))
            c.execute(query, (ctx.message.channel.id,))
            await bot.delete_channel(ctx.message.channel)
        else:
            await bot.say("Channel not in database.")
            await bot.delete_message(ctx.message)


   @bot.command(pass_context=True)
async def console(ctx):
    """Posts ticket data to the local console"""
    if ctx.message.author.server_permissions.administrator:
        await bot.delete_message(ctx.message)
        query = '''SELECT content FROM {} WHERE channelid = ?'''.format(combine_server_string(ctx.message.server))
        c.execute(query, (ctx.message.channel.id,))
        check_exist = c.fetchone()
        if check_exist is not None:
            print(check_exist[0])
        else:
            print("No data")


   @bot.command(pass_context=True)
async def post(ctx):
    """Posts ticket data to the channel"""
    if ctx.message.author.server_permissions.administrator:
        await bot.delete_message(ctx.message)
        query = '''SELECT content FROM {} WHERE channelid = ?'''.format(combine_server_string(ctx.message.server))
        c.execute(query, (ctx.message.channel.id,))
        check_exist = c.fetchone()
        if check_exist is not None:
            await bot.say('```' + check_exist[0] + '```')
        else:
            await bot.say('No data')


   @bot.command(pass_context=True)
async def private(ctx):
    """Pms ticket data to the admin"""
    if ctx.message.author.server_permissions.administrator:
        await bot.delete_message(ctx.message)
        query = '''SELECT content FROM {} WHERE channelid = ?'''.format(combine_server_string(ctx.message.server))
        c.execute(query, (ctx.message.channel.id,))
        check_exist = c.fetchone()
        if check_exist is not None:
            await bot.send_message(ctx.message.author, '```' + check_exist[0] + '```')
        else:
            await bot.say('No data')


   @bot.command(pass_context=True)
async def ids(ctx):
    """Returns a CSV file of all users on the server. May take some time."""
    if ctx.message.author.server_permissions.administrator:
        await bot.request_offline_members(ctx.message.server)
        print("This may take a while.")
        before = time.time()
        nicknames = [m.id for m in ctx.message.server.members]
        with open('ids.csv', mode='w', encoding='utf-8', newline='') as f:
            writer = csv.writer(f, dialect='excel')
            for v in nicknames:
                writer.writerow([await bot.get_user_info(v)])
        after = time.time()
        await bot.send_file(ctx.message.author, 'ids.csv', filename='ids.csv',
                            content="Also saved locally in ids.csv. Compiled in {:.4}ms.".format((after - before)*1000))


   @bot.command(pass_context=True)
async def sendids(ctx):
    """Sends a pm to all users on previously generated ids.txt"""
    if ctx.message.author.server_permissions.administrator:
        if os.path.isfile('ids.txt'):
            f = open('ids.txt')
            id_line = f.readline()
            while line:
                recip = await bot.get_user_info(id_line)
                try:
                    time.sleep(.1)
                    await bot.send_message(recip, ctx.message.content[len(command_prefix)+8:])
                except:
                    print('Can not send to ' + recip)
                id_line = f.readline()
            f.close()
        else:
            bot.send_message(ctx.message.author, "You haven't generated an ids.txt file. Run ```" + command_prefix
                             + 'textids```')
            print("You haven't generated an ids.txt file. Run '''" + command_prefix + 'textids```')


   @bot.command(pass_context=True)
async def textids(ctx):
    """Generate a local text file of ids"""
    if ctx.message.author.server_permissions.administrator:
        await bot.request_offline_members(ctx.message.server)
        id_numbers = [m.id for m in ctx.message.server.members]
        all_ids = ''
        for v in id_numbers:
            if all_ids == '':
                all_ids = v
            else:
                all_ids = all_ids + '\n' + v
        f = open('ids.txt', "w")
        f.write(all_ids)
        f.close()


   @bot.event
async def on_message(ctx):
    if ctx.channel.name == 'tickets' and not ctx.author.server_permissions.administrator and not\
            ct*****ntent == command_prefix + 'ticket':
        await bot.send_message(ctx.author, "You may only post " + command_prefix + 'ticket in the ticket channel')
        await bot.delete_message(ctx)
    else:
        await bot.process_commands(ctx)


   @bot.command(pass_context=True)
async def done(ctx):
    """Work in progress system to automatically accept btc"""
    if ctx.message.author.server_permissions.administrator:
        server = ctx.message.server
        c.execute('''SELECT address FROM Addresses WHERE (serverid=? and
                 ticket IS NULL)''', (server.id,))
        addr = c.fetchone()
        if addr is not None:
            dollars_due = float(re.sub("[^0-9.]+", '', ctx.message.content[len(command_prefix)+5:]))
            amount_due = dollars_to_btc(dollars_due)
            msg = await bot.say('Please send ' + str(amount_due) + ' bitcoin to ' + addr[0] + ' within 60 minutes')
            query = '''UPDATE {} SET address=?,  requiredbtc=?, dollaramnt=?, senttime=?,
             messageid=? WHERE channelid=?'''.format(combine_server_string(server))
            sent_time = time.time()
            msg_id = msg.id
            c.execute(query, (addr[0], amount_due, dollars_due, sent_time, msg_id, ctx.message.channel.id))
            query = '''SELECT ticket FROM {} WHERE channelid = ?'''.format(combine_server_string(ctx.message.server))
            c.execute(query, (ctx.message.channel.id,))
            ticket_number = c.fetchone()[0]
            c.execute('''UPDATE Addresses SET ticket=? WHERE address=?''', (ticket_number, addr[0]))
            conn.commit()
        else:
            await bot.say('Please generate some (more?) addresses.')


   @bot.command(pass_context=True)
async def seed(ctx):
    """Add 50 receiving addresses to the database. RUN ONCE"""
    await bot.delete_message(ctx.message)
    if ctx.message.author is ctx.message.server.owner:
        c.execute('''SELECT address FROM Addresses WHERE serverid=?''', (ctx.message.server.id,))
        already_ran = c.fetchone()
        if already_ran is None:
            server = ctx.message.server
            server_id = server.id
            k = keystore.from_seed(ctx.message.content[len(command_prefix) + 5:], '', False)
            for x in range(0, 49):
                addr = bitcoin.pubkey_to_address('p2pkh', k.derive_pubkey(False, x))
                addr_balance = get_balance(addr)
                c.execute('''INSERT INTO Addresses (serverid, address, balance)
                                                      VALUES(?,?,?)''', (server_id, addr, addr_balance,))
            conn.commit()
            await bot.send_message(ctx.message.author, "50 btc addresses have been generated and stored.")
        else:
            await bot.say("Addresses already generated.")


async def delete_channel(channel_id):
    await bot.delete_channel(channel_id)


async def update_payment(message_id, channel_id, new_btc, address, author_id, server_id):
    await bot.http.delete_message(channel_id, message_id, server_id)
    await bot.http.edit_message(message_id, channel_id, "<@" + author_id + "> Please send the updated amount of " +
                                new_btc + " to " + address, server_id)
    await bot.http.send_message(channel_id, "<@" + author_id + ">", server_id)


async def payment_time_left(channel_id, message_id, amount_due, address, sent_time):
    time_left = 60 - round((time.time() - float(sent_time)) / 60)
    await bot.http.edit_message(message_id, channel_id, 'Please send ' + str(amount_due) + ' bitcoin to ' + address +
                                ' within ' + str(time_left) + ' minutes')


def combine_server_string(server):
    return '[' + clean_string(server.name) + ":" + server.id + ']'


def get_balance(address):
    response = requests.get("https://bitaps.com/api/address/" + address)
    if response.status_code != 200:
        return None
    balance = round(float(response.json()['confirmed_balance'] / 100000000), 5)
    return balance


def dollars_to_btc(dollars_due):
    bitcoin_api_url = 'https://api.coinmarketcap.com/v1/ticker/bitcoin/'
    response = requests.get(bitcoin_api_url)
    response_json = response.json()
    btc_price = float(response_json[0]['price_usd'])
    return round(dollars_due / btc_price, 4)


def clean_string(string):
    cleaned_string = re.sub("[^0-9a-zA-Z ']+", '', string)
    return cleaned_string


def check_payments():
    conn2 = sqlite3.connect('database/data.db')
    c2 = conn2.cursor()
    for server in bot.servers:
        query = '''SELECT * FROM {} WHERE requiredbtc IS NOT NULL'''.format(combine_server_string(server))
        required = c2.execute(query)
        for k in required:
            channel_id = k[1]
            addr = k[5]
            required_btc = k[6]
            dollars_due = k[7]
            sent_time = k[8]
            message_id = k[9]
            c2.execute('''SELECT balance FROM Addresses WHERE address=?''', (addr,))
            start_balance = c2.fetchone()[0]
            if (float(start_balance) + float(get_balance(addr))) > float(required_btc):
                print("Ticket paid")
                query = '''DELETE FROM {} WHERE address=?'''.format(combine_server_string(server))
                c2.execute(query, (addr,))
                asyncio.run_coroutine_threadsafe(delete_channel(channel_id), loop)
            elif float(time.time()) > float(sent_time) + 3600:
                print("Ticket out of time")
                new_btc = dollars_to_btc(dollars_due)
                asyncio.run_coroutine_threadsafe(update_payment(message_id, channel_id, new_btc, addr, k[3],
                                                                server.id), loop)
                query = '''UPDATE {} SET requiredbtc=?, senttime=? 
                WHERE channelid=?'''.format(combine_server_string(server))
                c2.execute(query, (new_btc, time.time(), channel_id))
            else:
                print("Updating ticket time left")
                asyncio.run_coroutine_threadsafe(payment_time_left(channel_id, message_id,
                                                                   required_btc, addr, sent_time), loop)
    threading.Timer(60, check_payments).start()


if __name__ == '__main__':
    bot.run(token)