This commit is contained in:
tracyboehrer 2022-05-19 12:16:57 -05:00 коммит произвёл GitHub
Родитель 823c008cc1
Коммит 7f88c14385
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
49 изменённых файлов: 0 добавлений и 7434 удалений

16
dri/issueNotificationBot/.gitignore поставляемый
Просмотреть файл

@ -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);

4158
dri/issueNotificationBot/AzureFunction/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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();
}
}
}

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 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": {
}
}

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 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)