diff --git a/plugins/bitcoin.py b/plugins/bitcoin.py deleted file mode 100755 index b06e3ee..0000000 --- a/plugins/bitcoin.py +++ /dev/null @@ -1,24 +0,0 @@ -from util import http, hook - - -@hook.command(autohelp=False) -def bitcoin(inp, message=None): - """bitcoin -- gets current exchange rate for bitcoins from mtgox""" - data = http.get_json("https://data.mtgox.com/api/2/BTCUSD/money/ticker") - data = data['data'] - ticker = { - 'buy': data['buy']['display_short'].encode('ascii', 'ignore'), - 'high': data['high']['display_short'].encode('ascii', 'ignore'), - 'low': data['low']['display_short'].encode('ascii', 'ignore'), - 'vol': data['vol']['display_short'].encode('ascii', 'ignore'), - } - message("Current: \x0307{!s}\x0f - High: \x0307{!s}\x0f" - " - Low: \x0307{!s}\x0f - Volume: {!s}".format(ticker['buy'], ticker['high'], ticker['low'], ticker['vol'])) - -@hook.command(autohelp=False) -def litecoin(inp, message=None): - """litecoin -- gets current exchange rate for litecoins from BTC-E""" - data = http.get_json("https://btc-e.com/api/2/ltc_usd/ticker") - ticker = data['ticker'] - message("Current: \x0307${!s}\x0f - High: \x0307${!s}\x0f" - " - Low: \x0307${!s}\x0f - Volume: {!s}LTC".format(ticker['buy'],ticker['high'],ticker['low'],ticker['vol_cur'])) diff --git a/plugins/coins.py b/plugins/coins.py new file mode 100644 index 0000000..d566c07 --- /dev/null +++ b/plugins/coins.py @@ -0,0 +1,57 @@ +from util import http, hook + + +@hook.command("butt", autohelp=False) +@hook.command("btc", autohelp=False) +@hook.command(autohelp=False) +def bitcoin(inp): + "bitcoin -- Gets current exchange rate for bitcoins from several exchanges, default is Blockchain. Supports MtGox, Bitpay, Coinbase and BitStamp." + exchanges = { + "blockchain": { + "api_url": "https://blockchain.info/ticker", + "func": lambda data: u"Blockchain // Buy: \x0307${:,.2f}\x0f - Sell: \x0307${:,.2f}\x0f".format(data["USD"]["buy"], \ + data["USD"]["sell"]) + }, + "mtgox": { + "api_url": "https://mtgox.com/api/1/BTCUSD/ticker", + "func": lambda data: u"MtGox // Current: \x0307{}\x0f - High: \x0307{}\x0f - Low: \x0307{}\x0f - Best Ask: \x0307{}\x0f - Volume: {}".format(data['return']['last']['display'], \ + data['return']['high']['display'], data['return']['low']['display'], data['return']['buy']['display'], \ + data['return']['vol']['display']) + }, + "coinbase":{ + "api_url": "https://coinbase.com/api/v1/prices/spot_rate", + "func": lambda data: u"Coinbase // Current: \x0307${:,.2f}\x0f".format(float(data['amount'])) + }, + "bitpay": { + "api_url": "https://bitpay.com/api/rates", + "func": lambda data: u"Bitpay // Current: \x0307${:,.2f}\x0f".format(data[0]['rate']) + }, + "bitstamp": { + "api_url": "https://www.bitstamp.net/api/ticker/", + "func": lambda data: u"BitStamp // Current: \x0307${:,.2f}\x0f - High: \x0307${:,.2f}\x0f - Low: \x0307${:,.2f}\x0f - Volume: {:,.2f} BTC".format(float(data['last']), float(data['high']), float(data['low']), \ + float(data['volume'])) + } + } + + inp = inp.lower() + + if inp: + if inp in exchanges: + exchange = exchanges[inp] + else: + return "Invalid Exchange" + else: + exchange = exchanges["blockchain"] + + data = http.get_json(exchange["api_url"]) + func = exchange["func"] + return func(data) + + +@hook.command(autohelp=False) +def litecoin(inp, message=None): + """litecoin -- gets current exchange rate for litecoins from BTC-E""" + data = http.get_json("https://btc-e.com/api/2/ltc_usd/ticker") + ticker = data['ticker'] + message("Current: \x0307${:,.2f}\x0f - High: \x0307${:,.2f}\x0f" + " - Low: \x0307${:,.2f}\x0f - Volume: {:,.2f} LTC".format(ticker['buy'], ticker['high'], ticker['low'], ticker['vol_cur'])) diff --git a/plugins/minecraft_ping.py b/plugins/minecraft_ping.py index 3745356..423c714 100644 --- a/plugins/minecraft_ping.py +++ b/plugins/minecraft_ping.py @@ -1,6 +1,7 @@ from util import hook import socket import struct +import json try: import DNS @@ -10,50 +11,101 @@ except ImportError: pydns_installed = False -def format_motd(motd): - empty = "" - colors = [u"\x0300,\xa7f", u"\x0301,\xa70", u"\x0302,\xa71", u"\x0303,\xa72", u"\x0304,\xa7c", u"\x0305,\xa74", - u"\x0306,\xa75", u"\x0307,\xa76", u"\x0308,\xa7e", u"\x0309,\xa7a", u"\x0310,\xa73", u"\x0311,\xa7b", - u"\x0312,\xa71", u"\x0313,\xa7d", u"\x0314,\xa78", u"\x0315,\xa77", u"\x02,\xa7l", u"\x0310,\xa79", - u"\x09,\xa7o", u"\x13,\xa7m", u"\x0f,\xa7r", u"\x15,\xa7n"] - for s in colors: - lcol = s.split(",") - motd = motd.replace(lcol[1], lcol[0]) - motd = motd.replace(u"\xa7k", empty) +mccolors = [u"\x0300,\xa7f", u"\x0301,\xa70", u"\x0302,\xa71", u"\x0303,\xa72", u"\x0304,\xa7c", u"\x0305,\xa74", + u"\x0306,\xa75", u"\x0307,\xa76", u"\x0308,\xa7e", u"\x0309,\xa7a", u"\x0310,\xa73", u"\x0311,\xa7b", + u"\x0312,\xa71", u"\x0313,\xa7d", u"\x0314,\xa78", u"\x0315,\xa77", u"\x02,\xa7l", u"\x0310,\xa79", + u"\x09,\xa7o", u"\x13,\xa7m", u"\x0f,\xa7r", u"\x15,\xa7n"] + + +def mc_color_format(motd): + for colorcombo in mccolors: + colorarray = colorcombo.split(",") + motd = motd.replace(colorarray[1], colorarray[0]) + motd = motd.replace(u"\xa7k", "") return motd -def mcping_connect(host, port): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +def unpack_varint(s): + d = 0 + i = 0 + while True: + b = ord(s.recv(1)) + d |= (b & 0x7F) << 7 * i + i += 1 + if not b & 0x80: + return d + + +def pack_data(d): + return struct.pack('>b', len(d)) + d + + +def pack_port(i): + return struct.pack('>H', i) + + +def mcping_modern(host, port): + """ pings a server using the modern (1.7+) protocol and returns formatted output """ + # connect to the server + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((host, port)) + + # send handshake + status request + s.send(pack_data("\x00\x00" + pack_data(host.encode('utf8')) + pack_port(port) + "\x01")) + s.send(pack_data("\x00")) + + # read response + unpack_varint(s) # Packet length + unpack_varint(s) # Packet ID + l = unpack_varint(s) # String length + + if not l > 1: + raise Exception + + d = "" + while len(d) < l: + d += s.recv(1024) + + # Close our socket + s.close() + + # Load json and return + data = json.loads(d.decode('utf8')) try: - sock.connect((host, port)) - sock.send('\xfe\x01') - response = sock.recv(1) - print response - - if response[0] != '\xff': - return "Server gave invalid response: " + repr(response) - length = struct.unpack('!h', sock.recv(2))[0] - - values = sock.recv(length * 2).decode('utf-16be') - - data = values.split(u'\x00') # try to decode data using new format - if len(data) == 1: - # failed to decode data, server is using old format - data = values.split(u'\xa7') - message = u"{} - {}/{} players".format(data[0], data[1], data[2]) - else: - # decoded data, server is using new format - message = u"{} \x0f- {} - {}/{} players".format(data[3], data[2], data[4], data[5]) - - sock.close() - return message - - except: - return "Error pinging {}:{}, is it up? Double-check your address!".format(host, str(port)) + version = data["version"]["name"] + desc = data["description"] + max = data["players"]["max"] + online = data["players"]["online"] + except Exception as e: + return "Invalid data: {}; error: {}".format(data, e) + return mc_color_format(u"{}\x0f - {}\x0f - {}/{} players *".format(desc, version, online, max)).replace("\n", u"\x0f - ") -def srvData(domain): +def mcping_legacy(host, port): + """ pings a server using the legacy (1.6 and older) protocol and returns formatted output """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + sock.send('\xfe\x01') + response = sock.recv(1) + print response + if response[0] != '\xff': + return "Server gave invalid response: " + repr(response) + length = struct.unpack('!h', sock.recv(2))[0] + values = sock.recv(length * 2).decode('utf-16be') + data = values.split(u'\x00') # try to decode data using new format + if len(data) == 1: + # failed to decode data, server is using old format + data = values.split(u'\xa7') + message = u"{} - {}/{} players".format(mc_color_format(data[0]), data[1], data[2]) + else: + # decoded data, server is using new format + message = u"{} \x0f- {} - {}/{} players".format(mc_color_format(data[3]), mc_color_format(data[2]), data[4], data[5]) + sock.close() + return message + + +def get_srv_data(domain): + """ takes a domain and finds minecraft SRV records """ DNS.ParseResolvConf() srv_req = DNS.Request(qtype='srv') srv_result = srv_req.req('_minecraft._tcp.{}'.format(domain)) @@ -64,34 +116,64 @@ def srvData(domain): return data -@hook.command -def mcping(inp): - """mcping [:port] - Ping a Minecraft server to check status.""" +def parse_input(inp): + """ takes the input from the mcping command and returns the host and port """ inp = inp.strip().split(" ")[0] - if ":" in inp: host, port = inp.split(":", 1) try: port = int(port) except: - return "error: invalid port!" - return format_motd(mcping_connect(host, port)) + raise Exception("The port '{}' is invalid.".format(port)) + return host, port + if pydns_installed: + srv_data = get_srv_data(inp) + if srv_data: + return str(srv_data[1]), int(srv_data[0]) + return inp, 25565 - else: - host = inp - port = 25565 - rdata = format_motd(mcping_connect(host, port)) - if 'is it up' in rdata: - if pydns_installed: - getdata = srvData(inp) - try: - host = str(getdata[1]) - port = int(getdata[0]) - return format_motd(mcping_connect(host, port)) - except: - return "Error pinging {}, is it up? Double-check your address!".format(inp) - else: - return "Error pinging {}, is it up? Double-check your address!".format(inp) - else: - return rdata +@hook.command +@hook.command("mcp6") +def mcping6(inp): + """mcping6 [:port] - Ping a Minecraft server version 1.6 or smaller to check status.""" + try: + host, port = parse_input(inp) + except Exception as ex: + return ex.args[0] + try: + return mcping_legacy(host, port) + except: + return "The 1.6 server {}:{} looks offline from here.".format(host, port) + + +@hook.command +@hook.command("mcp7") +def mcping7(inp): + """mcping [:port] - Ping a Minecraft server version 1.7 or greater to check status.""" + try: + host, port = parse_input(inp) + except Exception as ex: + return ex.args[0] + try: + return mcping_modern(host, port) + except: + return "The 1.7 server {}:{} looks offline from here.".format(host, port) + + +@hook.command +@hook.command("mcp") +def mcping(inp): + """mcping [:port] - Ping a Minecraft server to check status.""" + try: + host, port = parse_input(inp) + except Exception as e: + return e.args[0] + + try: + return mcping_modern(host, port) + except: + try: + return mcping_legacy(host, port) + except: + return "The server {} ({}:{}) looks offline from here.".format(inp, host, port) diff --git a/plugins/newegg.py b/plugins/newegg.py new file mode 100644 index 0000000..ab667e1 --- /dev/null +++ b/plugins/newegg.py @@ -0,0 +1,92 @@ +from util import hook, http, text, web +import json +import re + +ITEM_URL = "http://www.newegg.com/Product/Product.aspx?Item={}" + +API_PRODUCT = "http://www.ows.newegg.com/Products.egg/{}/ProductDetails" +API_SEARCH = "http://www.ows.newegg.com/Search.egg/Advanced" + +NEWEGG_RE = (r"(?:(?:www.newegg.com|newegg.com)/Product/Product\.aspx\?Item=)([-_a-zA-Z0-9]+)", re.I) + + +def format_item(item): + """ takes a newegg API item object and returns a description """ + title = text.truncate_str(item["Title"], 50) + + # format the rating nicely if it exists + if not item["ReviewSummary"]["TotalReviews"] == "[]": + rating = "Rated {}/5 ({} ratings)".format(item["ReviewSummary"]["Rating"], + item["ReviewSummary"]["TotalReviews"][1:-1]) + else: + rating = "No Ratings" + + if not item["FinalPrice"] == item["OriginalPrice"]: + price = "{FinalPrice}, was {OriginalPrice}".format(**item) + else: + price = item["FinalPrice"] + + tags = [] + + if item["Instock"]: + tags.append("\x02Stock Available\x02") + else: + tags.append("\x02Out Of Stock\x02") + + if item["FreeShippingFlag"]: + tags.append("\x02Free Shipping\x02") + + if item["IsFeaturedItem"]: + tags.append("\x02Featured\x02") + + if item["IsShellShockerItem"]: + tags.append("\x02SHELL SHOCKER®\x02") + + # join all the tags together in a comma seperated string ("tag1, tag2, tag3") + tag_text = u", ".join(tags) + + # create the item URL and shorten it + url = web.try_isgd(ITEM_URL.format(item["NeweggItemNumber"])) + + return u"\x02{}\x02 ({}) - {} - {} - {}".format(title, price, rating, + tag_text, url) + + +@hook.regex(*NEWEGG_RE) +def newegg_url(match): + item_id = match.group(1) + item = http.get_json(API_PRODUCT.format(item_id)) + return format_item(item) + + +@hook.command +def newegg(inp): + """newegg -- Searches newegg.com for """ + + # form the search request + request = { + "PageNumber": 1, + "BrandId": -1, + "NValue": "", + "StoreDepaId": -1, + "NodeId": -1, + "Keyword": inp, + "IsSubCategorySearch": False, + "SubCategoryId": -1, + "Sort": "FEATURED", + "CategoryId": -1, + "IsUPCCodeSearch": False + } + + # submit the search request + r = http.get_json( + 'http://www.ows.newegg.com/Search.egg/Advanced', + post_data = json.dumps(request) + ) + + # get the first result + item = r["ProductListItems"][0] + + return format_item(item) + + diff --git a/plugins/stock.py b/plugins/stock.py index 154e2ff..d80fe05 100755 --- a/plugins/stock.py +++ b/plugins/stock.py @@ -25,7 +25,7 @@ def stock(inp): quote['PercentChange'] = 100 * change / (price - change) - ret = "%(Name)s - %(LastTradePriceOnly)s " \ + ret = "\x02%(Name)s\x02 (\x02%(symbol)s\x02) - %(LastTradePriceOnly)s " \ "\x03%(color)s%(Change)s (%(PercentChange).2f%%)\x03 " \ "Day Range: %(DaysRange)s " \ "MCAP: %(MarketCapitalization)s" % quote diff --git a/plugins/twitter.py b/plugins/twitter.py index 256ff71..cd1b2a0 100755 --- a/plugins/twitter.py +++ b/plugins/twitter.py @@ -35,7 +35,7 @@ def twitter(inp, bot=None): if e[0][0]['code'] == 34: return "Could not find tweet." else: - return "Error {}: {}".format(e[0][0]['code'], e[0][0]['message']) + return u"Error {}: {}".format(e[0][0]['code'], e[0][0]['message']) user = tweet.user @@ -59,21 +59,21 @@ def twitter(inp, bot=None): if e[0][0]['code'] == 34: return "Could not find user." else: - return "Error {}: {}".format(e[0][0]['code'], e[0][0]['message']) + return u"Error {}: {}".format(e[0][0]['code'], e[0][0]['message']) # get the users tweets user_timeline = api.user_timeline(id=user.id, count=tweet_number + 1) # if the timeline is empty, return an error if not user_timeline: - return "The user \x02{}\x02 has no tweets.".format(user.screen_name) + return u"The user \x02{}\x02 has no tweets.".format(user.screen_name) # grab the newest tweet from the users timeline try: tweet = user_timeline[tweet_number] except IndexError: tweet_count = len(user_timeline) - return "The user \x02{}\x02 only has \x02{}\x02 tweets.".format(user.screen_name, tweet_count) + return u"The user \x02{}\x02 only has \x02{}\x02 tweets.".format(user.screen_name, tweet_count) elif re.match(r'^#\w+$', inp): # user is searching by hashtag @@ -88,7 +88,7 @@ def twitter(inp, bot=None): text = " ".join(tweet.text.split()) if user.verified: - prefix = "+" + prefix = u"\u2713" else: prefix = "" @@ -126,17 +126,17 @@ def twuser(inp, bot=None): return "Unknown error" if user.verified: - prefix = "+" + prefix = u"\u2713" else: prefix = "" if user.location: - loc_str = " is located in \x02{}\x02 and".format(user.location) + loc_str = u" is located in \x02{}\x02 and".format(user.location) else: loc_str = "" if user.description: - desc_str = " The users description is \"{}\"".format(user.description) + desc_str = u" The users description is \"{}\"".format(user.description) else: desc_str = "" diff --git a/plugins/youtube.py b/plugins/youtube.py index 4f2675e..3097614 100755 --- a/plugins/youtube.py +++ b/plugins/youtube.py @@ -13,6 +13,10 @@ search_api_url = base_url + 'videos?v=2&alt=jsonc&max-results=1' video_url = "http://youtu.be/%s" +def plural(num=0, text=''): + return "%d %s%s" % (num, text, "s"[num==1:]) + + def get_video_description(video_id): request = http.get_json(api_url.format(video_id)) @@ -34,9 +38,13 @@ def get_video_description(video_id): out += '%dm ' % (length / 60 % 60) out += "%ds\x02" % (length % 60) - if 'rating' in data: - out += ' - rated \x02%.2f/5.0\x02 (%d)' % (data['rating'], - data['ratingCount']) + if 'ratingCount' in data: + likes = plural(int(data['likeCount']), "like") + dislikes = plural(data['ratingCount'] - int(data['likeCount']), "dislike") + + percent = 100 * float(data['likeCount'])/float(data['ratingCount']) + out += ' - {}, {} (\x02{:.1f}\x02%)'.format(likes, + dislikes, percent) if 'viewCount' in data: out += ' - \x02%s\x02 views' % format(data['viewCount'], ",d") @@ -56,16 +64,6 @@ def get_video_description(video_id): return out -def GetInHMS(seconds): - hours = seconds / 3600 - seconds -= 3600 * hours - minutes = seconds / 60 - seconds -= 60 * minutes - if hours == 0: - return "%02d:%02d" % (minutes, seconds) - return "%02d:%02d:%02d" % (hours, minutes, seconds) - - @hook.regex(*youtube_re) def youtube_url(match): return get_video_description(match.group(1))