UNPKG

hubot-vso-scripts

Version:
862 lines (684 loc) 34.9 kB
# Description: # A way to interact with Visual Studio Online. # # Dependencies: # "node-uuid": "~1.4.1" # "hubot": "~2.7.5" # "vso-client": "~0.2.0" # "parse-rss": "~0.1.1" # # Configuration: # HUBOT_VSONLINE_ACCOUNT - The Visual Studio Online account name (Required) # HUBOT_VSONLINE_USERNAME - Alternate credential username (Required in trust mode) # HUBOT_VSONLINE_PASSWORD - Alternate credential password (Required in trust mode) # HUBOT_VSONLINE_APP_ID - Visual Studio Online application ID (Required in impersonate mode) # HUBOT_VSONLINE_APP_SECRET - Visual Studio Online application secret (Required in impersonate mode) # HUBOT_VSONLINE_AUTHORIZATION_CALLBACK_URL - Visual Studio Online application oauth callback (Required in impersonate mode) # # Commands: # hubot vso room defaults - Shows room defaults (e.g. project, etc) # hubot vso room default <key> = <value> - Sets a room default project, etc. # hubot vso builds - Shows a list of build definitions # hubot vso build <build definition number> - Triggers a build # hubot vso create pbi|requirement|bug|feature|impediment|task <title> with description <description> - Creates a work item, and optionally sets a description (repro step for some work item types) # hubot vso assign <work item list> to @me | <user name> - Assigns one more or more work item(s) to you (@me) or a user name specified # hubot vso update work remaining <work item id> to <hours remaining> - Updates work remaining on a work item # hubot vso today - Shows work items you have touched and code commits/checkins you have made today # hubot vso commits [last <number> days] - Shows a list of Git commits you have made in the last day (or specified number of days) # hubot vso checkins [last <number> days] - Shows a list of TFVC checkins you have made in the last day (or specified number of days) # hubot vso projects - Shows a list of projects # hubot vso me - Shows info about your Visual Studio Online profile # hubot vso forget credentials - Removes the access token issued to Hubot when you accepted the authorization request # hubot vso status - Shows status for the Visual Studio Online service # hubot vso help <search text> - Get help from VS related forums about the <search text> # # Notes: Client = require 'vso-client' util = require 'util' uuid = require 'node-uuid' request = require 'request' rssParser = require 'parse-rss' {TextMessage} = require 'hubot' https = require('https') fs = require('fs') ######################################### # Constants ######################################### VSO_TOKEN_CLOSE_TO_EXPIRATION_MS = 120*1000 VSO_STATUS_URL = "http://www.visualstudio.com/support/support-overview-vs" REPOSITORIESIDKEY = "Ids" PROJECTCAPABILITIESKEY = "Capabilities" MAX_COMMENT_SIZE = 77 DEFAULT_API_VERSION = "1.0" ######################################### # Helper class to manage VSOnline brain # data ######################################### class VsoData constructor: (robot) -> ensureVsoData = ()=> robot.logger.debug "Ensuring vso data correct structure" @vsoData ||= {} @vsoData.rooms ||= {} @vsoData.authorizations ||= {} @vsoData.authorizations.states ||= {} @vsoData.authorizations.users ||= {} robot.brain.set 'vsonline', @vsoData # try to read vso data from brain @loaded = false @vsoData = robot.brain.get 'vsonline' if not @vsoData ensureVsoData() # and now subscribe for the onload for cases where brain is loading yet robot.brain.on 'loaded', => return if @loaded is true robot.logger.debug "Brain loaded. Recreate vso data with the data loaded from brain" @loaded = true @vsoData = robot.brain.get 'vsonline' ensureVsoData() else ensureVsoData() getInternalKey: (key, metadataKey) -> return key + (metadataKey || "") roomDefaults: (room) -> @vsoData.rooms[room] ||= {} getRoomDefault: (room, key, metadataKey) -> @vsoData.rooms[room]?[@getInternalKey key, metadataKey] addRoomDefault: (room, key, metadataKey, value) -> @roomDefaults(room)[@getInternalKey key, metadataKey] = value getOAuthTokenForUser: (userId) -> @vsoData.authorizations.users[userId] addOAuthTokenForUser: (userId, token) -> @vsoData.authorizations.users[userId] = token removeOAuthTokenForUser: (userId) -> delete @vsoData.authorizations.users[userId] addOAuthState: (state, stateData) -> @vsoData.authorizations.states[state] = stateData getOAuthState: (state) -> @vsoData.authorizations.states[state] removeOAuthState: (state) -> delete @vsoData.authorizations.states[state] module.exports = (robot) -> # The definition of team defaults. teamDefaultsList = { "project": help: "Project not set. Set with hubot vso room default project = <project name or ID>" callback : (msg, configName, wantedProjectName) -> setDefaultProject msg, configName, wantedProjectName "area path": help: "Area path not set. Set with hubot vso room default area path = <area path>" "repositories": help: "Repositories not set. Set with hubot vso room default repositories = <1 or more, comma-separated repository IDs or names>" callback : (msg, configName, wantedRepositories) -> setDefaultRepositories msg, configName, wantedRepositories } ######################################### # Parameter Helpers ######################################### # checks if a given value is in the set if it is, returns it # otherwise return undefined validateParameterValue = (value, allowedValues, parameterName) -> return value if value in allowedValues robot.logger.error "#{value} not in list of allowed values for parameter #{parameterName}" undefined ######################################### # SSL Configuration ######################################### configureSSL = () -> robot.logger.debug "Configuring SSL in VSO scripts" unless SSLPrivateKeyPath? and SSLCertKeyPath? robot.logger.error "not enough parameters to enable SSL. I need private key and certificate. disabling impersonate mode" impersonate = false return sslOptions = { requestCert: SSLRequestCertificate, rejectUnauthorized: SSLRejectUnauthorized, key: fs.readFileSync(SSLPrivateKeyPath), cert: fs.readFileSync(SSLCertKeyPath) } if (SSLCACertPath?) sslOptions.ca = ca: fs.readFileSync(SSLCACertPath) https.createServer(sslOptions, robot.router).listen(SSLPort) # Required env variables account = process.env.HUBOT_VSONLINE_ACCOUNT accountCollection = process.env.HUBOT_VSONLINE_COLLECTION_NAME || "DefaultCollection" # Optional env variables to allow override a different environment environmentDomain = process.env.HUBOT_VSONLINE_ENV_DOMAIN || "visualstudio.com" # Required env variables to run in trusted mode username = process.env.HUBOT_VSONLINE_USERNAME password = process.env.HUBOT_VSONLINE_PASSWORD ## Variables to support SSL (optional) SSLEnabled = process.env.HUBOT_VSONLINE_SSL_ENABLE || false SSLPort = process.env.HUBOT_VSONLINE_SSL_PORT || 443 SSLPrivateKeyPath = process.env.HUBOT_VSONLINE_SSL_PRIVATE_KEY_PATH SSLCertKeyPath = process.env.HUBOT_VSONLINE_SSL_CERT_KEY_PATH SSLRequestCertificate = process.env.HUBOT_VSONLINE_SSL_REQUESTCERT || false SSLRejectUnauthorized = process.env.HUBOT_VSONLINE_SSL_REJECTUNAUTHORIZED || false SSLCACertPath = process.env.HUBOT_VSONLINE_SSL_CA_KEY_PATH # Required env variables to run with OAuth (impersonate mode) appId = process.env.HUBOT_VSONLINE_APP_ID appSecret = process.env.HUBOT_VSONLINE_APP_SECRET oauthCallbackUrl = process.env.HUBOT_VSONLINE_AUTHORIZATION_CALLBACK_URL # OAuth optional env variables spsBaseUrl = process.env.HUBOT_VSONLINE_BASE_VSSPS_URL or "https://app.vssps.visualstudio.com" authorizedScopes = process.env.HUBOT_VSONLINE_AUTHORIZED_SCOPES or "vso.build_execute vso.work_write vso.code" # replies formatting replyFormat = (validateParameterValue process.env.HUBOT_VSONLINE_REPLY_FORMAT, ["plaintext", "html","markdown"], "HUBOT_VSONLINE_REPLY_FORMAT") or "plaintext" accountBaseUrl = "https://#{account}.#{environmentDomain}" impersonate = if appId then true else false robot.logger.info "VSOnline scripts running with impersonate set to #{impersonate} and replying with messages in format #{replyFormat}" if impersonate oauthCallbackPath = require('url').parse(oauthCallbackUrl).path accessTokenUrl = "#{spsBaseUrl}/oauth2/token" authorizeUrl = "#{spsBaseUrl}/oauth2/authorize" configureSSL() if SSLEnabled vsoData = new VsoData(robot) robot.on 'error', (err, msg) -> robot.logger.error "Error in robot: #{util.inspect(err)}" ######################################### # OAuth helper functions ######################################### sortScope = (scopes) -> return "" if not scopes return (scopes.split " ").sort().join " " needsVsoAuthorization = (msg) -> return false unless impersonate userToken = vsoData.getOAuthTokenForUser(msg.envelope.user.id) console.log JSON.stringify userToken return not userToken or (sortScope(userToken.scope)) != (sortScope(authorizedScopes)) or appId != userToken.appId buildVsoAuthorizationUrl = (state)-> "#{authorizeUrl}?\ client_id=#{appId}\ &response_type=Assertion&state=#{state}\ &scope=#{escape(authorizedScopes)}\ &redirect_uri=#{escape(oauthCallbackUrl)}" askForVsoAuthorization = (msg) -> state = uuid.v1().toString() vsoData.addOAuthState state, createdAt: new Date envelope: msg.envelope vsoAuthorizeUrl = buildVsoAuthorizationUrl state return reply msg, "You must authorize Hubot to interact with Visual Studio Online on your behalf: " + formatReplyUrl vsoAuthorizeUrl, "authorize" getVsoOAuthAccessToken = ({user, assertion, refresh, success, error}) -> tokenOperation = if refresh then Client.refreshToken else Client.getToken tokenOperationCallback = (err, res) -> unless err or res.Error? token = res expires_at = new Date expires_at.setTime( expires_at.getTime() + parseInt(token.expires_in, 10)*1000) token.expires_at = expires_at token.appId = appId vsoData.addOAuthTokenForUser(user.id, token) success(err, res) if typeof success is "function" else robot.logger.error "Error getting VSO oauth token: #{util.inspect(err or res.Error)}" error(err, res) if typeof error is "function" tokenOperation appSecret, assertion, oauthCallbackUrl, tokenOperationCallback, accessTokenUrl accessTokenExpired = (user) -> token = vsoData.getOAuthTokenForUser(user.id) expiresAt = new Date token.expires_at now = new Date return (expiresAt - now) < VSO_TOKEN_CLOSE_TO_EXPIRATION_MS ######################################### # work items helper functions ######################################### getField = (workItem, wi_refName) -> return workItem.fields[wi_refName] if workItem.fields[wi_refName] return null addFieldChange = (operations, wi_refName, val, operation = "add") -> operation = path : "/fields/#{wi_refName}" op : operation value : val operations.push operation ######################################### # VSOnline helper functions ######################################### createVsoClient = ({url, collection, user, apiVersion}) -> url ||= accountBaseUrl collection ||= accountCollection apiVersion || = DEFAULT_API_VERSION if impersonate token = vsoData.getOAuthTokenForUser user.id Client.createOAuthClient url, collection, token.access_token, { spsUri: spsBaseUrl , apiVersion : apiVersion } else Client.createClient url, collection, username, password, {apiVersion : apiVersion} runVsoCmd = (msg, {url, collection, cmd, apiVersion}) -> return askForVsoAuthorization(msg) if needsVsoAuthorization(msg) user = msg.envelope.user vsoCmd = () -> url ||= accountBaseUrl collection ||= accountCollection client = createVsoClient url: url, collection: collection, user: user, apiVersion: apiVersion cmd(client) if impersonate and accessTokenExpired(user) robot.logger.info "VSO token expired for user #{user.id}. Let's refresh" token = vsoData.getOAuthTokenForUser(user.id) getVsoOAuthAccessToken user: user assertion: token.refresh_token refresh: true success: vsoCmd error: (err, res) -> reply msg, "Your authorization to Hubot has been revoked or has expired." else vsoCmd() handleVsoError = (msg, err) -> reply msg, "Unable to execute command: #{escapeIfNecessary util.inspect(err)}" if err ######################################### # Message reply helper functions ######################################### escapeHTML = (s) -> ("" + s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') escapeIfNecessary = (text) -> return escapeHTML text if replyFormat == "html" return text formatReplyUrl = (url, text, options = {}) -> return "[#{text}](#{url})" if replyFormat == "markdown" return "<a href='#{url}'>#{escapeHTML text}</a>" if replyFormat == "html" # from this forward only formats that do not support links return "#{text} #{url}" if options.prependText return url reply = (msg, text) -> text = text.replace "\n","<br />" if replyFormat == "html" msg.reply text ######################################### # Room defaults helper functions ######################################### # Gets the room default value and sends the user a message # if the value is not set. # The metadataKey is optional. If the metatada has been requested # and there is no value set, the user will be asked to reenter the # room default key again. # We don't use a single object for the room default to be backward compatible checkRoomDefault = (msg, key, metadataKey) -> val = vsoData.getRoomDefault msg.envelope.room, key, metadataKey unless val help = teamDefaultsList[key]?.help or "Room default '#{key}' not set." if metadataKey help = "I am sorry but you have old information for this room default value. You wll have to set it up again\n#{help}" reply msg, help return val setRoomDefault = (msg, configName, value, metadataKey) -> vsoData.addRoomDefault msg.envelope.room, configName, metadataKey, value reply msg, "Room default #{configName} is now set to #{escapeIfNecessary value}" setDefaultProject = (msg, configName, wantedTeamProject) -> runVsoCmd msg, cmd: (client) -> client.getProject wantedTeamProject, true, (err,projectInfo) -> return handleVsoError msg, err if err if projectInfo.state.toLowerCase() != 'wellformed' return reply msg, "Invalid project. Current State #{projectInfo.state}" vsoData.addRoomDefault msg.envelope.room, configName, PROJECTCAPABILITIESKEY, projectInfo.capabilities setRoomDefault msg, configName, wantedTeamProject setDefaultRepositories = (msg, configName, wantedRepositories) -> runVsoCmd msg, cmd: (client) -> client.getRepositories null, (err,repositories) -> return handleVsoError msg, err if err if repositories.length == 0 reply msg, "No Git repositories found. No default is being set" else wantedRepositoriesList = wantedRepositories.split "," filteredRepoList = [] filteredRepoNameList = [] for repo in repositories if wantedRepositoriesList.indexOf(repo.id) != -1 or wantedRepositoriesList.indexOf(repo.name) != -1 filteredRepoList.push { "id" : repo.id "name" : repo.name } filteredRepoNameList.push repo.name if filteredRepoList.length == 0 reply msg, "No Git repositories found with the names or ids specified.\nNo default value changed" else vsoData.addRoomDefault msg.envelope.room, configName, REPOSITORIESIDKEY, filteredRepoList setRoomDefault msg, configName, filteredRepoNameList.join "," ######################################### # OAuth call back endpoint ######################################### if impersonate then robot.router.get oauthCallbackPath, (req, res) -> # check state argument state = req?.query?.state return res.send(400, "Invalid state") unless state and stateData = vsoData.getOAuthState(state) # check code argument code = req?.query?.code return res.send(400, "Missing code parameter") unless code getVsoOAuthAccessToken user: stateData.envelope.user, assertion: code, refresh: false, success: -> res.send """ <html> <body> <p>Great! You've authorized Hubot to perform tasks on your behalf. <p>You can now close this window.</p> </body> </html>""" vsoData.removeOAuthState state robot.receive new TextMessage stateData.envelope.user, stateData.envelope.message.text error: (err, resVso) -> robot.logger.error "Failed to get OAuth access token: " + util.inspect(err or resVso.Error) res.send """ <html> <body> <p>Ooops! It wasn't possible to get an OAuth access token for you.</p> <p>Error returned from VSO: #{util.inspect(err or resVso.Error)}</p> </body> </html>""" ######################################### # Profile related commands ######################################### robot.respond /vso me(\?)*/i, (msg) -> unless impersonate return reply msg, "Hubot is not running in impersonation mode." runVsoCmd msg, cmd: (client) -> client.getCurrentProfile (err, res) -> return handleVsoError msg, err if err reply msg, "Your name is #{res.displayName} and your email is #{escapeIfNecessary res.emailAddress}" robot.respond /vso forget credentials/i, (msg) -> unless impersonate return reply msg, "Hubot is not running in impersonation mode." vsoData.removeOAuthTokenForUser msg.envelope.user.id reply msg, "Hubot has removed your credentials and is no longer able to act on your behalf." ######################################### # Room defaults related commands ######################################### robot.respond /vso room defaults/i, (msg)-> defaults = vsoData.roomDefaults msg.envelope.room reply = "Defaults for this room:\n" reply += "#{key} is #{defaults?[key] or '{not set}'} \n" for key of teamDefaultsList reply msg, reply robot.respond /vso room default ([\w]+)\s*=\s*(.*)\s*$/i, (msg) -> configName = msg.match[1] value = msg.match[2] return reply msg, "This is not a known room setting: #{msg.match[1]}" unless configName of teamDefaultsList if teamDefaultsList[configName]?.callback teamDefaultsList[configName].callback msg, configName, value else setRoomDefault msg, configName, value robot.respond /vso projects/i, (msg) -> runVsoCmd msg, cmd: (client) -> client.getProjects (err, projects) -> return handleVsoError msg, err if err projectsReply = "Projects in account #{account}:\n" projectsReply += (escapeIfNecessary p.name) + "\n" for p in projects reply msg, projectsReply ######################################### # Build related commands ######################################### robot.respond /vso builds/i, (msg) -> runVsoCmd msg, cmd: (client) -> definitions=[] client.getBuildDefinitions (err, buildDefinitions) -> return handleVsoError msg, err if err if buildDefinitions.length == 0 reply msg, "No build definitions have been configured (or are visible to you)" else definitions.push "Build definitions in account #{account}:" for build in buildDefinitions definitions.push "{escapeIfNecessary build.name} (#{build.id})" reply msg, definitions.join "\n" robot.respond /vso build (.*)/i, (msg) -> buildId = msg.match[1] runVsoCmd msg, cmd: (client) -> buildRequest = definition: id: buildId reason: 'Manual' priority : 'Normal' client.queueBuild buildRequest, (err, buildResponse) -> return handleVsoError msg, err if err reply msg, "A build has been queued (hope you don't break it): " + buildResponse.url ######################################### # WIT related commands ######################################### robot.respond /vso assign (\d+(,\d+)*) to (.*)/i, (msg) -> idsList = msg.match[1] assignTo = msg.match[3].trim() if assignTo.toLowerCase() == "@me" assignTo = msg.envelope.user.displayName for id in idsList.split "," assignWorkItemToUser msg, id,assignTo robot.respond /vso update work remaining (\d+) to (\d+)/i, (msg) -> id=msg.match[1] workRemaining = msg.match[2] operations = [] addFieldChange operations, "Microsoft.VSTS.Scheduling.RemainingWork", workRemaining runVsoCmd msg, cmd: (client) -> client.updateWorkItem id, operations, (err, result) -> return handleVsoError msg, err if err if result.message reply msg, "Failed to update remaining work for ##{id} to #{workRemaining}. \nError: #{result.message}" else reply msg, "Updated remaining work to #{workRemaining} for work item #{formatReplyUrl result._links.html.href, '#'+id}" robot.respond /vso create (PBI|Requirement|Task|Feature|Impediment|Bug) (?:(?:(.*) with description($|[\s\S]+)?)|(.*))/im, (msg) -> return unless project = checkRoomDefault msg, "project" return unless projectCapabilities = checkRoomDefault msg, "project", PROJECTCAPABILITIESKEY title = msg.match[2] || msg.match[4] description = msg.match[3] || "" operations = [] workItemType = "" description = description.replace(/\n/g,"<br/>") if description addFieldChange operations, "System.Title", title switch msg.match[1].toLowerCase() when "pbi" workItemType = "Product Backlog Item" addFieldChange operations, "System.Description", description when "requirement" workItemType = "Requirement" addFieldChange operations, "System.Description", description when "task" workItemType = "Task" addFieldChange operations, "System.Description", description when "feature" workItemType = "Feature" addFieldChange operations, "System.Description", description when "impediment" workItemType = "Impediment" addFieldChange operations, "System.Description", description when "bug" workItemType = "Bug" addFieldChange operations, "Microsoft.VSTS.TCM.ReproSteps", description runVsoCmd msg, cmd: (client) -> client.createWorkItem operations, project, workItemType, (err, createdWorkItem) -> return handleVsoError msg, err if err if replyFormat == "plaintext" reply msg, "Work item #" + createdWorkItem.id + " created on project #{project}: " + createdWorkItem._links.html.href else reply msg, "Work item #{formatReplyUrl createdWorkItem._links.html.href, '#'+createdWorkItem.id} created on project #{project}" robot.respond /vso today/i, (msg) -> return unless project = checkRoomDefault msg, "project" return unless projectCapabilities = checkRoomDefault msg, "project", PROJECTCAPABILITIESKEY if projectCapabilities.versioncontrol.sourceControlType == 'Git' projectHasGitRepo = true return unless repositories = checkRoomDefault msg, "repositories", REPOSITORIESIDKEY runVsoCmd msg, cmd: (client) -> #TODO - we need to change to get the user profile from VSO myuser = msg.message.user.displayName wiql="\ select [System.Id], [System.WorkItemType], [System.Title] \ from WorkItems where [System.ChangedDate] = @today \ and [System.ChangedBy] = " + getWIQLUserIdentityFor msg if projectHasGitRepo? getCommitsForUser repositories, 1, msg, (pushes, repo) -> numPushes = Object.keys(pushes).length mypushes = [] if numPushes > 0 mypushes.push "Here are your commits in Git repository " + repo.name + ":" for push in pushes mypushes.push formatGitCommit(push, repo) reply msg, mypushes.join "\n" else reply msg, "No code commits found for you today on Git repository " + repo.name else itemPath = "$/#{project}" getCheckinsForUser itemPath, 1, msg, (checkins) -> if checkins.length == 0 reply msg, "No code checkins found for you today on #{escapeIfNecessary itemPath}" else mycheckins = [] mycheckins.push "Here are your checkins in #{project} team project :" for checkin in checkins mycheckins.push formatTfvcCommit(checkin) reply msg, mycheckins.join "\n" workItems = [] client.getWorkItemIds wiql, project, (err, ids) -> return handleVsoError msg, err if err numWorkItems = Object.keys(ids).length if numWorkItems > 0 workItemIds=[] workItemIds.push id for id in ids client.getWorkItemsById workItemIds, null, null, null, (err, items) -> return handleVsoError msg, err if err if items and items.length > 0 workItems.push "Here are the work items you have touched today on project " + project + ":" for workItem in items title = getField workItem, "System.Title" witType = getField workItem, "System.WorkItemType" workItems.push witType + " #" + workItem.id + ": " + title if title? and witType? reply msg, workItems.join "\n" else reply msg, "You have not touched any work items on project " + project + " today." robot.respond /vso commits *(last (\d+))?/i, (msg) -> return unless checkRoomDefault msg, "repositories" repositories = checkRoomDefault msg, "repositories", REPOSITORIESIDKEY getCommitsForUser repositories, (if msg.match.length > 2 and msg.match[2] then msg.match[2] else 1), msg, (pushes, repo) -> numPushes = Object.keys(pushes).length mypushes=[] if numPushes > 0 mypushes.push "Here are your commits in repo " + repo.name + ":" for push in pushes console.log push mypushes.push formatGitCommit(push, repo) reply msg, mypushes.join "\n" else reply msg, "No code commits found for you on Git repository " + repo.name robot.respond /vso checkins *(last (\d+))?/i, (msg) -> return unless project = checkRoomDefault msg, "project" return unless projectCapabilities = checkRoomDefault msg, "project", PROJECTCAPABILITIESKEY return reply msg, "#{project} team project is not using Team Foundation version control" if projectCapabilities.versioncontrol.sourceControlType != 'Tfvc' itemPath = "$/#{project}" lastDays = if msg.match.length > 2 and msg.match[2] then msg.match[2] else 1 getCheckinsForUser itemPath, lastDays, msg, (checkins) -> if checkins.length == 0 reply msg, "No code checkins found for you in #{escapeIfNecessary itemPath} for the last #{lastDays} day(s)." else mycheckins = [] mycheckins.push "Here are your checkins in #{project} team project for the last #{lastDays} day(s):" for checkin in checkins mycheckins.push formatTfvcCommit(checkin) reply msg, mycheckins.join "\n" formatGitCommit = (checkin, repo) -> if checkin.comment.length > MAX_COMMENT_SIZE comment = checkin.comment.substring(0,MAX_COMMENT_SIZE) + "..." else comment = checkin.comment webUrl = accountBaseUrl + "/" + accountCollection + "/_git/" + repo.name + "/commit/" + checkin.commitId return comment + " " + webUrl formatTfvcCommit = (checkin) -> if checkin.comment?.length > MAX_COMMENT_SIZE comment = checkin.comment.substring(0,MAX_COMMENT_SIZE) + "..." else comment = checkin.comment || "" return "#{checkin.changesetId} - #{comment}" getCommitsForUser = (repositories, sinceDays, msg, callback) -> runVsoCmd msg, cmd: (client) -> #TODO - we need to change to get the user profile from VSO myuser = msg.message.user.displayName dateToSearchFrom = getStartDate(sinceDays) if repositories.length == 0 reply msg, "No Git repositories found." else # use forEach to have a closure for repo repositories.forEach (repo) -> client.getCommits repo.id, null, myuser, null, dateToSearchFrom, (err,commits) -> return handleVsoError msg, err if err callback commits, repo getCheckinsForUser = (itemPath, sinceDays, msg, callback) -> runVsoCmd msg, cmd: (client) -> myuser = msg.message.user.displayName dateToSearchFrom = getStartDate(sinceDays) client.getChangeSets { itemPath : itemPath, fromDate : dateToSearchFrom, author : myuser, maxCommentLength : MAX_COMMENT_SIZE + 1}, (err,checkins) -> return handleVsoError msg, err if err callback checkins getWIQLUserIdentityFor = (msg) -> if impersonate return "@me" else return "'" + msg.envelope.user.displayName.replace("'","''") + "'" assignWorkItemToUser = (msg, id, assignTo) -> runVsoCmd msg, cmd: (client) -> client.getWorkItemsById id, ["System.Rev", "System.AssignedTo"], (err, items) -> return handleVsoError msg, err if err return reply msg, "Couldn't find work item #{id}" if items.length == 0 workItem = items[0] currentAssignedTo = getField workItem, "System.AssignedTo" if currentAssignedTo and currentAssignedTo.toUpperCase() == assignTo.toUpperCase() reply msg, "Work item ##{id} is already assigned to #{escapeIfNecessary currentAssignedTo}" else operations = [] addFieldChange operations, "System.AssignedTo", assignTo runVsoCmd msg, cmd: (client) -> client.updateWorkItem id, operations, (err, result) -> return handleVsoError msg, err if err if result.message reply msg, "Failed to assign ##{id} to #{escapeIfNecessary assignTo}. Check if the user exists.\nError: #{escapeIfNecessary result.message}" else reply msg, "Assigned to #{escapeIfNecessary assignTo} work item #{formatReplyUrl result._links.html.href, '#' + id}" ######################################### # Visual Studio Online Status related commands ######################################### robot.respond /vso status/i, (msg) -> request "https://www.windowsazurestatus.com/odata/ServiceCurrentIncidents?api-version=1.0&$filter=startswith(Name,'#{escape("Visual Studio")}')" , (err, response, body) -> if(err) robot.logger.error "Error getting status: #{util.inspect(err)}" reply msg, "Unable to get the current status of Visual Studio Online. " + formatReplyUrl VSO_STATUS_URL,"Visit", {prependText : true} else if response.statusCode == 200 status = JSON.parse body serviceStatusResponse = "Here is the current status of Visual Studio Online:\n" for vsoService in status.value serviceStatusResponse += vsoService.Name + " (" + vsoService.Status + ")\n" serviceStatusResponse += formatReplyUrl VSO_STATUS_URL, "Full details", {prependText : true} reply msg, serviceStatusResponse else reply msg, "Failed to get Visual Studio Online status. HTTP error code was " + response.statusCode ######################################### # MSDN related commands ######################################### robot.respond /vso help (.*)/i, (msg) -> searchText = msg.match[1] url = getRSSSearchUrl searchText rssParser url, (err,rss) -> if(err) robot.logger.error "error searching MSDN " + err reply msg, "Failed to get Visual Studio Online help. Error: " + err else if rss.length == 0 reply msg, "No results were found." else if rss.length > 5 then rss = rss[0...5] searchResults = "Here are the top search results for '#{escapeIfNecessary msg.match[1]}':\n\n" index = 1 for item in rss searchResults += "#{index}. #{formatReplyUrl item.link, item.title, { prependText : true}}\n" index++ searchResults += "\n" + formatReplyUrl (getSearchUrl searchText), "Full results",{ prependText : true} reply msg, searchResults getRSSSearchUrl = (searchString) -> return "http://social.msdn.microsoft.com/search/en-US/feed?format=RSS&theme=vscom&refinement=198%2c234&query=#{escape(searchString)}" getSearchUrl = (searchString) -> return "http://social.msdn.microsoft.com/Search/en-US/vscom?Refinement=198,234&emptyWatermark=true&ac=4&query=#{escape(searchString)}" ######################################### # Unhandled VSO command ######################################### robot.catchAll (msg) -> return unless msg.message.text.toLowerCase().indexOf(" vso ") isnt -1 msg.send """This command was not understood: #{msg.message.text}. Run 'hubot help vso' to see a list of Visual Studio Online commands.""" getStartDate = (numDays) -> date = new Date() date.setDate(date.getDate() - numDays) date.setUTCHours(0,0,0,0) date.toISOString()