Merge branch 'develop' into refresh
This commit is contained in:
commit
a78a86602d
7 changed files with 312 additions and 107 deletions
|
@ -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']))
|
57
plugins/coins.py
Normal file
57
plugins/coins.py
Normal file
|
@ -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 <exchange> -- 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']))
|
|
@ -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 <server>[: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 <server>[: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 <server>[: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 <server>[: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)
|
||||
|
|
92
plugins/newegg.py
Normal file
92
plugins/newegg.py
Normal file
|
@ -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 <item name> -- Searches newegg.com for <item name>"""
|
||||
|
||||
# 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)
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 = ""
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
Reference in a new issue