Removed NotificationBot (EOL) (#6510)
This commit is contained in:
Родитель
823c008cc1
Коммит
7f88c14385
|
@ -1,16 +0,0 @@
|
|||
# General
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
#Secrets
|
||||
.env
|
||||
appsettings.json
|
||||
|
||||
# Azure Function
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
||||
# Bot
|
||||
bin/
|
||||
obj/
|
||||
Properties/
|
|
@ -1,7 +0,0 @@
|
|||
GitHubToken=
|
||||
|
||||
MicrosoftAppId=
|
||||
MicrosoftAppPassword=
|
||||
BotBaseUrl=
|
||||
|
||||
UseTestRepo=true
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"version": "2.0"
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
const runTracker = require('./src');
|
||||
|
||||
const path = require('path');
|
||||
const ENV_FILE = path.join(__dirname, '.env');
|
||||
require('dotenv').config({ path: ENV_FILE });
|
||||
process.env.BotBaseUrl = 'http://localhost:3978';
|
||||
|
||||
runTracker(console);
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"name": "support-tracker-function",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start:local": "node local.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.0",
|
||||
"botframework-connector": "^4.10.0-preview-140956",
|
||||
"dotenv": "^8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.5.5",
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/node": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5"
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/proxies",
|
||||
"proxies": {}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
const globallyTrackedLabels = [
|
||||
'customer-reported'
|
||||
]
|
||||
|
||||
const repositories = [
|
||||
{ org: "Microsoft", repo: "botbuilder-azure", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botbuilder-cognitiveservices", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botbuilder-dotnet", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botbuilder-java", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botbuilder-js", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botbuilder-python", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botbuilder-samples", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botbuilder-tools", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botbuilder-v3", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botframework-emulator", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botframework-directlinejs", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botframework-solutions", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botframework-services", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botframework-sdk", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botframework-composer", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botframework-cli", labels: globallyTrackedLabels },
|
||||
{ org: "Microsoft", repo: "botframework-webchat", labels: globallyTrackedLabels },
|
||||
{ org: "MicrosoftDocs", repo: "bot-docs", labels: globallyTrackedLabels },
|
||||
]
|
||||
|
||||
if (process.env.UseTestRepo === 'true') repositories.push({ org: "mdrichardson", repo: "testRepoForIssueNotificationBot", labels: globallyTrackedLabels });
|
||||
|
||||
module.exports.GitHub = {
|
||||
repositories,
|
||||
source: 'GitHub'
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"bindings": [
|
||||
{
|
||||
"name": "myTimer",
|
||||
"type": "timerTrigger",
|
||||
"direction": "in",
|
||||
"schedule": "0 */30 * * * *"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
const { GitHubService } = require('./services');
|
||||
const { GitHub } = require('./config');
|
||||
|
||||
module.exports = async function (context) {
|
||||
context.log("Starting Processes")
|
||||
const gitHub = new GitHubService(GitHub, context);
|
||||
await gitHub.process();
|
||||
context.log("Finished Processes")
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
const axios = require('axios');
|
||||
const { MicrosoftAppCredentials } = require('botframework-connector');
|
||||
const path = require('path');
|
||||
const ENV_FILE = path.join(__dirname, '.env');
|
||||
require('dotenv').config({ path: ENV_FILE });
|
||||
|
||||
module.exports = class BotService {
|
||||
constructor(context) {
|
||||
this.context = context;
|
||||
this.credentials = new MicrosoftAppCredentials(process.env.MicrosoftAppId, process.env.MicrosoftAppPassword);
|
||||
this.client = axios.create({
|
||||
baseURL: process.env.BotBaseUrl
|
||||
});
|
||||
}
|
||||
|
||||
async sendData(body) {
|
||||
const token = await this.credentials.getToken();
|
||||
const response = await this.client.post('/api/data', body, {
|
||||
headers: { Authorization: `Bearer ${ token }` }
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
this.context.error(JSON.stringify(response, null, 2));
|
||||
} else {
|
||||
this.context.log(`Successfully sent GitHub data to the bot. Response Code: ${ response.status }`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
const axios = require('axios');
|
||||
const BotService = require('./botService');
|
||||
|
||||
module.exports = class GitHubService {
|
||||
|
||||
constructor({ repositories, source }, context) {
|
||||
this.repositories = repositories;
|
||||
this.source = source;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
async process() {
|
||||
try {
|
||||
let issues = await Promise.all(this.repositories.map(async repository => await this.getIssues(repository)));
|
||||
issues = issues.filter(issueArray => issueArray.length > 0).flat().flat();
|
||||
|
||||
const groupedIssues = this.groupIssues(issues);
|
||||
|
||||
const bot = new BotService(this.context);
|
||||
await bot.sendData(groupedIssues);
|
||||
|
||||
} catch ({ message }) {
|
||||
this.context.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
async getIssues({ org, repo, labels, ignoreLabels = [] }) {
|
||||
|
||||
const config = {
|
||||
method: 'POST',
|
||||
url: 'https://api.github.com/graphql',
|
||||
headers: { Authorization: `Bearer ${process.env.GitHubToken}`, 'Content-Type': 'application/json'}
|
||||
};
|
||||
|
||||
if (!!labels) {
|
||||
const issues = await labels.reduce(async (result, label) => {
|
||||
const query = this.getQuery(`repo:${org}/${repo} is:open is:issue label:${label} ${ignoreLabels.map(ignore => `-label:'${ignore}'`).join(' ')}`);
|
||||
const response = await axios({...config, data: query });
|
||||
if (response.data && response.data.errors && response.data.errors.length > 0) {
|
||||
this.context.error(JSON.stringify(response.data.errors, null, 2));
|
||||
return;
|
||||
}
|
||||
const { data: { data: { search: { edges: issues }}}} = response;
|
||||
if (issues.length > 0) {
|
||||
result.push(issues);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
return issues;
|
||||
} else {
|
||||
const query = this.getQuery(`repo:${org}/${repo} is:issue ${ignoreLabels.map(ignore => `-label:${ignore}`).join(' ')}`);
|
||||
const { data: { data: { search: { edges: issues }}}} = await axios({...config, data: query });
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
|
||||
// Go here for help creating/editing query: https://developer.github.com/v4/explorer/
|
||||
getQuery(search) {
|
||||
return {
|
||||
query:
|
||||
`{
|
||||
search(query: "${search}", type: ISSUE, first: 100) {
|
||||
edges {
|
||||
node {
|
||||
... on Issue {
|
||||
number
|
||||
repository {
|
||||
name
|
||||
url
|
||||
}
|
||||
title
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
name
|
||||
updatedAt
|
||||
url
|
||||
}
|
||||
}
|
||||
assignees(first: 100) {
|
||||
nodes {
|
||||
login
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
author {
|
||||
login
|
||||
url
|
||||
}
|
||||
body
|
||||
createdAt,
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
}
|
||||
}
|
||||
|
||||
groupIssues(issues) {
|
||||
const reportedNotReplied = [];
|
||||
const reportedAndReplied = [];
|
||||
const reportedNotRepliedNoAssignee = [];
|
||||
const reportedAndRepliedNoAssignee = [];
|
||||
|
||||
for (const issue of issues) {
|
||||
if (this.issueReportedNotReplied(issue)) {
|
||||
reportedNotReplied.push(issue);
|
||||
} else if (this.issueReportedAndReplied(issue)) {
|
||||
reportedAndReplied.push(issue);
|
||||
} else if (this.issueReportedNotRepliedNoAssignee(issue)) {
|
||||
reportedNotRepliedNoAssignee.push(issue);
|
||||
} else if (this.issueReportedAndRepliedNoAssignee(issue)) {
|
||||
reportedAndRepliedNoAssignee.push(issue);
|
||||
} else {
|
||||
this.context.error(`Unable to group issue:\n${ JSON.stringify(issue, null, 2) }`);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
reportedNotReplied,
|
||||
reportedAndReplied,
|
||||
reportedNotRepliedNoAssignee,
|
||||
reportedAndRepliedNoAssignee
|
||||
}
|
||||
}
|
||||
|
||||
issueReportedNotReplied(issue) {
|
||||
try {
|
||||
const labels = issue.node.labels.nodes;
|
||||
const labelValues = labels.map(label => label.name.toLowerCase());
|
||||
const assignees = issue.node.assignees.nodes;
|
||||
return labelValues.includes('customer-reported') && !labelValues.includes('customer-replied-to') && assignees.length > 0;
|
||||
} catch (err) {
|
||||
this.context.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
issueReportedAndReplied(issue) {
|
||||
try {
|
||||
const labels = issue.node.labels.nodes;
|
||||
const labelValues = labels.map(label => label.name.toLowerCase());
|
||||
const assignees = issue.node.assignees.nodes;
|
||||
return labelValues.includes('customer-reported') && labelValues.includes('customer-replied-to') && assignees.length > 0;
|
||||
} catch (err) {
|
||||
this.context.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
issueReportedNotRepliedNoAssignee(issue) {
|
||||
try {
|
||||
const labels = issue.node.labels.nodes;
|
||||
const labelValues = labels.map(label => label.name.toLowerCase());
|
||||
const assignees = issue.node.assignees.nodes;
|
||||
return labelValues.includes('customer-reported') && !labelValues.includes('customer-replied-to') && assignees.length === 0;
|
||||
} catch (err) {
|
||||
this.context.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
issueReportedAndRepliedNoAssignee(issue) {
|
||||
try {
|
||||
const labels = issue.node.labels.nodes;
|
||||
const labelValues = labels.map(label => label.name.toLowerCase());
|
||||
const assignees = issue.node.assignees.nodes;
|
||||
return labelValues.includes('customer-reported') && labelValues.includes('customer-replied-to') && assignees.length === 0;
|
||||
} catch (err) {
|
||||
this.context.error(err);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
const GitHubService = require('./githubService');
|
||||
|
||||
module.exports.GitHubService = GitHubService;
|
|
@ -1,50 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.Integration.ApplicationInsights.Core;
|
||||
using Microsoft.Bot.Builder.Integration.AspNet.Core;
|
||||
using Microsoft.Bot.Builder.TraceExtensions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace IssueNotificationBot
|
||||
{
|
||||
public class AdapterWithErrorHandler : BotFrameworkHttpAdapter
|
||||
{
|
||||
public AdapterWithErrorHandler(IConfiguration configuration, ILogger<BotFrameworkHttpAdapter> logger, TelemetryInitializerMiddleware telemetryInitializerMiddleware, ConversationState conversationState = null)
|
||||
: base(configuration, logger)
|
||||
{
|
||||
Use(telemetryInitializerMiddleware);
|
||||
|
||||
OnTurnError = async (turnContext, exception) =>
|
||||
{
|
||||
// Log any leaked exception from the application.
|
||||
logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");
|
||||
|
||||
// Send a message to the user
|
||||
await turnContext.SendActivityAsync("The bot encountered an error or bug.");
|
||||
await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code.");
|
||||
|
||||
if (conversationState != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Delete the conversationState for the current conversation to prevent the
|
||||
// bot from getting stuck in a error-loop caused by being in a bad state.
|
||||
// ConversationState should be thought of as similar to "cookie-state" in a Web pages.
|
||||
await conversationState.DeleteAsync(turnContext);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Send a trace activity, which will be displayed in the Bot Framework Emulator
|
||||
await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using IssueNotificationBot.Services;
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.Dialogs;
|
||||
using Microsoft.Bot.Builder.Teams;
|
||||
using Microsoft.Bot.Schema;
|
||||
using Microsoft.Bot.Schema.Teams;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot
|
||||
{
|
||||
public class IssueNotificationBot<T> : SignInBot<T> where T : Dialog
|
||||
{
|
||||
public IssueNotificationBot(ConversationState conversationState, UserState userState, T dialog, ILogger<SignInBot<T>> logger, UserStorage userStorage, NotificationHelper notificatioHelper)
|
||||
: base(conversationState, userState, dialog, logger, userStorage, notificatioHelper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override async Task OnTeamsMembersAddedAsync(IList<TeamsChannelAccount> membersAdded, TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInformation($"New members added: { string.Join(", ", membersAdded.Select(m => m.Name).ToArray())}");
|
||||
|
||||
if (turnContext.Activity.Conversation.ConversationType != Constants.PersonalConversationType)
|
||||
{
|
||||
foreach (var member in membersAdded)
|
||||
{
|
||||
// Greet each new user when user is added
|
||||
if (member.Id != turnContext.Activity.Recipient.Id && !await UserStorage.HaveUserDetails(member.Id))
|
||||
{
|
||||
await GreetNewTeamMember(member, turnContext, cancellationToken);
|
||||
}
|
||||
// If the bot is added, we need to get all members and message them to login, proactively, if we don't have their information already
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var teamMembers = await TeamsInfo.GetTeamMembersAsync(turnContext);
|
||||
foreach (var teamMember in teamMembers)
|
||||
{
|
||||
if (teamMember.Id != turnContext.Activity.Recipient.Id && !await UserStorage.HaveUserDetails(member.Id))
|
||||
{
|
||||
try
|
||||
{
|
||||
await GreetNewTeamMember(teamMember, turnContext, cancellationToken);
|
||||
}
|
||||
// Users that block the bot throw Forbidden errors. We'll catch all exceptions in case
|
||||
// unforseen errors occur; we want to message as many members as possible.
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(new EventId(1), e, $"Something went wrong when greeting { member.Name }");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
Logger.LogInformation($"Not in a Teams Team:\n{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnTeamsMembersRemovedAsync(IList<TeamsChannelAccount> teamsMembersRemoved, TeamInfo teamInfo, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInformation($"New members removed: { string.Join(", ", teamsMembersRemoved.Select(m => m.Name).ToArray())}");
|
||||
|
||||
if (turnContext.Activity.Conversation.ConversationType != Constants.PersonalConversationType)
|
||||
{
|
||||
foreach (var member in teamsMembersRemoved)
|
||||
{
|
||||
if (member.Id != turnContext.Activity.Recipient.Id)
|
||||
{
|
||||
await UserStorage.RemoveUser(member.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using IssueNotificationBot.Models;
|
||||
using IssueNotificationBot.Services;
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.Dialogs;
|
||||
using Microsoft.Bot.Builder.Teams;
|
||||
using Microsoft.Bot.Schema;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace IssueNotificationBot
|
||||
{
|
||||
public class SignInBot<T> : TeamsActivityHandler where T : Dialog
|
||||
{
|
||||
protected readonly BotState ConversationState;
|
||||
protected readonly Dialog Dialog;
|
||||
protected readonly ILogger Logger;
|
||||
protected readonly BotState UserState;
|
||||
protected readonly NotificationHelper NotificationHelper;
|
||||
protected readonly UserStorage UserStorage;
|
||||
protected TrackedUser Maintainer;
|
||||
|
||||
public SignInBot(ConversationState conversationState, UserState userState, T dialog, ILogger<SignInBot<T>> logger, UserStorage userStorage, NotificationHelper notificationHelper)
|
||||
{
|
||||
ConversationState = conversationState;
|
||||
UserState = userState;
|
||||
Dialog = dialog;
|
||||
Logger = logger;
|
||||
NotificationHelper = notificationHelper;
|
||||
UserStorage = userStorage;
|
||||
}
|
||||
|
||||
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.OnTurnAsync(turnContext, cancellationToken);
|
||||
|
||||
// Save any state changes that might have occurred during the turn.
|
||||
await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
|
||||
await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var handled = false;
|
||||
|
||||
turnContext.Activity.RemoveMentionText(turnContext.Activity.Recipient.Id);
|
||||
|
||||
//If the user sends the Login Command and we don't have their info, send them through the auth dialog
|
||||
if (string.Equals(turnContext.Activity.Text, Constants.LoginCommand, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
!await UserStorage.HaveUserDetails(turnContext.Activity.From.Id))
|
||||
{
|
||||
// We don't want to send the OAuth card to a group conversation
|
||||
if (turnContext.Activity.Conversation.ConversationType == Constants.PersonalConversationType)
|
||||
{
|
||||
Logger.LogInformation($"Running SignInDialog for {turnContext.Activity.From.Name}");
|
||||
|
||||
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation($"{turnContext.Activity.From.Name} sent the bot a message from a Group channel and we don't have their information.");
|
||||
|
||||
await turnContext.SendActivityAsync(Constants.NoOAuthInGroupConversationsResponse);
|
||||
}
|
||||
|
||||
handled = true;
|
||||
}
|
||||
|
||||
// Handle maintainer commands
|
||||
if (Maintainer == null)
|
||||
{
|
||||
Maintainer = await UserStorage.GetTrackedUserFromGitHubUserId(Constants.MaintainerGitHubId);
|
||||
}
|
||||
|
||||
// Check if Maintainer name matches incoming. ID seems to change between bots.
|
||||
if (!handled && turnContext.Activity.From.Name == Maintainer?.TeamsUserInfo.Name)
|
||||
{
|
||||
if (string.Equals(turnContext.Activity.Text, Constants.EnableMaintainerNotificationsCommand, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
Logger.LogInformation("Enabling Maintainer Notifications");
|
||||
NotificationHelper.NotifyMaintainer = true;
|
||||
await turnContext.SendActivityAsync("Maintainer Notifications Enabled");
|
||||
}
|
||||
else if (string.Equals(turnContext.Activity.Text, Constants.DisableMaintainerNotificationsCommand, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
Logger.LogInformation("Disabling Maintainer Notifications");
|
||||
NotificationHelper.NotifyMaintainer = false;
|
||||
await turnContext.SendActivityAsync("Maintainer Notifications Disabled");
|
||||
}
|
||||
else if (string.Equals(turnContext.Activity.Text, Constants.MaintainerTestCards, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
await SendMaintainerTestCards(turnContext);
|
||||
}
|
||||
else if (string.Equals(turnContext.Activity.Text, Constants.MaintainerResendGreetings, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
await ResendGreetings(turnContext, cancellationToken);
|
||||
}
|
||||
|
||||
handled = true;
|
||||
}
|
||||
|
||||
// Check for and catch Adaptive Card Submit actions
|
||||
if (!handled)
|
||||
{
|
||||
handled = await HandleAdaptiveCardSubmitActions(turnContext, cancellationToken);
|
||||
}
|
||||
|
||||
// Bot doesn't know how to handle anything else
|
||||
if (!handled)
|
||||
{
|
||||
Logger.LogInformation($"Unable to handle message: {turnContext.Activity.Text} from: {turnContext.Activity.From.Name}");
|
||||
await turnContext.SendActivityAsync(Constants.NoConversationResponse);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnTeamsSigninVerifyStateAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInformation("Running dialog with signin/verifystate from an Invoke Activity.");
|
||||
|
||||
// The OAuth Prompt needs to see the Invoke Activity in order to complete the login process.
|
||||
|
||||
// Run the Dialog with the new Invoke Activity.
|
||||
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<bool> HandleAdaptiveCardSubmitActions(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var handled = false;
|
||||
|
||||
// It's highly likely this is an Adaptive Card Submit action if Activity.Value is populated and Activity.Text is not
|
||||
if (turnContext.Activity.Value != null && string.IsNullOrEmpty(turnContext.Activity.Text))
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = ((JObject)turnContext.Activity.Value).ToObject<AdaptiveCardIssueSubmitAction>();
|
||||
Logger.LogInformation($"Received AdaptiveCard data: {data.action}");
|
||||
switch (data.action)
|
||||
{
|
||||
case Constants.HideIssueNotificationAction:
|
||||
await turnContext.DeleteActivityAsync(turnContext.Activity.ReplyToId, cancellationToken);
|
||||
break;
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Logger.LogError($"Unable to cast to AdaptiveCardSubmitAction.\n{e.Message}");
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
private async Task SendMaintainerTestCards(ITurnContext<IMessageActivity> turnContext)
|
||||
{
|
||||
var welcomeCard = TemplateCardHelper.GetUserWelcomeCard(
|
||||
Maintainer.GitHubDetails.Avatar_url,
|
||||
Maintainer.GitHubDetails.Login,
|
||||
Maintainer.GitHubDetails.Name,
|
||||
Maintainer);
|
||||
|
||||
var fakeIssue = new
|
||||
{
|
||||
Node = new
|
||||
{
|
||||
Title = "Test Title",
|
||||
CreatedAt = new DateTime(),
|
||||
Body = "Test Body",
|
||||
Url = "www.contoso.com",
|
||||
Number = 999
|
||||
}
|
||||
};
|
||||
|
||||
var issueCard = TemplateCardHelper.GetPersonalIssueCard(
|
||||
JsonConvert.DeserializeObject<GitHubIssueNode>(JsonConvert.SerializeObject(fakeIssue)),
|
||||
"TEST EXIRE MESSAGE",
|
||||
new DateTime(),
|
||||
"TEST ACTION",
|
||||
Maintainer);
|
||||
|
||||
await turnContext.SendActivitiesAsync(new IActivity[] { MessageFactory.Attachment(welcomeCard), MessageFactory.Attachment(issueCard) });
|
||||
}
|
||||
|
||||
private async Task ResendGreetings(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInformation("Resending Greetings");
|
||||
|
||||
var members = await TeamsInfo.GetTeamMembersAsync(turnContext);
|
||||
foreach (var member in members)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await UserStorage.HaveUserDetails(member.Id))
|
||||
{
|
||||
await GreetNewTeamMember(member, turnContext, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation($"Already have login info for: { member.Name }");
|
||||
}
|
||||
}
|
||||
// Users that block the bot throw Forbidden errors. We'll catch all exceptions in case
|
||||
// unforseen errors occur; we want to message as many members as possible.
|
||||
catch(Exception e)
|
||||
{
|
||||
Logger.LogError(new EventId(1), e, $"Something went wrong when greeting { member.Name }");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task GreetNewTeamMember(ChannelAccount member, ITurnContext turnContext, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInformation($"Greeting new member: { member.Name }");
|
||||
|
||||
var conversationReference = turnContext.Activity.GetConversationReference();
|
||||
conversationReference.User = member;
|
||||
|
||||
// TODO: Eventually, it would be nice to begin the SignInDialog here, proactively. However, I keep getting NotFound errors
|
||||
// When SignInDialog calls GetToken
|
||||
await NotificationHelper.CreatePersonalConversationAsync(conversationReference, async (turnContext2, cancellationToken2) =>
|
||||
{
|
||||
var activity = MessageFactory.Text($"Hello! I am {Constants.UserAgent} and I can notify you about your GitHub issues in the Bot Framework repositories that are about to \"expire\".\n" +
|
||||
"An \"expired\" issue is one with the `customer-reported` tag, and is nearing or past:\n" +
|
||||
"* 72 hours with no `customer-replied` tag\n" +
|
||||
"* 30 days and still open\n" +
|
||||
"* 90 days and still open\n\n" +
|
||||
"To get started, type \"login\" so that I can get your GitHub information.");
|
||||
activity.TeamsNotifyUser();
|
||||
await turnContext2.SendActivityAsync(activity, cancellationToken2);
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
namespace IssueNotificationBot
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public const string MaintainerGitHubId = "mdrichardson";
|
||||
|
||||
public const string GitHubUserStorageKey = "GitHubUsers";
|
||||
public const string TeamsIdToGitHubUserMapStorageKey = "TeamsIdToGitHubUser";
|
||||
|
||||
public const string GitHubApiBaseUrl = "https://api.github.com";
|
||||
public const string GitHubApiAuthenticatedUserPath = "/user";
|
||||
public const string UserAgent = "IssueNotificationBot";
|
||||
|
||||
public const int IssueNeedsResolutionHours = 24 * 30;
|
||||
public const bool ExcludeWeekendsFromExpireHours = true;
|
||||
|
||||
public const string PassedExpirationMessage = "has passed";
|
||||
public const string NearingExpirationMessage = "is nearing";
|
||||
|
||||
public const string NoActivityId = "NoActivityId";
|
||||
public const string PersonalConversationType = "personal";
|
||||
|
||||
public const string HideIssueNotificationAction = "hideIssueNotification";
|
||||
|
||||
public const string NoConversationResponse = "Sorry. I'm mostly a notification-only bot don't know how to respond to this.";
|
||||
public const string NoOAuthInGroupConversationsResponse = "Please sign in via the 1:1 conversation that I sent you previously. If you need me to send it again, please type \"login\" **in our 1:1 conversation**";
|
||||
|
||||
public const string LoginCommand = "login";
|
||||
public const string MaintainerCommandPrefix = "command:";
|
||||
public const string EnableMaintainerNotificationsCommand = MaintainerCommandPrefix + "enableNotifications";
|
||||
public const string DisableMaintainerNotificationsCommand = MaintainerCommandPrefix + "disableNotifications";
|
||||
public const string MaintainerTestCards = MaintainerCommandPrefix + "sendCards";
|
||||
public const string MaintainerResendGreetings = MaintainerCommandPrefix + "resendGreetings";
|
||||
|
||||
public const string TestRepo = "testRepoForIssueNotificationBot";
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.Integration.AspNet.Core;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot
|
||||
{
|
||||
[Route("api/messages")]
|
||||
[ApiController]
|
||||
public class BotController : ControllerBase
|
||||
{
|
||||
private readonly IBotFrameworkHttpAdapter _adapter;
|
||||
private readonly IBot _bot;
|
||||
|
||||
public BotController(IBotFrameworkHttpAdapter adapter, IBot bot)
|
||||
{
|
||||
_adapter = adapter;
|
||||
_bot = bot;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task PostAsync()
|
||||
{
|
||||
await _adapter.ProcessAsync(Request, Response, _bot);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using IssueNotificationBot.Models;
|
||||
using IssueNotificationBot.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Bot.Connector;
|
||||
using Microsoft.Bot.Connector.Authentication;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Rest.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot
|
||||
{
|
||||
// This ASP Controller handles data sent from the Azure Function that polls the GitHub issues.
|
||||
// It requires that the Azure Function uses the bot AppId and AppPassword
|
||||
[Route("api/data")]
|
||||
[ApiController]
|
||||
public class DataController : ControllerBase
|
||||
{
|
||||
private readonly IConfiguration Configuration;
|
||||
private readonly GitHubDataProcessor GitHubDataProcessor;
|
||||
private readonly ILogger Logger;
|
||||
|
||||
public DataController(IConfiguration configuration, GitHubDataProcessor gitHubDataProcessor, ILogger<DataController> logger)
|
||||
{
|
||||
Configuration = configuration;
|
||||
GitHubDataProcessor = gitHubDataProcessor;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<HttpStatusCode> PostAsync()
|
||||
{
|
||||
Logger.LogInformation("Received post on /api/data");
|
||||
using var reader = new System.IO.StreamReader(Request.Body);
|
||||
var json = await reader.ReadToEndAsync().ConfigureAwait(true);
|
||||
|
||||
try
|
||||
{
|
||||
var gitHubData = SafeJsonConvert.DeserializeObject<GitHubServiceData>(json);
|
||||
|
||||
var credentials = new SimpleCredentialProvider(Configuration["MicrosoftAppId"], Configuration["MicrosoftAppPassword"]);
|
||||
Request.Headers.TryGetValue("Authorization", out StringValues authHeader);
|
||||
var result = await JwtTokenValidation.ValidateAuthHeader(authHeader, credentials, new SimpleChannelProvider(), Channels.Directline);
|
||||
|
||||
if (result.IsAuthenticated)
|
||||
{
|
||||
await GitHubDataProcessor.ProcessData(gitHubData);
|
||||
Logger.LogInformation("Finished processing post on /api/data");
|
||||
return HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
return HttpStatusCode.Forbidden;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
Logger.LogError("Received invalid data on /api/data");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Something went wrong in /api/data controller: {e.Message}");
|
||||
}
|
||||
return HttpStatusCode.BadRequest;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"groupLocation": {
|
||||
"value": ""
|
||||
},
|
||||
"groupName": {
|
||||
"value": ""
|
||||
},
|
||||
"appId": {
|
||||
"value": ""
|
||||
},
|
||||
"appSecret": {
|
||||
"value": ""
|
||||
},
|
||||
"botId": {
|
||||
"value": ""
|
||||
},
|
||||
"botSku": {
|
||||
"value": ""
|
||||
},
|
||||
"newAppServicePlanName": {
|
||||
"value": ""
|
||||
},
|
||||
"newAppServicePlanSku": {
|
||||
"value": {
|
||||
"name": "S1",
|
||||
"tier": "Standard",
|
||||
"size": "S1",
|
||||
"family": "S",
|
||||
"capacity": 1
|
||||
}
|
||||
},
|
||||
"newAppServicePlanLocation": {
|
||||
"value": ""
|
||||
},
|
||||
"newWebAppName": {
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"appId": {
|
||||
"value": ""
|
||||
},
|
||||
"appSecret": {
|
||||
"value": ""
|
||||
},
|
||||
"botId": {
|
||||
"value": ""
|
||||
},
|
||||
"botSku": {
|
||||
"value": ""
|
||||
},
|
||||
"newAppServicePlanName": {
|
||||
"value": ""
|
||||
},
|
||||
"newAppServicePlanSku": {
|
||||
"value": {
|
||||
"name": "S1",
|
||||
"tier": "Standard",
|
||||
"size": "S1",
|
||||
"family": "S",
|
||||
"capacity": 1
|
||||
}
|
||||
},
|
||||
"appServicePlanLocation": {
|
||||
"value": ""
|
||||
},
|
||||
"existingAppServicePlan": {
|
||||
"value": ""
|
||||
},
|
||||
"newWebAppName": {
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"groupLocation": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Specifies the location of the Resource Group."
|
||||
}
|
||||
},
|
||||
"groupName": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Specifies the name of the Resource Group."
|
||||
}
|
||||
},
|
||||
"appId": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings."
|
||||
}
|
||||
},
|
||||
"appSecret": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings."
|
||||
}
|
||||
},
|
||||
"botId": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable."
|
||||
}
|
||||
},
|
||||
"botSku": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1."
|
||||
}
|
||||
},
|
||||
"newAppServicePlanName": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "The name of the App Service Plan."
|
||||
}
|
||||
},
|
||||
"newAppServicePlanSku": {
|
||||
"type": "object",
|
||||
"defaultValue": {
|
||||
"name": "S1",
|
||||
"tier": "Standard",
|
||||
"size": "S1",
|
||||
"family": "S",
|
||||
"capacity": 1
|
||||
},
|
||||
"metadata": {
|
||||
"description": "The SKU of the App Service Plan. Defaults to Standard values."
|
||||
}
|
||||
},
|
||||
"newAppServicePlanLocation": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "The location of the App Service Plan. Defaults to \"westus\"."
|
||||
}
|
||||
},
|
||||
"newWebAppName": {
|
||||
"type": "string",
|
||||
"defaultValue": "",
|
||||
"metadata": {
|
||||
"description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"."
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"appServicePlanName": "[parameters('newAppServicePlanName')]",
|
||||
"resourcesLocation": "[parameters('newAppServicePlanLocation')]",
|
||||
"webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]",
|
||||
"siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]",
|
||||
"botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"name": "[parameters('groupName')]",
|
||||
"type": "Microsoft.Resources/resourceGroups",
|
||||
"apiVersion": "2018-05-01",
|
||||
"location": "[parameters('groupLocation')]",
|
||||
"properties": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Resources/deployments",
|
||||
"apiVersion": "2018-05-01",
|
||||
"name": "storageDeployment",
|
||||
"resourceGroup": "[parameters('groupName')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"mode": "Incremental",
|
||||
"template": {
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {},
|
||||
"variables": {},
|
||||
"resources": [
|
||||
{
|
||||
"comments": "Create a new App Service Plan",
|
||||
"type": "Microsoft.Web/serverfarms",
|
||||
"name": "[variables('appServicePlanName')]",
|
||||
"apiVersion": "2018-02-01",
|
||||
"location": "[variables('resourcesLocation')]",
|
||||
"sku": "[parameters('newAppServicePlanSku')]",
|
||||
"properties": {
|
||||
"name": "[variables('appServicePlanName')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comments": "Create a Web App using the new App Service Plan",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"apiVersion": "2015-08-01",
|
||||
"location": "[variables('resourcesLocation')]",
|
||||
"kind": "app",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]"
|
||||
],
|
||||
"name": "[variables('webAppName')]",
|
||||
"properties": {
|
||||
"name": "[variables('webAppName')]",
|
||||
"serverFarmId": "[variables('appServicePlanName')]",
|
||||
"siteConfig": {
|
||||
"appSettings": [
|
||||
{
|
||||
"name": "WEBSITE_NODE_DEFAULT_VERSION",
|
||||
"value": "10.14.1"
|
||||
},
|
||||
{
|
||||
"name": "MicrosoftAppId",
|
||||
"value": "[parameters('appId')]"
|
||||
},
|
||||
{
|
||||
"name": "MicrosoftAppPassword",
|
||||
"value": "[parameters('appSecret')]"
|
||||
}
|
||||
],
|
||||
"cors": {
|
||||
"allowedOrigins": [
|
||||
"https://botservice.hosting.portal.azure.net",
|
||||
"https://hosting.onecloud.azure-test.net/"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2017-12-01",
|
||||
"type": "Microsoft.BotService/botServices",
|
||||
"name": "[parameters('botId')]",
|
||||
"location": "global",
|
||||
"kind": "bot",
|
||||
"sku": {
|
||||
"name": "[parameters('botSku')]"
|
||||
},
|
||||
"properties": {
|
||||
"name": "[parameters('botId')]",
|
||||
"displayName": "[parameters('botId')]",
|
||||
"endpoint": "[variables('botEndpoint')]",
|
||||
"msaAppId": "[parameters('appId')]",
|
||||
"developerAppInsightsApplicationId": null,
|
||||
"developerAppInsightKey": null,
|
||||
"publishingCredentials": null,
|
||||
"storageResourceId": null
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites/', variables('webAppName'))]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"appId": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings."
|
||||
}
|
||||
},
|
||||
"appSecret": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"."
|
||||
}
|
||||
},
|
||||
"botId": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable."
|
||||
}
|
||||
},
|
||||
"botSku": {
|
||||
"defaultValue": "F0",
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1."
|
||||
}
|
||||
},
|
||||
"newAppServicePlanName": {
|
||||
"type": "string",
|
||||
"defaultValue": "",
|
||||
"metadata": {
|
||||
"description": "The name of the new App Service Plan."
|
||||
}
|
||||
},
|
||||
"newAppServicePlanSku": {
|
||||
"type": "object",
|
||||
"defaultValue": {
|
||||
"name": "S1",
|
||||
"tier": "Standard",
|
||||
"size": "S1",
|
||||
"family": "S",
|
||||
"capacity": 1
|
||||
},
|
||||
"metadata": {
|
||||
"description": "The SKU of the App Service Plan. Defaults to Standard values."
|
||||
}
|
||||
},
|
||||
"appServicePlanLocation": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "The location of the App Service Plan."
|
||||
}
|
||||
},
|
||||
"existingAppServicePlan": {
|
||||
"type": "string",
|
||||
"defaultValue": "",
|
||||
"metadata": {
|
||||
"description": "Name of the existing App Service Plan used to create the Web App for the bot."
|
||||
}
|
||||
},
|
||||
"newWebAppName": {
|
||||
"type": "string",
|
||||
"defaultValue": "",
|
||||
"metadata": {
|
||||
"description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"."
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]",
|
||||
"useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]",
|
||||
"servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]",
|
||||
"resourcesLocation": "[parameters('appServicePlanLocation')]",
|
||||
"webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]",
|
||||
"siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]",
|
||||
"botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.",
|
||||
"type": "Microsoft.Web/serverfarms",
|
||||
"condition": "[not(variables('useExistingAppServicePlan'))]",
|
||||
"name": "[variables('servicePlanName')]",
|
||||
"apiVersion": "2018-02-01",
|
||||
"location": "[variables('resourcesLocation')]",
|
||||
"sku": "[parameters('newAppServicePlanSku')]",
|
||||
"properties": {
|
||||
"name": "[variables('servicePlanName')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comments": "Create a Web App using an App Service Plan",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"apiVersion": "2015-08-01",
|
||||
"location": "[variables('resourcesLocation')]",
|
||||
"kind": "app",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]"
|
||||
],
|
||||
"name": "[variables('webAppName')]",
|
||||
"properties": {
|
||||
"name": "[variables('webAppName')]",
|
||||
"serverFarmId": "[variables('servicePlanName')]",
|
||||
"siteConfig": {
|
||||
"appSettings": [
|
||||
{
|
||||
"name": "WEBSITE_NODE_DEFAULT_VERSION",
|
||||
"value": "10.14.1"
|
||||
},
|
||||
{
|
||||
"name": "MicrosoftAppId",
|
||||
"value": "[parameters('appId')]"
|
||||
},
|
||||
{
|
||||
"name": "MicrosoftAppPassword",
|
||||
"value": "[parameters('appSecret')]"
|
||||
}
|
||||
],
|
||||
"cors": {
|
||||
"allowedOrigins": [
|
||||
"https://botservice.hosting.portal.azure.net",
|
||||
"https://hosting.onecloud.azure-test.net/"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "2017-12-01",
|
||||
"type": "Microsoft.BotService/botServices",
|
||||
"name": "[parameters('botId')]",
|
||||
"location": "global",
|
||||
"kind": "bot",
|
||||
"sku": {
|
||||
"name": "[parameters('botSku')]"
|
||||
},
|
||||
"properties": {
|
||||
"name": "[parameters('botId')]",
|
||||
"displayName": "[parameters('botId')]",
|
||||
"endpoint": "[variables('botEndpoint')]",
|
||||
"msaAppId": "[parameters('appId')]",
|
||||
"developerAppInsightsApplicationId": null,
|
||||
"developerAppInsightKey": null,
|
||||
"publishingCredentials": null,
|
||||
"storageResourceId": null
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites/', variables('webAppName'))]"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.Dialogs;
|
||||
using Microsoft.Bot.Schema;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot
|
||||
{
|
||||
public class LogoutDialog : ComponentDialog
|
||||
{
|
||||
private readonly ILogger Logger;
|
||||
public LogoutDialog(string id, string connectionName, ILogger<LogoutDialog> logger)
|
||||
: base(id)
|
||||
{
|
||||
ConnectionName = connectionName;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
protected string ConnectionName { get; }
|
||||
|
||||
protected override async Task<DialogTurnResult> OnBeginDialogAsync(DialogContext innerDc, object options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await InterruptAsync(innerDc, cancellationToken);
|
||||
return result ?? await base.OnBeginDialogAsync(innerDc, options, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await InterruptAsync(innerDc, cancellationToken);
|
||||
return result ?? await base.OnContinueDialogAsync(innerDc, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (innerDc.Context.Activity.Type == ActivityTypes.Message)
|
||||
{
|
||||
var text = innerDc.Context.Activity.Text?.ToLowerInvariant();
|
||||
|
||||
if (text == "logout")
|
||||
{
|
||||
// The bot adapter encapsulates the authentication processes.
|
||||
var botAdapter = (BotFrameworkAdapter)innerDc.Context.Adapter;
|
||||
await botAdapter.SignOutUserAsync(innerDc.Context, ConnectionName, null, cancellationToken);
|
||||
await innerDc.Context.SendActivityAsync(MessageFactory.Text("You have been signed out."), cancellationToken);
|
||||
Logger.LogInformation($"{innerDc.Context.Activity.From.Name} has logged out");
|
||||
return await innerDc.CancelAllDialogsAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using IssueNotificationBot.Models;
|
||||
using IssueNotificationBot.Services;
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.Dialogs;
|
||||
using Microsoft.Bot.Builder.Teams;
|
||||
using Microsoft.Bot.Schema;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot
|
||||
{
|
||||
public class SignInDialog : LogoutDialog
|
||||
{
|
||||
protected readonly ILogger Logger;
|
||||
protected readonly UserStorage UserStorage;
|
||||
|
||||
public SignInDialog(IConfiguration configuration, ILogger<SignInDialog> logger, UserStorage userStorage)
|
||||
: base(nameof(SignInDialog), configuration["ConnectionName"], logger)
|
||||
{
|
||||
Logger = logger;
|
||||
UserStorage = userStorage;
|
||||
|
||||
AddDialog(new OAuthPrompt(
|
||||
nameof(OAuthPrompt),
|
||||
new OAuthPromptSettings
|
||||
{
|
||||
ConnectionName = ConnectionName,
|
||||
Text = "Please Sign In Through GitHub. This allows me to customize your experience to your GitHub profile.",
|
||||
Title = "Sign In",
|
||||
Timeout = 300000, // User has 5 minutes to login (1000 * 60 * 5)
|
||||
}));
|
||||
|
||||
AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
|
||||
|
||||
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
|
||||
{
|
||||
PromptStepAsync,
|
||||
LoginStepAsync,
|
||||
}));
|
||||
|
||||
// The initial child Dialog to run.
|
||||
InitialDialogId = nameof(WaterfallDialog);
|
||||
}
|
||||
|
||||
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
|
||||
{
|
||||
return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the token from the previous step. Note that we could also have gotten the
|
||||
// token directly from the prompt itself. There is an example of this in the next method.
|
||||
var tokenResponse = (TokenResponse)stepContext.Result;
|
||||
if (tokenResponse != null)
|
||||
{
|
||||
Logger.LogInformation($"{stepContext.Context.Activity.From} has logged in");
|
||||
|
||||
// Get the user's GitHub information with their token
|
||||
var client = new GitHubClient(tokenResponse.Token);
|
||||
var user = new TrackedUser(await client.GetAuthenticatedUser())
|
||||
{
|
||||
ConversationReference = stepContext.Context.Activity.GetConversationReference()
|
||||
};
|
||||
|
||||
var maintainer = await UserStorage.GetTrackedUserFromGitHubUserId(Constants.MaintainerGitHubId);
|
||||
var card = TemplateCardHelper.GetUserWelcomeCard(user.GitHubDetails.Avatar_url, user.GitHubDetails.Login, user.GitHubDetails.Name, maintainer);
|
||||
|
||||
await stepContext.Context.SendActivityAsync(MessageFactory.Attachment(card), cancellationToken);
|
||||
|
||||
// Get the user's Teams information
|
||||
try
|
||||
{
|
||||
user.TeamsUserInfo = await TeamsInfo.GetMemberAsync(stepContext.Context, stepContext.Context.Activity.From.Id);
|
||||
}
|
||||
catch (ErrorResponseException e)
|
||||
{
|
||||
Logger.LogError($"Unable to get TeamsUserInfo for {stepContext.Context.Activity.From.Name}: {e.Message}");
|
||||
}
|
||||
|
||||
// Add the user to persistent storage
|
||||
await UserStorage.AddGitHubUser(user);
|
||||
await UserStorage.AddTeamsUserToGitHubUserMap(new TeamsUserToGitHubMap(user.TeamsUserInfo.Id, user.GitHubDetails.Login));
|
||||
}
|
||||
else
|
||||
{
|
||||
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken);
|
||||
Logger.LogWarning($"Unsuccessful login for {stepContext.Context.Activity.From}");
|
||||
}
|
||||
|
||||
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>RCS1090</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<NoWarn>RCS1090</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove=".gitignore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AdaptiveCards" Version="1.2.4" />
|
||||
<PackageReference Include="AdaptiveCards.Templating" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.5" />
|
||||
<PackageReference Include="Microsoft.Bot.Builder.ApplicationInsights" Version="4.9.3" />
|
||||
<PackageReference Include="Microsoft.Bot.Builder.Azure" Version="4.9.3" />
|
||||
<PackageReference Include="Microsoft.Bot.Builder.Dialogs" Version="4.9.3" />
|
||||
<PackageReference Include="Microsoft.Bot.Builder.Integration.ApplicationInsights.Core" Version="4.9.3" />
|
||||
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.9.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -1,25 +0,0 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30204.135
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IssueNotificationBot", "IssueNotificationBot.csproj", "{A9A5A9F0-6F7E-48BB-8984-5F656BD290F2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A9A5A9F0-6F7E-48BB-8984-5F656BD290F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9A5A9F0-6F7E-48BB-8984-5F656BD290F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9A5A9F0-6F7E-48BB-8984-5F656BD290F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9A5A9F0-6F7E-48BB-8984-5F656BD290F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {853CC59A-5AC4-4980-A6AF-C5C269FD8595}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace IssueNotificationBot.Models
|
||||
{
|
||||
public class AdaptiveCardIssueSubmitAction
|
||||
{
|
||||
public string action;
|
||||
public int issueNumber;
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
namespace IssueNotificationBot.Models
|
||||
{
|
||||
public class GitHubServiceData
|
||||
{
|
||||
[JsonProperty(PropertyName = "reportedNotReplied")]
|
||||
public readonly GitHubIssueNode[] ReportedNotReplied;
|
||||
[JsonProperty(PropertyName = "reportedAndReplied")]
|
||||
public readonly GitHubIssueNode[] ReportedAndReplied;
|
||||
[JsonProperty(PropertyName = "reportedNotRepliedNoAssignee")]
|
||||
public readonly GitHubIssueNode[] ReportedNotRepliedNoAssignee;
|
||||
[JsonProperty(PropertyName = "reportedAndRepliedNoAssignee")]
|
||||
public readonly GitHubIssueNode[] ReportedAndRepliedNoAssignee;
|
||||
}
|
||||
|
||||
public class GitHubIssueNode
|
||||
{
|
||||
[JsonProperty(PropertyName = "node")]
|
||||
private readonly GitHubIssue Node;
|
||||
public int Number { get { return Node.Number; } }
|
||||
public GitHubRepository Repository { get { return Node.Repository; } }
|
||||
public string Title { get { return Node.Title; } }
|
||||
public GitHubLabel[] Labels { get { return Node.Labels.Nodes; } }
|
||||
public GitHubAssignee[] Assignees { get { return Node.Assignees.Nodes; } }
|
||||
public GitHubAuthor Author { get { return Node.Author; } }
|
||||
public string Body { get { return Node.Body; } }
|
||||
public DateTime CreatedAt { get { return Node.CreatedAt; } }
|
||||
public Uri Url { get { return Node.Url; } }
|
||||
}
|
||||
|
||||
public class GitHubIssue
|
||||
{
|
||||
[JsonProperty(PropertyName = "number")]
|
||||
public readonly int Number;
|
||||
[JsonProperty(PropertyName = "repository")]
|
||||
public readonly GitHubRepository Repository;
|
||||
[JsonProperty(PropertyName = "title")]
|
||||
public readonly string Title;
|
||||
[JsonProperty(PropertyName = "labels")]
|
||||
public readonly GitHubLabelsNodes Labels;
|
||||
[JsonProperty(PropertyName = "assignees")]
|
||||
public readonly GitHubAssigneesNodes Assignees;
|
||||
[JsonProperty(PropertyName = "author")]
|
||||
public readonly GitHubAuthor Author;
|
||||
[JsonProperty(PropertyName = "body")]
|
||||
public readonly string Body;
|
||||
[JsonProperty(PropertyName = "createdAt")]
|
||||
public readonly DateTime CreatedAt;
|
||||
[JsonProperty(PropertyName = "url")]
|
||||
public readonly Uri Url;
|
||||
}
|
||||
|
||||
public class GitHubRepository
|
||||
{
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public readonly string Name;
|
||||
[JsonProperty(PropertyName = "url")]
|
||||
public readonly Uri Url;
|
||||
}
|
||||
|
||||
public class GitHubLabelsNodes
|
||||
{
|
||||
[JsonProperty(PropertyName = "nodes")]
|
||||
public readonly GitHubLabel[] Nodes;
|
||||
}
|
||||
|
||||
public class GitHubLabel
|
||||
{
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public readonly string Name;
|
||||
[JsonProperty(PropertyName = "updatedAt")]
|
||||
public readonly DateTime UpdatedAt;
|
||||
[JsonProperty(PropertyName = "url")]
|
||||
public readonly Uri Url;
|
||||
}
|
||||
|
||||
public class GitHubAssigneesNodes
|
||||
{
|
||||
[JsonProperty(PropertyName = "nodes")]
|
||||
public readonly GitHubAssignee[] Nodes;
|
||||
}
|
||||
|
||||
public class GitHubAssignee
|
||||
{
|
||||
[JsonProperty(PropertyName = "login")]
|
||||
public readonly string Login;
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public readonly string Name;
|
||||
[JsonProperty(PropertyName = "url")]
|
||||
public readonly Uri Url;
|
||||
}
|
||||
|
||||
public class GitHubAuthor
|
||||
{
|
||||
[JsonProperty(PropertyName = "login")]
|
||||
public readonly string Login;
|
||||
[JsonProperty(PropertyName = "url")]
|
||||
public readonly Uri Url;
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
#nullable enable
|
||||
namespace IssueNotificationBot.Models
|
||||
{
|
||||
public class GitHubUserResponse
|
||||
{
|
||||
[JsonProperty(PropertyName = "login")]
|
||||
public string? Login;
|
||||
[JsonProperty(PropertyName = "id")]
|
||||
public int? Id;
|
||||
[JsonProperty(PropertyName = "node_id")]
|
||||
public string? Node_id;
|
||||
[JsonProperty(PropertyName = "avatar_url")]
|
||||
public string? Avatar_url;
|
||||
[JsonProperty(PropertyName = "garvatar_id")]
|
||||
public string? Garvatar_id;
|
||||
[JsonProperty(PropertyName = "url")]
|
||||
public Uri? Url;
|
||||
[JsonProperty(PropertyName = "html_url")]
|
||||
public Uri? Html_url;
|
||||
[JsonProperty(PropertyName = "followers_url")]
|
||||
public Uri? Followers_url;
|
||||
[JsonProperty(PropertyName = "following_url")]
|
||||
public Uri? Following_url;
|
||||
[JsonProperty(PropertyName = "gists_url")]
|
||||
public Uri? Gists_url;
|
||||
[JsonProperty(PropertyName = "starred_url")]
|
||||
public Uri? Starred_url;
|
||||
[JsonProperty(PropertyName = "subscriptions_url")]
|
||||
public Uri? Subscriptions_url;
|
||||
[JsonProperty(PropertyName = "organizations_url")]
|
||||
public Uri? Organizations_url;
|
||||
[JsonProperty(PropertyName = "repos_url")]
|
||||
public Uri? Repos_url;
|
||||
[JsonProperty(PropertyName = "events_url")]
|
||||
public Uri? Events_url;
|
||||
[JsonProperty(PropertyName = "received_events_url")]
|
||||
public Uri? Received_events_url;
|
||||
[JsonProperty(PropertyName = "type")]
|
||||
public string? Type;
|
||||
[JsonProperty(PropertyName = "site_admin")]
|
||||
public bool? Site_admin;
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string? Name;
|
||||
[JsonProperty(PropertyName = "company")]
|
||||
public string? Company;
|
||||
[JsonProperty(PropertyName = "blog")]
|
||||
public string? Blog;
|
||||
[JsonProperty(PropertyName = "location")]
|
||||
public string? Location;
|
||||
[JsonProperty(PropertyName = "email")]
|
||||
public string? Email;
|
||||
[JsonProperty(PropertyName = "hireable")]
|
||||
public bool? Hireable;
|
||||
[JsonProperty(PropertyName = "bio")]
|
||||
public string? Bio;
|
||||
[JsonProperty(PropertyName = "twitter_username")]
|
||||
public string? Twitter_username;
|
||||
[JsonProperty(PropertyName = "public_repos")]
|
||||
public int? Public_repos;
|
||||
[JsonProperty(PropertyName = "public_gists")]
|
||||
public int? Public_gists;
|
||||
[JsonProperty(PropertyName = "followers")]
|
||||
public int? Followers;
|
||||
[JsonProperty(PropertyName = "following")]
|
||||
public int? Following;
|
||||
[JsonProperty(PropertyName = "created_at")]
|
||||
public DateTime? Created_at;
|
||||
[JsonProperty(PropertyName = "updated_at")]
|
||||
public DateTime? Updated_at;
|
||||
[JsonProperty(PropertyName = "private_gists")]
|
||||
public int? Private_gists;
|
||||
[JsonProperty(PropertyName = "total_private_repos")]
|
||||
public int? Total_private_repos;
|
||||
[JsonProperty(PropertyName = "owned_private_repos")]
|
||||
public int? Owned_private_repos;
|
||||
[JsonProperty(PropertyName = "disk_usage")]
|
||||
public int? Disk_usage;
|
||||
[JsonProperty(PropertyName = "collaborators")]
|
||||
public int? Collaborators;
|
||||
[JsonProperty(PropertyName = "two_factor_authentication")]
|
||||
public bool? Two_factor_authentication;
|
||||
[JsonProperty(PropertyName = "plan")]
|
||||
public GitHubUserPlan? Plan;
|
||||
}
|
||||
|
||||
public class GitHubUserPlan
|
||||
{
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string? Name;
|
||||
[JsonProperty(PropertyName = "space")]
|
||||
public int? Space;
|
||||
[JsonProperty(PropertyName = "collaborators")]
|
||||
public int? Collaborators;
|
||||
[JsonProperty(PropertyName = "private_repos")]
|
||||
public int? Private_repos;
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace IssueNotificationBot.Models
|
||||
{
|
||||
public class TeamsUserToGitHubMap
|
||||
{
|
||||
public readonly string TeamsUserId;
|
||||
public readonly string GitHubUserLogin;
|
||||
|
||||
public TeamsUserToGitHubMap(string teamsUserId, string gitHubUserLogin)
|
||||
{
|
||||
TeamsUserId = teamsUserId;
|
||||
GitHubUserLogin = gitHubUserLogin;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot.Models
|
||||
{
|
||||
public class TrackedActivity
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.Bot.Schema;
|
||||
using Microsoft.Bot.Schema.Teams;
|
||||
|
||||
namespace IssueNotificationBot.Models
|
||||
{
|
||||
public class TrackedUser
|
||||
{
|
||||
public NotificationSettings NotificationSettings = new NotificationSettings();
|
||||
public TeamsChannelAccount TeamsUserInfo;
|
||||
public GitHubUserResponse GitHubDetails;
|
||||
public ConversationReference ConversationReference;
|
||||
|
||||
public TrackedUser(GitHubUserResponse gitHubUserResponse)
|
||||
{
|
||||
GitHubDetails = gitHubUserResponse;
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationSettings
|
||||
{
|
||||
public bool Enabled = true;
|
||||
public TimePeriodNotification[] TimePeriodNotifications =
|
||||
{
|
||||
new TimePeriodNotification(
|
||||
24 * 3,
|
||||
"72h",
|
||||
24,
|
||||
6
|
||||
),
|
||||
new TimePeriodNotification(
|
||||
24 * 30,
|
||||
"30d",
|
||||
24 * 3,
|
||||
24
|
||||
),
|
||||
new TimePeriodNotification(
|
||||
24 * 90,
|
||||
"90d",
|
||||
24 * 7,
|
||||
24 * 3
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
public class TimePeriodNotification
|
||||
{
|
||||
public string Name;
|
||||
public bool Enabled = true;
|
||||
public int ExpireHours;
|
||||
public int NotifyPriorToExpiryHours;
|
||||
public int NotificationFrequency;
|
||||
|
||||
public TimePeriodNotification(int _expireHours, string name, int notifyPriorToExpiryHours, int notificationFrequency, bool enabled = true)
|
||||
{
|
||||
ExpireHours = _expireHours;
|
||||
Name = name;
|
||||
NotifyPriorToExpiryHours = notifyPriorToExpiryHours;
|
||||
NotificationFrequency = notificationFrequency;
|
||||
Enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace IssueNotificationBot
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.ConfigureLogging((logging) =>
|
||||
{
|
||||
logging.AddDebug();
|
||||
logging.AddConsole();
|
||||
});
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
{
|
||||
"type": "AdaptiveCard",
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.2",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Issue ${nearingOrExpiredMessage} expiration!",
|
||||
"size": "extraLarge",
|
||||
"weight": "bolder",
|
||||
"color": "attention"
|
||||
},
|
||||
{
|
||||
"type": "FactSet",
|
||||
"facts": [
|
||||
{
|
||||
"title": "Title",
|
||||
"value": "${issueTitle}"
|
||||
},
|
||||
{
|
||||
"title": "Created",
|
||||
"value": "${issueCreated} UTC"
|
||||
},
|
||||
{
|
||||
"title": "Expires",
|
||||
"value": "${issueExpires} UTC"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Summary",
|
||||
"size": "Large",
|
||||
"weight": "Bolder",
|
||||
"color": "Accent"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${body}",
|
||||
"wrap": true,
|
||||
"maxLines": 5,
|
||||
"spacing": "Large",
|
||||
"separator": true,
|
||||
"size": "Small",
|
||||
"weight": "Lighter"
|
||||
},
|
||||
{
|
||||
"type": "ActionSet",
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.OpenUrl",
|
||||
"title": "Open Issue to ${action}",
|
||||
"url": "${issueUrl}",
|
||||
"iconUrl": "https://cdn0.iconfinder.com/data/icons/octicons/1024/mark-github-512.png"
|
||||
},
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Hide Notification",
|
||||
"data": {
|
||||
"action": "hideIssueNotification",
|
||||
"issueNumber": "${issueNumber}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "RichTextBlock",
|
||||
"inlines": [
|
||||
{
|
||||
"type": "TextRun",
|
||||
"text": "Contact ",
|
||||
"size": "Small"
|
||||
},
|
||||
{
|
||||
"type": "TextRun",
|
||||
"text": "${maintainerName} ",
|
||||
"color": "Accent"
|
||||
},
|
||||
{
|
||||
"type": "TextRun",
|
||||
"text": "(${maintainerEmail}) ",
|
||||
"color": "Attention",
|
||||
"size": "Small"
|
||||
},
|
||||
{
|
||||
"type": "TextRun",
|
||||
"text": "for help with this bot.",
|
||||
"size": "Small"
|
||||
}
|
||||
],
|
||||
"$when": "${maintainerName != null && maintainerEmail != null}"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
{
|
||||
"type": "AdaptiveCard",
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.2",
|
||||
"body": [
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"width": 20,
|
||||
"items": [
|
||||
{
|
||||
"type": "Image",
|
||||
"url": "${avatar_url}",
|
||||
"size": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"width": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "@${login}",
|
||||
"color": "Accent",
|
||||
"height": "stretch"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Hello, **${name}**!",
|
||||
"size": "Large"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "I'm going to monitor the Bot Framework issues on GitHub assigned to you and send you notifications when your issues are close to expiring.",
|
||||
"wrap": true,
|
||||
"spacing": "Medium"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Stay tuned!",
|
||||
"size": "Large",
|
||||
"weight": "Bolder",
|
||||
"color": "Warning",
|
||||
"spacing": "Small"
|
||||
},
|
||||
{
|
||||
"type": "RichTextBlock",
|
||||
"inlines": [
|
||||
{
|
||||
"type": "TextRun",
|
||||
"text": "Contact ",
|
||||
"size": "Small"
|
||||
},
|
||||
{
|
||||
"type": "TextRun",
|
||||
"text": "${maintainerName} ",
|
||||
"color": "Accent"
|
||||
},
|
||||
{
|
||||
"type": "TextRun",
|
||||
"text": "(${maintainerEmail}) ",
|
||||
"color": "Attention",
|
||||
"size": "Small"
|
||||
},
|
||||
{
|
||||
"type": "TextRun",
|
||||
"text": "for help with this bot.",
|
||||
"size": "Small"
|
||||
}
|
||||
],
|
||||
"$when": "${maintainerName != null && maintainerEmail != null}"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using IssueNotificationBot.Models;
|
||||
using Microsoft.Rest.Serialization;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot.Services
|
||||
{
|
||||
public class GitHubClient
|
||||
{
|
||||
private readonly string _token;
|
||||
private readonly HttpClient _client;
|
||||
public GitHubClient(string token)
|
||||
{
|
||||
_token = token;
|
||||
_client = new HttpClient();
|
||||
_client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"token {_token}");
|
||||
_client.DefaultRequestHeaders.UserAgent.TryParseAdd(Constants.UserAgent);
|
||||
}
|
||||
|
||||
public async Task<GitHubUserResponse> GetAuthenticatedUser()
|
||||
{
|
||||
return await GetJsonResponseAsObject<GitHubUserResponse>(Constants.GitHubApiAuthenticatedUserPath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<T> GetJsonResponseAsObject<T>(string path)
|
||||
{
|
||||
var request = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(Constants.GitHubApiBaseUrl + path)
|
||||
};
|
||||
var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
||||
return SafeJsonConvert.DeserializeObject<T>(response.Content.ReadAsStringAsync().Result);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using IssueNotificationBot.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot.Services
|
||||
{
|
||||
public class GitHubDataProcessor
|
||||
{
|
||||
private readonly UserStorage UserStorage;
|
||||
private Dictionary<string, TrackedUser> TrackedUsers;
|
||||
private readonly NotificationHelper NotificationHelper;
|
||||
private readonly IConfiguration Configuration;
|
||||
private readonly ILogger Logger;
|
||||
public GitHubDataProcessor(UserStorage userStorage, NotificationHelper notificationHelper, IConfiguration configuration, ILogger<GitHubDataProcessor> logger)
|
||||
{
|
||||
UserStorage = userStorage;
|
||||
NotificationHelper = notificationHelper;
|
||||
Configuration = configuration;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task ProcessData(GitHubServiceData data)
|
||||
{
|
||||
Logger.LogInformation("Processing data");
|
||||
TrackedUsers = await UserStorage.GetGitHubUsers();
|
||||
|
||||
await ProcessReportedNotReplied(data.ReportedNotReplied);
|
||||
}
|
||||
|
||||
private async Task ProcessReportedNotReplied(GitHubIssueNode[] issues)
|
||||
{
|
||||
Logger.LogInformation("Processing ProcessReportedNotReplied");
|
||||
foreach (var issue in issues)
|
||||
{
|
||||
foreach (var assignee in GetAssigneeUserData(issue))
|
||||
{
|
||||
await NotifyAssigneeAsNecessary(assignee, issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<TrackedUser> GetAssigneeUserData(GitHubIssueNode issue)
|
||||
{
|
||||
var users = new List<TrackedUser>();
|
||||
foreach (var assignee in issue.Assignees)
|
||||
{
|
||||
if (TrackedUsers.TryGetValue(assignee.Login, out TrackedUser user))
|
||||
{
|
||||
users.Add(user);
|
||||
}
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
private async Task NotifyAssigneeAsNecessary(TrackedUser user, GitHubIssueNode issue)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (user.NotificationSettings.Enabled)
|
||||
{
|
||||
// Check each time period from largest to smallest
|
||||
foreach (TimePeriodNotification timePeriod in user.NotificationSettings.TimePeriodNotifications.OrderByDescending(item => item.ExpireHours).ToList())
|
||||
{
|
||||
// Stop checking if we've already sent the notification
|
||||
if (NotificationHelper.IssueActivityMap.TryGetValue(issue.Number, out MappedIssue mappedActivity) && mappedActivity.Users.TryGetValue(user.TeamsUserInfo.Id, out MappedActivityUser mappedUser))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust the message we send to the user
|
||||
var action = "Respond";
|
||||
if (IssueExpiredNeedsResolve(timePeriod))
|
||||
{
|
||||
action = "Resolve";
|
||||
}
|
||||
|
||||
string nearingOrExpiredMessage = null;
|
||||
var expires = GetExpiration(issue, timePeriod, now);
|
||||
|
||||
// TESTING ONLY: This allows us to set up a separate repository and some issues to test that the bot works.
|
||||
// Here, we can manually set the issue created time to mock an expired issue.
|
||||
if (Configuration?["EnableTestMode"] == "true" && issue.Repository.Name == Constants.TestRepo)
|
||||
{
|
||||
expires = new DateTime(2020, 1, 1);
|
||||
}
|
||||
|
||||
if (IssueExpiredNeedsResponse(expires, now))
|
||||
{
|
||||
nearingOrExpiredMessage = Constants.PassedExpirationMessage;
|
||||
}
|
||||
else if (IssueNearingExpirationNeedsResponse(timePeriod, expires, now))
|
||||
{
|
||||
nearingOrExpiredMessage = Constants.NearingExpirationMessage;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(nearingOrExpiredMessage) && !UserNotifiedWithinWindow(timePeriod, now, issue, user.TeamsUserInfo.Id))
|
||||
{
|
||||
await NotificationHelper.SendIssueNotificationToUserAsync(user, issue, nearingOrExpiredMessage, expires, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DateTime GetExpiration(GitHubIssueNode issue, TimePeriodNotification timePeriod, DateTime now)
|
||||
{
|
||||
var adjustedExpiration = issue.CreatedAt.AddHours(timePeriod.ExpireHours);
|
||||
|
||||
// 30d and 90d periods always include weekends.
|
||||
if (timePeriod.ExpireHours != 72) return adjustedExpiration;
|
||||
|
||||
// Adjust the expiration time to not include weekends.
|
||||
if (Constants.ExcludeWeekendsFromExpireHours)
|
||||
{
|
||||
var weekendDays = 0;
|
||||
for (DateTime date = issue.CreatedAt; date.Date <= now.Date; date = date.AddDays(1))
|
||||
{
|
||||
if ((date.DayOfWeek == DayOfWeek.Saturday) || (date.DayOfWeek == DayOfWeek.Sunday))
|
||||
{
|
||||
weekendDays++;
|
||||
}
|
||||
}
|
||||
|
||||
adjustedExpiration.AddDays(weekendDays);
|
||||
}
|
||||
|
||||
return adjustedExpiration;
|
||||
}
|
||||
|
||||
private bool IssueExpiredNeedsResolve(TimePeriodNotification timePeriod)
|
||||
{
|
||||
return timePeriod.ExpireHours > Constants.IssueNeedsResolutionHours;
|
||||
}
|
||||
|
||||
private bool IssueExpiredNeedsResponse(DateTime expires, DateTime now)
|
||||
{
|
||||
return now >= expires;
|
||||
}
|
||||
|
||||
private bool IssueNearingExpirationNeedsResponse(TimePeriodNotification timePeriod, DateTime expires, DateTime now)
|
||||
{
|
||||
return now >= expires.AddHours(-timePeriod.NotifyPriorToExpiryHours);
|
||||
}
|
||||
|
||||
private bool UserNotifiedWithinWindow(TimePeriodNotification timePeriod, DateTime now, GitHubIssueNode issue, string teamsUserId)
|
||||
{
|
||||
var mappedActivity = NotificationHelper.GetMappedIssue(issue.Number);
|
||||
if (mappedActivity != null && mappedActivity.Users.TryGetValue(teamsUserId, out MappedActivityUser mappedUser))
|
||||
{
|
||||
return mappedUser.SentAt.AddHours(timePeriod.NotificationFrequency) <= now;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using IssueNotificationBot.Models;
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.Integration.AspNet.Core;
|
||||
using Microsoft.Bot.Builder.Teams;
|
||||
using Microsoft.Bot.Connector;
|
||||
using Microsoft.Bot.Connector.Authentication;
|
||||
using Microsoft.Bot.Schema;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot.Services
|
||||
{
|
||||
public class NotificationHelper
|
||||
{
|
||||
private readonly IBotFrameworkHttpAdapter Adapter;
|
||||
private readonly IConfiguration Configuration;
|
||||
public readonly ConcurrentDictionary<int, MappedIssue> IssueActivityMap = new ConcurrentDictionary<int, MappedIssue>();
|
||||
private readonly ILogger Logger;
|
||||
public bool NotifyMaintainer = true;
|
||||
private readonly UserStorage UserStorage;
|
||||
|
||||
public NotificationHelper(IBotFrameworkHttpAdapter adapter, IConfiguration configuration, ILogger<NotificationHelper> logger, UserStorage userStorage)
|
||||
{
|
||||
Adapter = adapter;
|
||||
Configuration = configuration;
|
||||
Logger = logger;
|
||||
UserStorage = userStorage;
|
||||
|
||||
// If the channel is the Emulator, and authentication is not in use,
|
||||
// the AppId will be null. We generate a random AppId for this case only.
|
||||
// This is not required for production, since the AppId will have a value.
|
||||
if (string.IsNullOrEmpty(Configuration["MicrosoftAppId"]))
|
||||
{
|
||||
Configuration["MicrosoftAppId"] = Guid.NewGuid().ToString(); //if no AppId, use a random Guid
|
||||
}
|
||||
|
||||
// Notify the maintainer of this bot of any errors via Teams.
|
||||
// We need to do this here and not in AdapterWithErrorHandler to avoid circular dependencies.
|
||||
var originalOnTurnError = (adapter as AdapterWithErrorHandler)?.OnTurnError;
|
||||
|
||||
(adapter as AdapterWithErrorHandler)!.OnTurnError = async (turnContext, exception) =>
|
||||
{
|
||||
if (NotifyMaintainer)
|
||||
{
|
||||
var maintainer = await userStorage.GetTrackedUserFromGitHubUserId(Constants.MaintainerGitHubId);
|
||||
if (maintainer != null)
|
||||
{
|
||||
var errorMessage = $"Error occurred for {turnContext?.Activity?.From?.Name}:\n{exception.Message}\n{exception.StackTrace}\n{turnContext?.Activity}";
|
||||
Logger.LogError(errorMessage);
|
||||
await SendProactiveNotificationToUserAsync(maintainer, MessageFactory.Text(errorMessage));
|
||||
}
|
||||
|
||||
await turnContext.SendActivityAsync("I've notified the maintainer of this bot about this error.");
|
||||
}
|
||||
|
||||
await originalOnTurnError(turnContext, exception);
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ResourceResponse> SendProactiveNotificationToUserAsync(TrackedUser user, IActivity activity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<ResourceResponse>();
|
||||
await CreatePersonalConversationAsync(user.ConversationReference, async (turnContext, cancellationToken2) =>
|
||||
{
|
||||
var activityId = await turnContext.SendActivityAsync(activity, cancellationToken2);
|
||||
tcs.SetResult(activityId);
|
||||
}, cancellationToken);
|
||||
|
||||
return tcs.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task SendIssueNotificationToUserAsync(TrackedUser user, GitHubIssueNode issue, string nearingOrExpiredMessage, DateTime expires, string action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Logger.LogInformation($"Sending notification to {user.TeamsUserInfo.Name} for {issue.Number}");
|
||||
|
||||
var maintainer = await UserStorage.GetTrackedUserFromGitHubUserId(Constants.MaintainerGitHubId);
|
||||
|
||||
var card = TemplateCardHelper.GetPersonalIssueCard(issue, nearingOrExpiredMessage, expires, action, maintainer);
|
||||
|
||||
var activity = MessageFactory.Attachment(card);
|
||||
activity.TeamsNotifyUser();
|
||||
|
||||
var activityId = await SendProactiveNotificationToUserAsync(user, activity, cancellationToken);
|
||||
StoreIssueCardActivityId(activityId.Id, issue.Number, user.TeamsUserInfo.Id);
|
||||
}
|
||||
|
||||
public async Task CreatePersonalConversationAsync(ConversationReference conversationReference, BotCallbackHandler callback, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInformation($"Creating personal conversation for {conversationReference.User.Name}");
|
||||
|
||||
var serviceUrl = conversationReference.ServiceUrl;
|
||||
var credentials = new MicrosoftAppCredentials(Configuration["MicrosoftAppId"], Configuration["MicrosoftAppPassword"]);
|
||||
|
||||
var conversationParameters = new ConversationParameters
|
||||
{
|
||||
IsGroup = false,
|
||||
Members = new ChannelAccount[]
|
||||
{
|
||||
conversationReference.User
|
||||
},
|
||||
TenantId = conversationReference.Conversation.TenantId,
|
||||
Bot = conversationReference.Bot
|
||||
};
|
||||
|
||||
AppCredentials.TrustServiceUrl(serviceUrl);
|
||||
|
||||
await ((BotFrameworkAdapter)Adapter).CreateConversationAsync(
|
||||
Channels.Msteams,
|
||||
serviceUrl,
|
||||
credentials,
|
||||
conversationParameters,
|
||||
async (turnContext1, cancellationToken1) =>
|
||||
{
|
||||
Logger.LogInformation($"Continuing conversation for {conversationReference.User.Name}");
|
||||
|
||||
var conversationReference2 = turnContext1.Activity.GetConversationReference();
|
||||
conversationReference2.User = conversationReference.User;
|
||||
await ((BotFrameworkAdapter)Adapter).ContinueConversationAsync(
|
||||
Configuration["MicrosoftAppId"],
|
||||
conversationReference2,
|
||||
async (turnContext2, cancellationToken2) => await callback(turnContext2, cancellationToken2),
|
||||
cancellationToken1);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private void StoreIssueCardActivityId(string activityId, int issueNumber, string teamsUserId)
|
||||
{
|
||||
Logger.LogInformation($"Storing IssueCard {issueNumber} for activityId {activityId}");
|
||||
var newMappedActivity = new MappedIssue(activityId, teamsUserId);
|
||||
IssueActivityMap.AddOrUpdate(issueNumber, newMappedActivity, (_, oldValue) =>
|
||||
{
|
||||
oldValue.Users[teamsUserId] = new MappedActivityUser(activityId);
|
||||
return oldValue;
|
||||
});
|
||||
}
|
||||
|
||||
public MappedIssue GetMappedIssue(int issueNumber)
|
||||
{
|
||||
if (IssueActivityMap.TryGetValue(issueNumber, out MappedIssue mappedIssue))
|
||||
{
|
||||
return mappedIssue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public MappedActivityUser GetMappedActivityFromIssueAndUser(int issueNumber, string teamsUserId)
|
||||
{
|
||||
var mappedIssue = GetMappedIssue(issueNumber);
|
||||
if (mappedIssue != null && mappedIssue.Users.TryGetValue(teamsUserId, out MappedActivityUser mappedUser))
|
||||
{
|
||||
return mappedUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void HideActivity(int issueNumber, string teamsUserId)
|
||||
{
|
||||
if (IssueActivityMap.TryGetValue(issueNumber, out MappedIssue mappedActivity))
|
||||
{
|
||||
Logger.LogInformation($"Hiding issue {issueNumber} for user {teamsUserId}");
|
||||
|
||||
if (mappedActivity.Users.ContainsKey(teamsUserId))
|
||||
{
|
||||
mappedActivity.Users[teamsUserId].Hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MappedIssue
|
||||
{
|
||||
public readonly ConcurrentDictionary<string, MappedActivityUser> Users = new ConcurrentDictionary<string, MappedActivityUser>();
|
||||
|
||||
public MappedIssue(string activityId, string teamsUserId)
|
||||
{
|
||||
Users[teamsUserId] = new MappedActivityUser(activityId);
|
||||
}
|
||||
}
|
||||
|
||||
public class MappedActivityUser
|
||||
{
|
||||
public bool Hidden;
|
||||
public readonly string ActivityId;
|
||||
public readonly DateTime SentAt = DateTime.Now;
|
||||
|
||||
public MappedActivityUser(string activityId)
|
||||
{
|
||||
ActivityId = activityId;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
using AdaptiveCards;
|
||||
using AdaptiveCards.Templating;
|
||||
using IssueNotificationBot.Models;
|
||||
using Microsoft.Bot.Schema;
|
||||
using Microsoft.Rest.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace IssueNotificationBot.Services
|
||||
{
|
||||
public static class TemplateCardHelper
|
||||
{
|
||||
public static Attachment GetUserWelcomeCard(string avatar_url, string login, string name, TrackedUser maintainer)
|
||||
{
|
||||
// User may not have their name set in their profile
|
||||
name ??= "GitHub User";
|
||||
|
||||
var paths = new[] { ".", "Resources", "userWelcomeCard.json" };
|
||||
var adaptiveCardJson = File.ReadAllText(Path.Combine(paths));
|
||||
|
||||
var templateCard = new AdaptiveCardTemplate(adaptiveCardJson);
|
||||
|
||||
var templateData = SafeJsonConvert.SerializeObject(new
|
||||
{
|
||||
avatar_url,
|
||||
login,
|
||||
name,
|
||||
maintainerName = maintainer?.TeamsUserInfo?.Name,
|
||||
maintainerEmail = maintainer?.TeamsUserInfo?.Email
|
||||
});
|
||||
var cardJson = templateCard.Expand(templateData);
|
||||
|
||||
return new Attachment()
|
||||
{
|
||||
ContentType = AdaptiveCard.ContentType,
|
||||
Content = JsonConvert.DeserializeObject<AdaptiveCard>(cardJson),
|
||||
};
|
||||
}
|
||||
|
||||
public static Attachment GetPersonalIssueCard(GitHubIssueNode issue, string nearingOrExpiredMessage, DateTime expires, string action, TrackedUser maintainer)
|
||||
{
|
||||
var paths = new[] { ".", "Resources", "personalIssueCard.json" };
|
||||
var adaptiveCardJson = File.ReadAllText(Path.Combine(paths));
|
||||
|
||||
var templateCard = new AdaptiveCardTemplate(adaptiveCardJson);
|
||||
|
||||
var templateData = SafeJsonConvert.SerializeObject(new
|
||||
{
|
||||
nearingOrExpiredMessage,
|
||||
issueTitle = issue.Title,
|
||||
issueCreated = issue.CreatedAt,
|
||||
issueExpires = expires,
|
||||
body = issue.Body,
|
||||
issueUrl = issue.Url,
|
||||
issueNumber = issue.Number,
|
||||
action,
|
||||
maintainerName = maintainer?.TeamsUserInfo?.Name,
|
||||
maintainerEmail = maintainer?.TeamsUserInfo?.Email
|
||||
});
|
||||
var cardJson = templateCard.Expand(templateData);
|
||||
|
||||
return new Attachment()
|
||||
{
|
||||
ContentType = AdaptiveCard.ContentType,
|
||||
Content = JsonConvert.DeserializeObject<AdaptiveCard>(cardJson),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
using IssueNotificationBot.Models;
|
||||
using Microsoft.Azure.Cosmos;
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IssueNotificationBot.Services
|
||||
{
|
||||
public class UserStorage
|
||||
{
|
||||
protected readonly IStorage Db;
|
||||
protected readonly ILogger Logger;
|
||||
|
||||
public UserStorage(IStorage db, ILogger<UserStorage> logger)
|
||||
{
|
||||
Db = db;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task AddGitHubUser(TrackedUser user)
|
||||
{
|
||||
Logger.LogInformation($"Storing GitHub User: {user.GitHubDetails.Name}");
|
||||
|
||||
var users = await GetGitHubUsers();
|
||||
users.TryAdd(user.GitHubDetails.Login, user);
|
||||
await Db.WriteAsync(new Dictionary<string, object>() { { Constants.GitHubUserStorageKey, users } });
|
||||
}
|
||||
|
||||
public async Task RemoveGitHubUser(string gitHubUserLogin)
|
||||
{
|
||||
Logger.LogInformation($"Removing GitHub User: {gitHubUserLogin}");
|
||||
|
||||
var users = await GetGitHubUsers();
|
||||
users.Remove(gitHubUserLogin);
|
||||
await Db.WriteAsync(new Dictionary<string, object>() { { Constants.GitHubUserStorageKey, users } });
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, TrackedUser>> GetGitHubUsers()
|
||||
{
|
||||
return await GetUsersDb<TrackedUser>(Constants.GitHubUserStorageKey);
|
||||
}
|
||||
|
||||
public async Task RemoveUser(string teamsUserId)
|
||||
{
|
||||
var userMap = await GetTeamsUserToGitHubMap(teamsUserId);
|
||||
|
||||
await RemoveFromTeamsUserToGitHubUserMap(userMap);
|
||||
await RemoveGitHubUser(userMap.GitHubUserLogin);
|
||||
}
|
||||
|
||||
public async Task AddTeamsUserToGitHubUserMap(TeamsUserToGitHubMap user)
|
||||
{
|
||||
Logger.LogInformation($"Adding Teams User: {user.TeamsUserId}/{user.GitHubUserLogin} to GitHubUsersMap");
|
||||
|
||||
var users = await GetTeamsUsers();
|
||||
users.TryAdd(user.TeamsUserId, user);
|
||||
await Db.WriteAsync(new Dictionary<string, object>() { { Constants.TeamsIdToGitHubUserMapStorageKey, users } });
|
||||
}
|
||||
|
||||
public async Task RemoveFromTeamsUserToGitHubUserMap(TeamsUserToGitHubMap user)
|
||||
{
|
||||
Logger.LogInformation($"Removing Teams User: {user.TeamsUserId}/{user.GitHubUserLogin} to GitHubUsersMap");
|
||||
|
||||
var users = await GetTeamsUsers();
|
||||
users.Remove(user.TeamsUserId);
|
||||
await Db.WriteAsync(new Dictionary<string, object>() { { Constants.TeamsIdToGitHubUserMapStorageKey, users } });
|
||||
}
|
||||
|
||||
public async Task<TeamsUserToGitHubMap> GetTeamsUserToGitHubMap(string teamsUserId)
|
||||
{
|
||||
var teamsUsers = await GetTeamsUsers();
|
||||
if (teamsUsers.TryGetValue(teamsUserId, out TeamsUserToGitHubMap user))
|
||||
{
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, TeamsUserToGitHubMap>> GetTeamsUsers()
|
||||
{
|
||||
return await GetUsersDb<TeamsUserToGitHubMap>(Constants.TeamsIdToGitHubUserMapStorageKey);
|
||||
}
|
||||
|
||||
public async Task<bool> HaveUserDetails(string teamsUserId)
|
||||
{
|
||||
var teamsUsers = await GetTeamsUsers();
|
||||
if (!teamsUsers.TryGetValue(teamsUserId, out TeamsUserToGitHubMap user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var gitHubUsers = await GetGitHubUsers();
|
||||
return gitHubUsers.ContainsKey(user.GitHubUserLogin);
|
||||
}
|
||||
|
||||
public async Task<TrackedUser> GetTrackedUserFromGitHubUserId(string gitHubUserId)
|
||||
{
|
||||
var users = await GetGitHubUsers();
|
||||
if (users.TryGetValue(gitHubUserId, out TrackedUser trackedUser))
|
||||
{
|
||||
return trackedUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<TrackedUser> GetTrackedUserFromTeamsUserId(string teamsUserId)
|
||||
{
|
||||
var gitHubUserId = (await GetTeamsUserToGitHubMap(teamsUserId))?.GitHubUserLogin;
|
||||
return await GetTrackedUserFromGitHubUserId(gitHubUserId);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, T>> GetUsersDb<T>(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var document = await Db.ReadAsync<Dictionary<string, T>>(new string[] { key });
|
||||
return document.TryGetValue(key, out Dictionary<string, T> users) ? users : new Dictionary<string, T>();
|
||||
}
|
||||
catch (CosmosException e) when (e.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
// NotFound *should* only indicate that the Container hasn't been created yet.
|
||||
Logger.LogWarning($"404 when reading Cosmos Container: {key} DB.");
|
||||
return new Dictionary<string, T>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using IssueNotificationBot.Services;
|
||||
using Microsoft.ApplicationInsights.Extensibility;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.ApplicationInsights;
|
||||
using Microsoft.Bot.Builder.Azure;
|
||||
using Microsoft.Bot.Builder.Integration.ApplicationInsights.Core;
|
||||
using Microsoft.Bot.Builder.Integration.AspNet.Core;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace IssueNotificationBot
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IWebHostEnvironment env)
|
||||
{
|
||||
var builder = new ConfigurationBuilder()
|
||||
.SetBasePath(env.ContentRootPath)
|
||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
|
||||
.AddEnvironmentVariables();
|
||||
Configuration = builder.Build();
|
||||
}
|
||||
|
||||
public IConfigurationRoot Configuration { get; }
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers().AddNewtonsoftJson();
|
||||
|
||||
// Add Application Insights services into service collection
|
||||
services.AddApplicationInsightsTelemetry();
|
||||
|
||||
// Create the telemetry client.
|
||||
services.AddSingleton<IBotTelemetryClient, BotTelemetryClient>();
|
||||
|
||||
// Add telemetry initializer that will set the correlation context for all telemetry items.
|
||||
services.AddSingleton<ITelemetryInitializer, OperationCorrelationTelemetryInitializer>();
|
||||
|
||||
// Add telemetry initializer that sets the user ID and session ID (in addition to other bot-specific properties such as activity ID)
|
||||
services.AddSingleton<ITelemetryInitializer, TelemetryBotIdInitializer>();
|
||||
|
||||
// Create the telemetry middleware to initialize telemetry gathering
|
||||
services.AddSingleton<TelemetryInitializerMiddleware>();
|
||||
|
||||
// Create the telemetry middleware (used by the telemetry initializer) to track conversation events
|
||||
services.AddSingleton<TelemetryLoggerMiddleware>();
|
||||
|
||||
// Create the Bot Framework Adapter with error handling enabled.
|
||||
services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
|
||||
|
||||
// Create the User state. (Used in this bot's Dialog implementation.)
|
||||
services.AddSingleton<UserState>();
|
||||
|
||||
// Create the Conversation state. (Used by the Dialog system itself.)
|
||||
services.AddSingleton<ConversationState>();
|
||||
|
||||
// The Dialog that will be run by the bot.
|
||||
services.AddSingleton<SignInDialog>();
|
||||
|
||||
// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
|
||||
services.AddTransient<IBot, IssueNotificationBot<SignInDialog>>();
|
||||
|
||||
// Cosmos Storage
|
||||
var storage = new CosmosDbPartitionedStorage(
|
||||
new CosmosDbPartitionedStorageOptions
|
||||
{
|
||||
CosmosDbEndpoint = Configuration.GetValue<string>("CosmosDbEndpoint"),
|
||||
AuthKey = Configuration.GetValue<string>("CosmosDbAuthKey"),
|
||||
DatabaseId = Configuration.GetValue<string>("CosmosDbDatabaseId"),
|
||||
ContainerId = Configuration.GetValue<string>("CosmosDbContainerId"),
|
||||
CompatibilityMode = false,
|
||||
});
|
||||
services.AddSingleton<IStorage>(storage);
|
||||
services.AddSingleton<UserStorage>();
|
||||
|
||||
services.AddSingleton<GitHubDataProcessor>();
|
||||
services.AddSingleton<NotificationHelper>();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseDefaultFiles()
|
||||
.UseStaticFiles()
|
||||
.UseRouting()
|
||||
.UseAuthorization()
|
||||
.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||
|
||||
// app.UseHttpsRedirection();
|
||||
}
|
||||
}
|
||||
}
|
Двоичные данные
dri/issueNotificationBot/Bot/TeamsAppManifest/color.png
Двоичные данные
dri/issueNotificationBot/Bot/TeamsAppManifest/color.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 15 KiB |
|
@ -1,67 +0,0 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.7/MicrosoftTeams.schema.json",
|
||||
"manifestVersion": "1.7",
|
||||
"version": "1.0.0",
|
||||
"id": "<AppId>",
|
||||
"packageName": "com.notification.bot",
|
||||
"localizationInfo": {
|
||||
"defaultLanguageTag": "en-us",
|
||||
"additionalLanguages": [
|
||||
]
|
||||
},
|
||||
"developer": {
|
||||
"name": "Michael Richardson",
|
||||
"websiteUrl": "https://dev.botframework.com/",
|
||||
"privacyUrl": "https://aka.ms/bf-privacy",
|
||||
"termsOfUseUrl": "https://aka.ms/bf-terms",
|
||||
"mpnId": "1234567890"
|
||||
},
|
||||
"name": {
|
||||
"short": "Notification Bot",
|
||||
"full": "GitHub Issue Notification Bot"
|
||||
},
|
||||
"description": {
|
||||
"short": "GitHub Issue Notification Bot",
|
||||
"full": "GitHub Issue Notification Bot"
|
||||
},
|
||||
"icons": {
|
||||
"outline": "outline.png",
|
||||
"color": "color.png"
|
||||
},
|
||||
"accentColor": "#fc4103",
|
||||
"configurableTabs": [
|
||||
],
|
||||
"staticTabs": [
|
||||
],
|
||||
"bots": [
|
||||
{
|
||||
"botId": "<AppId>",
|
||||
"scopes": [
|
||||
"team",
|
||||
"personal",
|
||||
"groupchat"
|
||||
],
|
||||
"needsChannelSelector": false,
|
||||
"isNotificationOnly": false,
|
||||
"supportsFiles": false,
|
||||
"commandLists": [
|
||||
]
|
||||
}
|
||||
],
|
||||
"connectors": [
|
||||
],
|
||||
"composeExtensions": [
|
||||
],
|
||||
"permissions": [
|
||||
"identity",
|
||||
"messageTeamMembers"
|
||||
],
|
||||
"devicePermissions": [
|
||||
],
|
||||
"validDomains": [
|
||||
"token.botframework.com"
|
||||
],
|
||||
"showLoadingIndicator": false,
|
||||
"activities": {
|
||||
}
|
||||
}
|
Двоичные данные
dri/issueNotificationBot/Bot/TeamsAppManifest/outline.png
Двоичные данные
dri/issueNotificationBot/Bot/TeamsAppManifest/outline.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 1.8 KiB |
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"MicrosoftAppId": "",
|
||||
"MicrosoftAppPassword": "",
|
||||
"ConnectionName": "",
|
||||
"CosmosDbEndpoint": "",
|
||||
"CosmosDbAuthKey": "",
|
||||
"CosmosDbDatabaseId": "",
|
||||
"CosmosDbContainerId": "",
|
||||
"ApplicationInsights": {
|
||||
"InstrumentationKey": ""
|
||||
},
|
||||
"EnableTestMode": "false"
|
||||
}
|
|
@ -1,417 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Bot Authentication Sample</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
font-family: Segoe UI;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
background-image: url("data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 4638.9 651.6' style='enable-background:new 0 0 4638.9 651.6;' xml:space='preserve'%3E%3Cstyle type='text/css'%3E .st0%7Bfill:%2355A0E0;%7D .st1%7Bfill:none;%7D .st2%7Bfill:%230058A8;%7D .st3%7Bfill:%23328BD8;%7D .st4%7Bfill:%23B6DCF1;%7D .st5%7Bopacity:0.2;fill:url(%23SVGID_1_);enable-background:new ;%7D%0A%3C/style%3E%3Crect y='1.1' class='st0' width='4640' height='646.3'/%3E%3Cpath class='st1' d='M3987.8,323.6L4310.3,1.1h-65.6l-460.1,460.1c-17.5,17.5-46.1,17.5-63.6,0L3260.9,1.1H0v646.3h3660.3 L3889,418.7c17.5-17.5,46.1-17.5,63.6,0l228.7,228.7h66.6l-260.2-260.2C3970.3,369.8,3970.3,341.1,3987.8,323.6z'/%3E%3Cpath class='st2' d='M3784.6,461.2L4244.7,1.1h-983.9l460.1,460.1C3738.4,478.7,3767.1,478.7,3784.6,461.2z'/%3E%3Cpath class='st3' d='M4640,1.1h-329.8l-322.5,322.5c-17.5,17.5-17.5,46.1,0,63.6l260.2,260.2H4640L4640,1.1L4640,1.1z'/%3E%3Cpath class='st4' d='M3889,418.8l-228.7,228.7h521.1l-228.7-228.7C3935.2,401.3,3906.5,401.3,3889,418.8z'/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='3713.7576' y1='438.1175' x2='3911.4084' y2='14.2535' gradientTransform='matrix(1 0 0 -1 0 641.3969)'%3E%3Cstop offset='0' style='stop-color:%23FFFFFF;stop-opacity:0.5'/%3E%3Cstop offset='1' style='stop-color:%23FFFFFF'/%3E%3C/linearGradient%3E%3Cpath class='st5' d='M3952.7,124.5c-17.5-17.5-46.1-17.5-63.6,0l-523,523h1109.6L3952.7,124.5z'/%3E%3C/svg%3E%0A");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
background-position: right;
|
||||
background-color: #55A0E0;
|
||||
width: 100%;
|
||||
font-size: 44px;
|
||||
height: 120px;
|
||||
color: white;
|
||||
padding: 30px 0 40px 0px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20version%3D%221.1%22%20id%3D%22Layer_1%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20x%3D%220px%22%20y%3D%220px%22%0A%09%20viewBox%3D%220%200%20150.2%20125%22%20style%3D%22enable-background%3Anew%200%200%20150.2%20125%3B%22%20xml%3Aspace%3D%22preserve%22%3E%0A%3Cstyle%20type%3D%22text/css%22%3E%0A%09.st0%7Bfill%3Anone%3B%7D%0A%09.st1%7Bfill%3A%23FFFFFF%3B%7D%0A%3C/style%3E%0A%3Crect%20x%3D%220.5%22%20class%3D%22st0%22%20width%3D%22149.7%22%20height%3D%22125%22/%3E%0A%3Cg%3E%0A%09%3Cpath%20class%3D%22st1%22%20d%3D%22M59%2C102.9L21.8%2C66c-3.5-3.5-3.5-9.1%2C0-12.5l37-36.5l2.9%2C3l-37%2C36.4c-1.8%2C1.8-1.8%2C4.7%2C0%2C6.6l37.2%2C37L59%2C102.9z%22%0A%09%09/%3E%0A%3C/g%3E%0A%3Cg%3E%0A%09%3Cpath%20class%3D%22st1%22%20d%3D%22M92.5%2C102.9l-3-3l37.2-37c0.9-0.9%2C1.4-2%2C1.4-3.3c0-1.2-0.5-2.4-1.4-3.3L89.5%2C20l2.9-3l37.2%2C36.4%0A%09%09c1.7%2C1.7%2C2.6%2C3.9%2C2.6%2C6.3s-0.9%2C4.6-2.6%2C6.3L92.5%2C102.9z%22/%3E%0A%3C/g%3E%0A%3Cg%3E%0A%09%3Cpath%20class%3D%22st1%22%20d%3D%22M90.1%2C68.4c-4.5%2C0-8-3.5-8-8.1c0-4.5%2C3.5-8.1%2C8-8.1c4.4%2C0%2C8%2C3.7%2C8%2C8.1C98.1%2C64.7%2C94.4%2C68.4%2C90.1%2C68.4z%0A%09%09%20M90.1%2C56.5c-2.2%2C0-3.8%2C1.7-3.8%2C3.9c0%2C2.2%2C1.7%2C3.9%2C3.8%2C3.9c1.9%2C0%2C3.8-1.6%2C3.8-3.9S91.9%2C56.5%2C90.1%2C56.5z%22/%3E%0A%3C/g%3E%0A%3Cg%3E%0A%09%3Cpath%20class%3D%22st1%22%20d%3D%22M61.4%2C68.4c-4.5%2C0-8-3.5-8-8.1c0-4.5%2C3.5-8.1%2C8-8.1c4.4%2C0%2C8%2C3.7%2C8%2C8.1C69.5%2C64.7%2C65.8%2C68.4%2C61.4%2C68.4z%0A%09%09%20M61.4%2C56.5c-2.2%2C0-3.8%2C1.7-3.8%2C3.9c0%2C2.2%2C1.7%2C3.9%2C3.8%2C3.9c1.9%2C0%2C3.8-1.6%2C3.8-3.9S63.3%2C56.5%2C61.4%2C56.5z%22/%3E%0A%3C/g%3E%0A%3C/svg%3E%0A");
|
||||
background-repeat: no-repeat;
|
||||
float: left;
|
||||
height: 140px;
|
||||
width: 140px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
padding-left: 1%;
|
||||
color: #FFFFFF;
|
||||
font-family: "Segoe UI";
|
||||
font-size: 72px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.35px;
|
||||
line-height: 96px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-inner-container {
|
||||
min-width: 480px;
|
||||
max-width: 1366px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-inner-container::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.main-content-area {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
color: #000000;
|
||||
font-family: "Segoe UI";
|
||||
font-size: 46px;
|
||||
font-weight: 300;
|
||||
line-height: 62px;
|
||||
}
|
||||
|
||||
.main-text {
|
||||
color: #808080;
|
||||
font-size: 24px;
|
||||
font-family: "Segoe UI";
|
||||
font-size: 24px;
|
||||
font-weight: 200;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.main-text-p1{
|
||||
padding-top: 48px;
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
height: 32px;
|
||||
width: 571px;
|
||||
color: #808080;
|
||||
font-family: "Segoe UI";
|
||||
font-size: 24px;
|
||||
font-weight: 200;
|
||||
line-height: 32px;
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.how-to-build-section {
|
||||
padding-top: 20px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.how-to-build-section>h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.35px;
|
||||
line-height: 22px;
|
||||
margin: 0 0 24px 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.step-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-container dl {
|
||||
border-left: 1px solid #A0A0A0;
|
||||
display: block;
|
||||
padding: 0 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.step-container dl>dt::before {
|
||||
background-color: white;
|
||||
border: 1px solid #A0A0A0;
|
||||
border-radius: 100%;
|
||||
content: '';
|
||||
left: 47px;
|
||||
height: 11px;
|
||||
position: absolute;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.step-container dl>.test-bullet::before {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
.step-container dl>dt {
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.step-container dl>dd {
|
||||
font-size: inherit;
|
||||
line-height: 20px;
|
||||
margin-left: 0;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.step-container:last-child dl {
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
|
||||
.ctaLink {
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: #006AB1;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ctaLink:focus {
|
||||
outline: 1px solid #00bcf2;
|
||||
}
|
||||
|
||||
.ctaLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
display: flex;
|
||||
height: 38px;
|
||||
margin-right: 15px;
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
.step-icon>div {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.ms-logo-container {
|
||||
min-width: 580px;
|
||||
max-width: 980px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transition: bottom 400ms;
|
||||
}
|
||||
|
||||
.ms-logo {
|
||||
float: right;
|
||||
background-image: url("data:image/svg+xml;utf8,%0A%3Csvg%20version%3D%221.1%22%20id%3D%22MS-symbol%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20x%3D%220px%22%20y%3D%220px%22%0A%09%20viewBox%3D%220%200%20400%20120%22%20style%3D%22enable-background%3Anew%200%200%20400%20120%3B%22%20xml%3Aspace%3D%22preserve%22%3E%0A%3Cstyle%20type%3D%22text/css%22%3E%0A%09.st0%7Bfill%3Anone%3B%7D%0A%09.st1%7Bfill%3A%23737474%3B%7D%0A%09.st2%7Bfill%3A%23D63F26%3B%7D%0A%09.st3%7Bfill%3A%23167D3E%3B%7D%0A%09.st4%7Bfill%3A%232E76BC%3B%7D%0A%09.st5%7Bfill%3A%23FDB813%3B%7D%0A%3C/style%3E%0A%3Crect%20x%3D%220.6%22%20class%3D%22st0%22%20width%3D%22398.7%22%20height%3D%22119%22/%3E%0A%3Cpath%20class%3D%22st1%22%20d%3D%22M171.3%2C38.4v43.2h-7.5V47.7h-0.1l-13.4%2C33.9h-5l-13.7-33.9h-0.1v33.9h-6.9V38.4h10.8l12.4%2C32h0.2l13.1-32H171.3%0A%09z%20M177.6%2C41.7c0-1.2%2C0.4-2.2%2C1.3-3c0.9-0.8%2C1.9-1.2%2C3.1-1.2c1.3%2C0%2C2.4%2C0.4%2C3.2%2C1.3c0.8%2C0.8%2C1.3%2C1.8%2C1.3%2C3c0%2C1.2-0.4%2C2.2-1.3%2C3%0A%09c-0.9%2C0.8-1.9%2C1.2-3.2%2C1.2s-2.3-0.4-3.1-1.2C178%2C43.8%2C177.6%2C42.8%2C177.6%2C41.7z%20M185.7%2C50.6v31h-7.3v-31H185.7z%20M207.8%2C76.3%0A%09c1.1%2C0%2C2.3-0.3%2C3.6-0.8c1.3-0.5%2C2.5-1.2%2C3.6-2v6.8c-1.2%2C0.7-2.5%2C1.2-4%2C1.5c-1.5%2C0.3-3.1%2C0.5-4.9%2C0.5c-4.6%2C0-8.3-1.4-11.1-4.3%0A%09c-2.9-2.9-4.3-6.6-4.3-11c0-5%2C1.5-9.1%2C4.4-12.3c2.9-3.2%2C7-4.8%2C12.4-4.8c1.4%2C0%2C2.7%2C0.2%2C4.1%2C0.5c1.4%2C0.4%2C2.5%2C0.8%2C3.3%2C1.2v7%0A%09c-1.1-0.8-2.3-1.5-3.4-1.9c-1.2-0.5-2.4-0.7-3.6-0.7c-2.9%2C0-5.2%2C0.9-7%2C2.8c-1.8%2C1.9-2.7%2C4.4-2.7%2C7.6c0%2C3.1%2C0.8%2C5.6%2C2.5%2C7.3%0A%09C202.6%2C75.4%2C204.9%2C76.3%2C207.8%2C76.3z%20M235.7%2C50.1c0.6%2C0%2C1.1%2C0%2C1.6%2C0.1s0.9%2C0.2%2C1.2%2C0.3v7.4c-0.4-0.3-0.9-0.5-1.7-0.8%0A%09c-0.7-0.3-1.6-0.4-2.7-0.4c-1.8%2C0-3.3%2C0.8-4.5%2C2.3c-1.2%2C1.5-1.9%2C3.8-1.9%2C7v15.6h-7.3v-31h7.3v4.9h0.1c0.7-1.7%2C1.7-3%2C3-4%0A%09C232.2%2C50.6%2C233.8%2C50.1%2C235.7%2C50.1z%20M238.9%2C66.6c0-5.1%2C1.4-9.2%2C4.3-12.2c2.9-3%2C6.9-4.5%2C12.1-4.5c4.8%2C0%2C8.6%2C1.4%2C11.3%2C4.3%0A%09c2.7%2C2.9%2C4.1%2C6.8%2C4.1%2C11.7c0%2C5-1.4%2C9-4.3%2C12c-2.9%2C3-6.8%2C4.5-11.8%2C4.5c-4.8%2C0-8.6-1.4-11.4-4.2C240.3%2C75.3%2C238.9%2C71.4%2C238.9%2C66.6z%0A%09%20M246.5%2C66.3c0%2C3.2%2C0.7%2C5.7%2C2.2%2C7.4c1.5%2C1.7%2C3.6%2C2.6%2C6.3%2C2.6c2.7%2C0%2C4.7-0.9%2C6.1-2.6c1.4-1.7%2C2.1-4.2%2C2.1-7.6c0-3.3-0.7-5.8-2.2-7.5%0A%09c-1.4-1.7-3.4-2.5-6-2.5c-2.7%2C0-4.7%2C0.9-6.2%2C2.7C247.2%2C60.5%2C246.5%2C63%2C246.5%2C66.3z%20M281.5%2C58.8c0%2C1%2C0.3%2C1.9%2C1%2C2.5%0A%09c0.7%2C0.6%2C2.1%2C1.3%2C4.4%2C2.2c2.9%2C1.2%2C5%2C2.5%2C6.1%2C3.9c1.2%2C1.5%2C1.8%2C3.2%2C1.8%2C5.3c0%2C2.9-1.1%2C5.3-3.4%2C7c-2.2%2C1.8-5.3%2C2.7-9.1%2C2.7%0A%09c-1.3%2C0-2.7-0.2-4.3-0.5c-1.6-0.3-2.9-0.7-4-1.2v-7.2c1.3%2C0.9%2C2.8%2C1.7%2C4.3%2C2.2c1.5%2C0.5%2C2.9%2C0.8%2C4.2%2C0.8c1.6%2C0%2C2.9-0.2%2C3.6-0.7%0A%09c0.8-0.5%2C1.2-1.2%2C1.2-2.3c0-1-0.4-1.9-1.2-2.5c-0.8-0.7-2.4-1.5-4.6-2.4c-2.7-1.1-4.6-2.4-5.7-3.8c-1.1-1.4-1.7-3.2-1.7-5.4%0A%09c0-2.8%2C1.1-5.1%2C3.3-6.9c2.2-1.8%2C5.1-2.7%2C8.6-2.7c1.1%2C0%2C2.3%2C0.1%2C3.6%2C0.4c1.3%2C0.2%2C2.5%2C0.6%2C3.4%2C0.9v6.9c-1-0.6-2.1-1.2-3.4-1.7%0A%09c-1.3-0.5-2.6-0.7-3.8-0.7c-1.4%2C0-2.5%2C0.3-3.2%2C0.8C281.9%2C57.1%2C281.5%2C57.8%2C281.5%2C58.8z%20M297.9%2C66.6c0-5.1%2C1.4-9.2%2C4.3-12.2%0A%09c2.9-3%2C6.9-4.5%2C12.1-4.5c4.8%2C0%2C8.6%2C1.4%2C11.3%2C4.3c2.7%2C2.9%2C4.1%2C6.8%2C4.1%2C11.7c0%2C5-1.4%2C9-4.3%2C12c-2.9%2C3-6.8%2C4.5-11.8%2C4.5%0A%09c-4.8%2C0-8.6-1.4-11.4-4.2C299.4%2C75.3%2C297.9%2C71.4%2C297.9%2C66.6z%20M305.5%2C66.3c0%2C3.2%2C0.7%2C5.7%2C2.2%2C7.4c1.5%2C1.7%2C3.6%2C2.6%2C6.3%2C2.6%0A%09c2.7%2C0%2C4.7-0.9%2C6.1-2.6c1.4-1.7%2C2.1-4.2%2C2.1-7.6c0-3.3-0.7-5.8-2.2-7.5c-1.4-1.7-3.4-2.5-6-2.5c-2.7%2C0-4.7%2C0.9-6.2%2C2.7%0A%09C306.3%2C60.5%2C305.5%2C63%2C305.5%2C66.3z%20M353.9%2C56.6h-10.9v25h-7.4v-25h-5.2v-6h5.2v-4.3c0-3.3%2C1.1-5.9%2C3.2-8c2.1-2.1%2C4.8-3.1%2C8.1-3.1%0A%09c0.9%2C0%2C1.7%2C0%2C2.4%2C0.1c0.7%2C0.1%2C1.3%2C0.2%2C1.8%2C0.4V42c-0.2-0.1-0.7-0.3-1.3-0.5c-0.6-0.2-1.3-0.3-2.1-0.3c-1.5%2C0-2.7%2C0.5-3.5%2C1.4%0A%09s-1.2%2C2.4-1.2%2C4.2v3.7h10.9v-7l7.3-2.2v9.2h7.4v6h-7.4v14.5c0%2C1.9%2C0.3%2C3.3%2C1%2C4c0.7%2C0.8%2C1.8%2C1.2%2C3.3%2C1.2c0.4%2C0%2C0.9-0.1%2C1.5-0.3%0A%09c0.6-0.2%2C1.1-0.4%2C1.6-0.7v6c-0.5%2C0.3-1.2%2C0.5-2.3%2C0.7c-1.1%2C0.2-2.1%2C0.3-3.2%2C0.3c-3.1%2C0-5.4-0.8-6.9-2.5c-1.5-1.6-2.3-4.1-2.3-7.4%0A%09V56.6z%22/%3E%0A%3Cg%3E%0A%09%3Crect%20x%3D%2231%22%20y%3D%2224%22%20class%3D%22st2%22%20width%3D%2234.2%22%20height%3D%2234.2%22/%3E%0A%09%3Crect%20x%3D%2268.8%22%20y%3D%2224%22%20class%3D%22st3%22%20width%3D%2234.2%22%20height%3D%2234.2%22/%3E%0A%09%3Crect%20x%3D%2231%22%20y%3D%2261.8%22%20class%3D%22st4%22%20width%3D%2234.2%22%20height%3D%2234.2%22/%3E%0A%09%3Crect%20x%3D%2268.8%22%20y%3D%2261.8%22%20class%3D%22st5%22%20width%3D%2234.2%22%20height%3D%2234.2%22/%3E%0A%3C/g%3E%0A%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.ms-logo-container>div {
|
||||
min-height: 60px;
|
||||
width: 150px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.row {
|
||||
padding: 90px 0px 0 20px;
|
||||
min-width: 480px;
|
||||
max-width: 1366px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.column {
|
||||
float: left;
|
||||
width: 45%;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.row:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.download-the-emulator {
|
||||
height: 20px;
|
||||
color: #0063B1;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
padding-bottom: 70px;
|
||||
}
|
||||
|
||||
.how-to-iframe {
|
||||
max-width: 700px !important;
|
||||
min-width: 650px !important;
|
||||
height: 700px !important;
|
||||
}
|
||||
|
||||
.remove-frame-height {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1300px) {
|
||||
.ms-logo {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
font-size: 40x;
|
||||
}
|
||||
|
||||
.column {
|
||||
float: none;
|
||||
padding-top: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ms-logo-container {
|
||||
padding-top: 30px;
|
||||
min-width: 480px;
|
||||
max-width: 650px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.row {
|
||||
padding: 20px 0px 0 20px;
|
||||
min-width: 480px;
|
||||
max-width: 650px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1370px) {
|
||||
header {
|
||||
background-color: #55A0E0;
|
||||
background-size: auto 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1230px) {
|
||||
header {
|
||||
background-color: #55A0E0;
|
||||
background-size: auto 200px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1000px) {
|
||||
header {
|
||||
background-color: #55A0E0;
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 632px) {
|
||||
.header-text {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.row {
|
||||
padding: 10px 0px 0 10px;
|
||||
max-width: 490px !important;
|
||||
min-width: 410px !important;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.main-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.step-container dl>dd {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.column {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
height: 110px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.how-to-iframe {
|
||||
max-width: 480px !important;
|
||||
min-width: 400px !important;
|
||||
height: 650px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-frame-height {
|
||||
max-height: 10px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadFrame();
|
||||
});
|
||||
var loadFrame = function () {
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.setAttribute("id", "iframe");
|
||||
var offLineHTMLContent = "";
|
||||
var frameElement = document.getElementById("how-to-iframe");
|
||||
if (window.navigator.onLine) {
|
||||
iframe.src = 'https://docs.botframework.com/static/abs/pages/f5.htm';
|
||||
iframe.setAttribute("scrolling", "no");
|
||||
iframe.setAttribute("frameborder", "0");
|
||||
iframe.setAttribute("width", "100%");
|
||||
iframe.setAttribute("height", "100%");
|
||||
var frameDiv = document.getElementById("how-to-iframe");
|
||||
frameDiv.appendChild(iframe);
|
||||
} else {
|
||||
frameElement.classList.add("remove-frame-height");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-inner-container">
|
||||
<div class="header-icon" style="display: inline-block"></div>
|
||||
<div class="header-text" style="display: inline-block">Bot Authentication Sample</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="row">
|
||||
<div class="column" class="main-content-area">
|
||||
<div class="content-title">Your bot is ready!</div>
|
||||
<div class="main-text main-text-p1">You can test your bot in the Bot Framework Emulator<br />
|
||||
by connecting to http://localhost:3978/api/messages.</div>
|
||||
<div class="main-text download-the-emulator"><a class="ctaLink" href="https://aka.ms/bot-framework-F5-download-emulator"
|
||||
target="_blank">Download the Emulator</a></div>
|
||||
<div class="main-text">Visit <a class="ctaLink" href="https://aka.ms/bot-framework-F5-abs-home" target="_blank">Azure
|
||||
Bot Service</a> to register your bot and add it to<br />
|
||||
various channels. The bot's endpoint URL typically looks
|
||||
like this:</div>
|
||||
<div class="endpoint">https://<i>your_bots_hostname</i>/api/messages</div>
|
||||
</div>
|
||||
<div class="column how-to-iframe" id="how-to-iframe"></div>
|
||||
</div>
|
||||
<div class="ms-logo-container">
|
||||
<div class="ms-logo"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,88 +0,0 @@
|
|||
# Issue Notification Bot
|
||||
|
||||
This bot tracks GitHub issues in the Bot Framework repos and sends notifications via Teams to the assignees if:
|
||||
|
||||
* Issue is tagged with `customer-reported`, and:
|
||||
* Issue has not been tagged with `customer-replied` in last 3 business days, or
|
||||
* Issue has not been closed in the last 30 days, or
|
||||
* Issue has not been closed in the last 90 days
|
||||
|
||||
*Note: These are based on SLA timelines*
|
||||
|
||||
It is broken up into two main folders:
|
||||
|
||||
1. AzureFunction - This lives in an Azure Function and queries GitHub every half hour
|
||||
2. Bot - This is the bot. It received data from the Azure Function on `/api/data` and sends notifications via Teams
|
||||
|
||||
## Azure Function
|
||||
|
||||
Every half hour, the Azure Function queries GitHub for all issues within the Bot Framework. It categorizes them based on tags and whether or not they've been replied to, then sends them to the bot at the `/api/data` endpoint.
|
||||
|
||||
### Setup
|
||||
|
||||
Rename `.env.sample` to `.env` and fill out the following environment variables:
|
||||
|
||||
```cmd
|
||||
GitHubToken=<Your GitHub Access Token Created in GitHub>
|
||||
|
||||
MicrosoftAppId=<AppId of the Bot>
|
||||
MicrosoftAppPassword=<AppPassword of the Bot>
|
||||
BotBaseUrl=<https://<myBot>.azurewebsites.net>
|
||||
|
||||
UseTestRepo=<true|false - this allows you to create a test repo and fake issues. The bot then changes the expiration time so the issue appears expired>
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
Run locally via `npm run start:local`
|
||||
|
||||
### Deploy
|
||||
|
||||
This is easiest to deploy by using the [Azure Functions VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions)
|
||||
|
||||
## Bot
|
||||
|
||||
This is a pretty simple, primarily-notifications bot. It listens for the data sent from the Azure Function on `/api/data`, authenticates the request, processes the data, then sends out notifications to the appropriate users based on issue expiration.
|
||||
|
||||
Once installed to a Teams Team/Channel, it queries all members on the Team. For any team members who have not provided their GitHub info to the bot, it sends them a notification message, requesting they log in so that we can notify them.
|
||||
|
||||
### Setup
|
||||
|
||||
Rename `appsettings.json.sample` to `appsettings.json` and fill out the following environment variables:
|
||||
|
||||
```json
|
||||
{
|
||||
"MicrosoftAppId": "",
|
||||
"MicrosoftAppPassword": "",
|
||||
"ConnectionName": "<OAuth Connection Name (for GitHub OAuth)>",
|
||||
"CosmosDbEndpoint": "",
|
||||
"CosmosDbAuthKey": "",
|
||||
"CosmosDbDatabaseId": "",
|
||||
"CosmosDbContainerId": "",
|
||||
"ApplicationInsights": {
|
||||
"InstrumentationKey": ""
|
||||
},
|
||||
"EnableTestMode": "<true|false - this allows you to create a test repo and fake issues. The bot then changes the expiration time so the issue appears expired>"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Deploy
|
||||
|
||||
Deploy like a standard bot. Be sure to update the Teams App Manifest.
|
||||
|
||||
### Maintainer
|
||||
|
||||
The Bot has some additional commands that it only responds to if they come from the maintainer listed in `Bot/Constants.cs > MaintainerGitHubId`. Currently, the commands are:
|
||||
|
||||
* `command:enableNotifications`: Send bot error notifications to the Maintainer via Teams.
|
||||
* `command:disableNotifications`: Turn off notifications of bot errors.
|
||||
* `command:sendCards`: Send test adaptive cards to the maintainer (for testing purposes).
|
||||
* `command:resendGreetings`: Query the channel the message comes from for all team members. For any team members who have not provided their GitHub info to the bot, it sends them a notification message, requesting they log in so that we can notify them.
|
||||
|
||||
## Links
|
||||
|
||||
* [Bot Web App](https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/0389857f-2464-451b-ac83-5f54d565b1a7/resourceGroups/v-micricMAIN/providers/Microsoft.BotService/botServices/IssueNotificationBot/overview)
|
||||
* [Bot App Service](https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/0389857f-2464-451b-ac83-5f54d565b1a7/resourceGroups/v-micricMAIN/providers/Microsoft.Web/sites/issuenotificationbot/appServices)
|
||||
* [Azure Function](https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/0389857f-2464-451b-ac83-5f54d565b1a7/resourceGroups/v-micricMAIN/providers/Microsoft.Web/sites/IssueNotificationIssueRetriever/appServices)
|
||||
* [Cosmos Storage](https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/0389857f-2464-451b-ac83-5f54d565b1a7/resourceGroups/v-micricMAIN/providers/Microsoft.DocumentDb/databaseAccounts/vmicriccosmos/dataExplorer)
|
Загрузка…
Ссылка в новой задаче