private HeroCard RenderLinkHeroCard(RedditLinkModel post) { return(new HeroCard { Title = post.Title, Text = post.Subreddit, Images = new List <CardImage> { new CardImage(post.Thumbnail), }, }); }
private async Task <MessagingExtensionResponse> GetRedditPostAsync(string postLink) { try { // Execute the domain logic to get the reddit link RedditLinkModel redditLink = await this.redditHttpClient.GetLinkAsync(postLink); var preview = new MessagingExtensionAttachment( contentType: HeroCard.ContentType, contentUrl: null, content: this.RenderLinkHeroCard(redditLink)); return(new MessagingExtensionResponse { ComposeExtension = new MessagingExtensionResult { Type = "result", AttachmentLayout = AttachmentLayoutTypes.List, Attachments = new List <MessagingExtensionAttachment>() { new MessagingExtensionAttachment { ContentType = AdaptiveCard.ContentType, Content = this.RenderLinkAdaptiveCard(redditLink), Preview = preview, }, }, }, }); } #pragma warning disable CA1031 // This is a top-level handler and should avoid throwing exceptions. catch (Exception ex) #pragma warning restore CA1031 { this.logger.LogError(ex, "Failed to get reddit post"); return(null); } }
private AdaptiveCard RenderLinkAdaptiveCard(RedditLinkModel post) { var titleBlock = new AdaptiveTextBlock { Text = $"[{post.Title}]({post.Link})", Size = AdaptiveTextSize.Large, Wrap = true, MaxLines = 2, }; var upvoteColumn = new AdaptiveColumn { Width = AdaptiveColumnWidth.Auto, Items = new List <AdaptiveElement> { new AdaptiveTextBlock { Text = this.localizer.GetString("↑ {0}", post.Score), }, }, }; var commentColumn = new AdaptiveColumn { Width = AdaptiveColumnWidth.Auto, Items = new List <AdaptiveElement> { new AdaptiveTextBlock { Text = this.localizer.GetString( "🗨️ [{0}](https://www.reddit.com/r/{1}/comments/{2})", post.NumComments, post.Subreddit, post.Id), }, }, }; var subredditColumn = new AdaptiveColumn { Width = AdaptiveColumnWidth.Stretch, Items = new List <AdaptiveElement> { new AdaptiveTextBlock { Text = $"[/r/{post.Subreddit}](https://www.reddit.com/r/{post.Subreddit})", HorizontalAlignment = AdaptiveHorizontalAlignment.Right, Size = AdaptiveTextSize.Default, Weight = AdaptiveTextWeight.Bolder, }, }, }; var infoColumns = new AdaptiveColumnSet { Columns = new List <AdaptiveColumn> { upvoteColumn, commentColumn, subredditColumn, }, }; AdaptiveElement preview; if (post.Thumbnail != null) { preview = new AdaptiveImage { Url = new Uri(post.Thumbnail), HorizontalAlignment = AdaptiveHorizontalAlignment.Center, Separator = true, }; } else { preview = new AdaptiveTextBlock { Text = post.SelfText ?? this.localizer.GetString("Preview Not Available"), Wrap = true, Separator = true, }; } var bottomLeftColumn = new AdaptiveColumn { Width = AdaptiveColumnWidth.Auto, Items = new List <AdaptiveElement> { new AdaptiveTextBlock { Text = this.localizer.GetString("Posted by [/u/{0}](https://www.reddit.com/u/{0})", post.Author), Size = AdaptiveTextSize.Small, Weight = AdaptiveTextWeight.Lighter, }, }, }; var createdText = $"{{{{DATE({post.Created.DateTime.ToString("yyyy-MM-ddThh:mm:ssZ", CultureInfo.InvariantCulture)})}}}}"; var bottomRightColumn = new AdaptiveColumn { Width = AdaptiveColumnWidth.Stretch, Items = new List <AdaptiveElement> { new AdaptiveTextBlock { Text = createdText, HorizontalAlignment = AdaptiveHorizontalAlignment.Right, Size = AdaptiveTextSize.Small, Weight = AdaptiveTextWeight.Lighter, }, }, }; var bottomColumns = new AdaptiveColumnSet { Columns = new List <AdaptiveColumn> { bottomLeftColumn, bottomRightColumn, }, }; var card = new AdaptiveCard("1.0") { Body = new List <AdaptiveElement> { titleBlock, infoColumns, preview, bottomColumns, }, Actions = new List <AdaptiveAction> { new AdaptiveOpenUrlAction { Title = this.localizer.GetString("Open in Reddit"), Url = new Uri(post.Link), }, }, }; return(card); }
/// <summary> /// Get the information about a post. /// </summary> /// <param name="authToken">The reddit auth token.</param> /// <param name="postLink">The url to the reddit post.</param> /// <returns>A Task resolving to the reddit link model for the post.</returns> /// <exception cref="RedditUnauthorizedException"> Thrown when the call to Reddit API was unauthorized.</exception> /// <exception cref="RedditRequestException"> Thrown when the call to Reddit API was unsuccessful.</exception> /// <exception cref="ArgumentException"> Thrown when post link is malformed or not for a post.</exception> /// <remarks> /// See: https://www.reddit.com/dev/api#GET_api_info . /// </remarks> public async Task <RedditLinkModel> GetLinkAsync(string authToken, string postLink) { if (string.IsNullOrEmpty(authToken)) { // Throw an exception to cause the activity handler to prompt the user for login. throw new RedditUnauthorizedException($"{nameof(authToken)} is null"); } Match m = RedditHttpClient.ParameterExtrator.Matches(postLink)?[0]; string subreddit = m?.Groups?["subreddit"]?.Value; string id = m?.Groups?["id"]?.Value; if (string.IsNullOrEmpty(subreddit)) { throw new ArgumentException($"Unable to find subreddit in url: ${postLink}", nameof(postLink)); } if (string.IsNullOrEmpty(id)) { throw new ArgumentException($"Unable to find 'thing-id' in url: ${postLink}", nameof(postLink)); } this.logger.LogInformation($"Extracted id: {id}, subreddit: {subreddit}"); string stringContent = string.Empty; using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"https://oauth.reddit.com/r/{subreddit}/api/info?id=t3_{id}")) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken); request.Headers.UserAgent.Add(new ProductInfoHeaderValue(this.options.Value.ClientUserAgent, "1.0")); HttpResponseMessage result = await this.httpClient.SendAsync(request).ConfigureAwait(false); if (result.StatusCode == HttpStatusCode.Unauthorized) { // Throw an exception to cause the activity handler to prompt the user for login. throw new RedditUnauthorizedException(result.ReasonPhrase); } if (!result.IsSuccessStatusCode) { throw new RedditRequestException(result.ReasonPhrase); } stringContent = await result.Content .ReadAsStringAsync() .ConfigureAwait(false); } JObject root = JObject.Parse(stringContent); JToken firstPost = root?["data"]?["children"]?[0]?["data"]; // Preview images are html encoded urls. string thumbnailUrl = WebUtility.HtmlDecode((string)firstPost?["preview"]?["images"]?[0]?["source"]?["url"]); RedditLinkModel redditLink = new RedditLinkModel { Id = (string)firstPost?["id"], Title = (string)firstPost?["title"], Score = (int)firstPost?["score"], Subreddit = (string)firstPost?["subreddit"], Thumbnail = thumbnailUrl, SelfText = (string)firstPost?["selftext"], NumComments = (int)firstPost?["num_comments"], Link = "https://www.reddit.com" + (string)firstPost?["permalink"], Author = (string)firstPost?["author"], Created = DateTimeOffset.FromUnixTimeSeconds((long)firstPost?["created_utc"]), }; return(redditLink); }
/// <summary> /// Get the login or preview response for the given link. /// </summary> /// <param name="turnContext">The turn context.</param> /// <param name="query">The matched url.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A Task resolving to either a login card or </returns> /// <remarks> /// For more information on Link Unfurling see the documentation /// https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=dotnet /// /// This method also implements messaging extension authentication to get the reddit API token for the user. /// https://docs.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/add-authentication /// </remarks> protected override async Task <MessagingExtensionResponse> OnTeamsAppBasedLinkQueryAsync( ITurnContext <IInvokeActivity> turnContext, AppBasedLinkQuery query, CancellationToken cancellationToken = default) { turnContext = turnContext ?? throw new ArgumentNullException(nameof(turnContext)); query = query ?? throw new ArgumentNullException(nameof(query)); IUserTokenProvider tokenProvider = turnContext.Adapter as IUserTokenProvider; // Get the magic code out of the request for when the login flow is completed. // https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0#securing-the-sign-in-url string magicCode = (turnContext.Activity?.Value as JObject)?.Value <string>("state"); // Get the token from the Azure Bot Framework Token Service to handle token storage // https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0#about-the-bot-framework-token-service var tokenResponse = await tokenProvider ?.GetUserTokenAsync( turnContext : turnContext, connectionName : this.options.Value.BotFrameworkConnectionName, magicCode : magicCode, cancellationToken : cancellationToken); try { // Execute the domain logic to get the reddit link RedditLinkModel redditLink = await this.redditHttpClient.GetLinkAsync(tokenResponse?.Token, query.Url); var preview = new MessagingExtensionAttachment( contentType: HeroCard.ContentType, contentUrl: null, content: this.RenderLinkHeroCard(redditLink)); return(new MessagingExtensionResponse { ComposeExtension = new MessagingExtensionResult { Type = "result", AttachmentLayout = AttachmentLayoutTypes.List, Attachments = new List <MessagingExtensionAttachment>() { new MessagingExtensionAttachment { ContentType = AdaptiveCard.ContentType, Content = this.RenderLinkAdaptiveCard(redditLink), Preview = preview, }, }, }, }); } catch (RedditUnauthorizedException) { this.logger.LogInformation("Attempt to fetch post resulted in unauthorized, triggering log-in flow"); // "log out" the user, so log-in gets a new token. await tokenProvider.SignOutUserAsync( turnContext : turnContext, connectionName : this.options.Value.BotFrameworkConnectionName, cancellationToken : cancellationToken); return(await this .GetAuthenticationMessagingExtensionResponseAsync(turnContext, cancellationToken) .ConfigureAwait(false)); } #pragma warning disable CA1031 catch (Exception ex) #pragma warning restore CA1031 { this.logger.LogError(ex, "Failed to get reddit post"); return(null); } }
private AdaptiveCard RenderLinkAdaptiveCard(RedditLinkModel post) { var titleBlock = new AdaptiveTextBlock { Text = post.Title, Size = AdaptiveTextSize.Large, Wrap = true, MaxLines = 2, }; var upvoteColumn = new AdaptiveColumn { Width = AdaptiveColumnWidth.Auto, Items = new List <AdaptiveElement> { new AdaptiveTextBlock { Text = $"↑ {post.Score}", }, }, }; var commentColumn = new AdaptiveColumn { Width = AdaptiveColumnWidth.Auto, Items = new List <AdaptiveElement> { new AdaptiveTextBlock { Text = $"🗨️ {post.NumComments}", }, }, }; var subredditColumn = new AdaptiveColumn { Width = AdaptiveColumnWidth.Stretch, Items = new List <AdaptiveElement> { new AdaptiveTextBlock { Text = $"/r/{post.Subreddit}", HorizontalAlignment = AdaptiveHorizontalAlignment.Right, Size = AdaptiveTextSize.Default, Weight = AdaptiveTextWeight.Bolder, }, }, }; var infoColumns = new AdaptiveColumnSet { Columns = new List <AdaptiveColumn> { upvoteColumn, commentColumn, subredditColumn, }, }; AdaptiveElement preview; if (post.Thumbnail != null) { preview = new AdaptiveImage { Url = new Uri(post.Thumbnail), HorizontalAlignment = AdaptiveHorizontalAlignment.Center, Separator = true, }; } else { preview = new AdaptiveTextBlock { Text = post.SelfText ?? this.localizer.GetString("Preview not available"), Wrap = true, Separator = true, }; } return(new AdaptiveCard("1.0") { Body = new List <AdaptiveElement> { titleBlock, infoColumns, preview, }, Actions = new List <AdaptiveAction> { new AdaptiveOpenUrlAction { Title = this.localizer.GetString("Open in Reddit"), Url = new Uri(post.Link), }, }, }); }