hubot-nba
Version:
A hubot script for NBA stats
441 lines (371 loc) • 11.3 kB
text/coffeescript
# Description:
# A hubot script for NBA stats
#
# Dependencies:
#
# Configuration:
#
# Commands:
# hubot nba player <player name> - view individual stats
# hubot nba team <team name> - view team stats
# hubot nba roster <team name> - view list team's players
# hubot nba coaches <team name> - view list team's coaches
# hubot nba scores - view the scores and schedules of today's games
# hubot nba standings - view Eastern and Western conference standings
# hubot nba hustle - view hustle stat leaders
#
# Author:
# brandly
#
nba = require 'nba'
request = require 'superagent'
cheerio = require 'cheerio'
_ = require 'lodash'
Case = require 'case'
module.exports = (robot) ->
robot.Response::markdown = (text) ->
# Check if we're using Telegram adapter
if robot.adapterName == 'telegram'
# Get the Telegram API instance from the adapter
robot.adapter.bot.sendMessage(
@message.room,
text,
parse_mode: "Markdown"
)
else
@send text
robot.respond /nba player (.*)/i, (res) ->
name = res.match[1]
playerIdFromName name, (error, player) ->
if not player?
res.markdown "Couldn't find player with name \"#{name}\""
return
getPlayerSummary player.PERSON_ID, (error, summary) ->
res.markdown error or summary
robot.respond /nba team (.*)/i, (res) ->
name = res.match[1]
TeamId = nba.teamIdFromName name
if not TeamId?
res.markdown "Couldn't find team with name \"#{name}\""
return
nba.stats.teamStats({ TeamId }).then (data) ->
if not data.length
res.markdown "Couldn't find stats for team \"#{TeamId}\""
return
info = data[0]
res.markdown """
#{info.teamName} (#{info.w}-#{info.l})
#{info.pts}pts, #{info.ast}ast, #{info.reb}reb
"""
, (reason) ->
res.markdown """
Error getting team stats
#{JSON.stringify reason, null, 2}
"""
robot.respond /nba roster (.*)/i, (res) ->
name = res.match[1]
TeamID = nba.teamIdFromName name
if not TeamID?
res.markdown "Couldn't find team with name \"#{name}\""
return
playersFromTeamId TeamID, (error, players) ->
if error?
res.markdown """
Error getting team roster
#{JSON.stringify error, null, 2}
"""
return
listings = players.map (player) ->
if player.DRAFT_YEAR?
pick = "Round #{player.DRAFT_ROUND}, Pick #{player.DRAFT_NUMBER}"
draftDetails = "#{pick} (#{player.DRAFT_YEAR})"
else
draftDetails = "(Undrafted)"
name = "#{player.PLAYER_FIRST_NAME} #{player.PLAYER_LAST_NAME}"
"""
#{name} ##{player.JERSEY_NUMBER} (#{player.POSITION})
#{displayHeight player.HEIGHT} #{player.WEIGHT} lbs
#{player.COLLEGE} | #{draftDetails}
""".trim()
res.markdown listings.join('\n\n')
robot.respond /nba coaches (.*)/i, (res) ->
name = res.match[1]
TeamID = nba.teamIdFromName name
if not TeamID?
res.markdown "Couldn't find team with name \"#{name}\""
return
nba.stats.commonTeamRoster({ TeamID }).then ({ coaches }) ->
listings = coaches.map (coach) ->
"""
#{coach.coachName}, #{coach.coachType}
#{coach.school or ''}
""".trim()
res.markdown listings.join('\n\n')
, (reason) ->
res.markdown """
Error getting team coaches
#{JSON.stringify reason, null, 2}
"""
robot.respond /nba scores/i, (res) ->
getContext = (game) ->
if game.hasBegun
return "#{game.away.score} - #{game.home.score}"
else if game.series
return game.series
else
return "First matchup"
getTeamNames = (game) ->
{ away, home } = game
if game.isOver
homeTeamWon = home.score > away.score
if homeTeamWon
"#{away.name} at *#{home.name}*"
else
"*#{away.name}* at #{home.name}"
else
"#{away.name} at #{home.name}"
getScores (err, scores) ->
response = scores.map (game) ->
"""
#{getTeamNames(game)}
#{game.status} | #{getContext(game)}
"""
res.markdown response.join('\n\n')
robot.respond /nba standing(s?)/i, (res) ->
displayTeam = (t) ->
behind = if t.gamesBehind is '-' then '' else "(#{t.gamesBehind}GB)"
"""
##{t.seed} #{t.name} #{behind}
#{t.wins}W - #{t.losses}L (#{t.winPercent})
"""
getConferenceStandings (err, conferences) ->
response = conferences.map (conference) ->
"""
#{conference.name}
#{conference.teams.map(displayTeam).join('\n\n')}
"""
res.markdown response.join('\n\n\n')
robot.respond /nba hustle/i, (res) ->
hustleLeaders (error, stats) ->
if error?
res.markdown """
Error getting hustle leaders
#{JSON.stringify error, null, 2}
"""
return
commonKeys = [
'playerId'
'playerName'
'teamId'
'teamAbbreviation'
'age'
'rank'
]
statLeaderLists = stats.map (stat) ->
listings = stat.leaders.map (leader) ->
countKey = Object.keys(leader).find (key) ->
!_.includes(commonKeys, key)
{
playerName,
teamAbbreviation
} = leader
"""
#{playerName} (#{teamAbbreviation}) #{leader[countKey]}
"""
"""
> #{stat.name}
#{listings.join '\n'}
"""
res.markdown(statLeaderLists.join '\n\n')
displayAverages = (avg) ->
toTable [
"#{avg.gp} GP"
"#{avg.min} MIN"
"#{avg.pts} PTS"
"#{avg.fgm} FGM"
"#{avg.fga} FGA"
"#{displayPercentage avg.fgPct} FG%"
"#{avg.fG3M} 3PM"
"#{avg.fG3A} 3PA"
"#{displayPercentage avg.fg3Pct} 3P%"
"#{avg.ftm} FTM"
"#{avg.fta} FTA"
"#{displayPercentage avg.ftPct} FT%"
"#{avg.oreb} OREB"
"#{avg.dreb} DREB"
"#{avg.reb} REB"
"#{avg.ast} AST"
"#{avg.stl} STL"
"#{avg.blk} BLK"
]
displayPercentage = (num) ->
(num * 100).toFixed 1
toTable = (stats) ->
columnCount = 2
rowsPerColumn = stats.length / columnCount
listOfColumns = _.range(columnCount).map (index) ->
stats.slice(index * rowsPerColumn, (index + 1) * rowsPerColumn)
paddedColumns = listOfColumns.map padColumn
joinedRows = _.range(paddedColumns[0].length).map (rowIndex) ->
_.range(columnCount)
.map (columnIndex) ->
paddedColumns[columnIndex][rowIndex]
.join ' | '
"```\n#{joinedRows.join '\n'}\n```"
padColumn = (column) ->
widestColumn = Math.max.apply(Math, column.map (item) -> item.length)
column.map (item) ->
while item.length < widestColumn
index = item.indexOf(' ')
item = item.slice(0, index) + ' ' + item.slice(index, item.length)
item
currentScoresUrl = [
'http://data.nba.com',
'/data/5s/v2015/json/mobile_teams/nba'
'/2024/scores/00_todays_scores.json'
].join ''
requestCurrentScores = (cb) ->
request
.get(currentScoresUrl)
.end (err, res) ->
cb err, JSON.parse(res.text)
getScores = (cb) ->
requestCurrentScores (err, data) ->
return cb(err, null) if err?
formattedScores = data.gs.g.map (game) ->
{
hasBegun: !!game.cl
isOver: game.stt is 'Final'
status: buildStatus(game)
away: buildTeam(game.v)
home: buildTeam(game.h)
series: game.lm.seri
}
cb null, formattedScores
buildTeam = (team) ->
{
id: team.tid,
city: team.tc,
name: team.tn,
abbrev: team.ta,
score: team.s
}
buildStatus = (game) ->
if game.stt is 'Final'
return 'Final'
else if not game.cl? or game.cl is '00:00.0'
return game.stt
else
return "#{game.cl} - #{game.stt}"
conferenceStandingsUrl = [
'http://cdn.espn.go.com'
'/core/nba/standings?xhr=1&device=desktop'
].join ''
requestConferenceStandings = (cb) ->
request
.get(conferenceStandingsUrl)
.end (err, res) ->
cb err, JSON.parse(res.text)
getConferenceStandings = (cb) ->
requestConferenceStandings (err, data) ->
return cb(err, null) if err?
conferences = data.content.standings.groups.map buildConference
cb null, conferences
buildConference = (data) ->
{
name: data.name,
teams: data.standings.entries.map buildTeamStanding
}
buildTeamStanding = (data) ->
getStat = (stats, name) ->
matches = stats.filter (stat) -> stat.name is name
return matches[0].displayValue
{ team, stats } = data
return {
name: team.name,
city: team.location,
seed: team.seed,
abbrev: team.abbreviation,
wins: getStat(stats, 'wins'),
losses: getStat(stats, 'losses'),
winPercent: getStat(stats, 'winPercent'),
gamesBehind: getStat(stats, 'gamesBehind')
}
getPlayerProfile = (opts) ->
nba.stats.playerProfile(opts)
.then (profile) ->
regularSeason = profile.seasonTotalsRegularSeason
averages = regularSeason[regularSeason.length - 1]
return {
averages
}
getPlayerSummary = (PlayerID, callback) ->
Promise.all([
nba.stats.playerInfo({ PlayerID }),
getPlayerProfile({ PlayerID })
]).then ([playerInfo, playerProfile]) ->
info = playerInfo.commonPlayerInfo[0]
{ averages } = playerProfile
callback null, """
#{info.displayFirstLast} ##{info.jersey}
#{info.teamCity} #{info.teamName} | #{info.position}
#{displayHeight info.height} #{info.weight} lbs
Season averages
#{displayAverages(averages)}
http://nba.com/stats/player/#{PlayerID}/career
"""
, (reason) ->
callback """
Error getting player stats
#{JSON.stringify reason, null, 2}
"""
displayHeight = (str) ->
[feet, inches] = str.split '-'
"#{feet}'#{inches}\""
hustleLeaders = (callback) ->
nba.stats.playerHustleLeaders().then (val) ->
stats = val.resultSets.map (set) ->
name: Case.title(set.name).replace 'Player ', ''
leaders: set.rowSet.map (row) ->
_.zip(set.headers, row)
.reduce (store, val) ->
store[Case.camel val[0]] = val[1]
return store
, {}
callback null, stats
, (error) -> callback(error)
cachedPlayers = null
fetchPlayers = (cb) ->
if cachedPlayers?
cb null, cachedPlayers
return
fetch('https://www.nba.com/players')
.then((res) -> res.text())
.then((text) ->
$ = cheerio.load text
data = JSON.parse($('#__NEXT_DATA__').text())
players = data.props.pageProps.players.map((player) ->
Object.assign({}, player, {
FULL_NAME: "#{player.PLAYER_FIRST_NAME} #{player.PLAYER_LAST_NAME}"
})
)
cachedPlayers = players
cb null, players
)
.catch (error) -> cb(error)
playerIdFromName = (name, cb) ->
fetchPlayers (err, players) ->
if err?
cb err
return
cb null, players.find((p) ->
p.FULL_NAME.toLowerCase().includes(name.toLowerCase())
)
playersFromTeamId = (teamId, cb) ->
fetchPlayers (err, players) ->
if err?
cb err
return
cb null, players.filter((p) ->
p.TEAM_ID == teamId
)