From d1528f0b701838215d49813fc1d9e74371a8e046 Mon Sep 17 00:00:00 2001 From: Chebotov Nikolay Date: Sat, 28 Mar 2020 13:24:33 +0300 Subject: [PATCH] Add feature: get original tweets from AzureService; Update nuget-packages for AzureService. --- DotNetRu.AzureService/AppSettings.cs | 7 ++ DotNetRu.AzureService/ApplicationLogging.cs | 15 +++ .../Controllers/TweetController.cs | 51 ++++++++++ .../DotNetRu.AzureService.csproj | 5 +- DotNetRu.AzureService/Startup.cs | 9 +- .../Tweet/StringExtensions.cs | 20 ++++ DotNetRu.AzureService/Tweet/Tweet.cs | 77 +++++++++++++++ DotNetRu.AzureService/Tweet/TweetService.cs | 99 +++++++++++++++++++ .../appsettings.Development.json | 4 + 9 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 DotNetRu.AzureService/ApplicationLogging.cs create mode 100644 DotNetRu.AzureService/Controllers/TweetController.cs create mode 100644 DotNetRu.AzureService/Tweet/StringExtensions.cs create mode 100644 DotNetRu.AzureService/Tweet/Tweet.cs create mode 100644 DotNetRu.AzureService/Tweet/TweetService.cs diff --git a/DotNetRu.AzureService/AppSettings.cs b/DotNetRu.AzureService/AppSettings.cs index f40b23b7..feadd6cb 100644 --- a/DotNetRu.AzureService/AppSettings.cs +++ b/DotNetRu.AzureService/AppSettings.cs @@ -10,4 +10,11 @@ public class RealmSettings public string RealmName { get; set; } } + + public class TweetSettings + { + public string ConsumerKey { get; set; } + + public string ConsumerSecret { get; set; } + } } diff --git a/DotNetRu.AzureService/ApplicationLogging.cs b/DotNetRu.AzureService/ApplicationLogging.cs new file mode 100644 index 00000000..e3a2ad36 --- /dev/null +++ b/DotNetRu.AzureService/ApplicationLogging.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Logging; + +namespace DotNetRu.Azure +{ + /// + /// Shared logger + /// + internal static class ApplicationLogging + { + internal static ILoggerFactory LoggerFactory { get; set; }// = new LoggerFactory(); + internal static ILogger CreateLogger() => LoggerFactory.CreateLogger(); + internal static ILogger CreateLogger(string categoryName) => LoggerFactory.CreateLogger(categoryName); + + } +} diff --git a/DotNetRu.AzureService/Controllers/TweetController.cs b/DotNetRu.AzureService/Controllers/TweetController.cs new file mode 100644 index 00000000..62d8de60 --- /dev/null +++ b/DotNetRu.AzureService/Controllers/TweetController.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using DotNetRu.AzureService; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace DotNetRu.Azure +{ + [Route("tweet")] + public class TweetController : ControllerBase + { + private readonly ILogger logger; + + private readonly TweetSettings tweetSettings; + + private readonly PushNotificationsManager pushNotificationsManager; + + public TweetController( + ILogger logger, + TweetSettings tweetSettings, + PushNotificationsManager pushNotificationsManager) + { + this.logger = logger; + this.tweetSettings = tweetSettings; + this.pushNotificationsManager = pushNotificationsManager; + } + + [HttpGet] + [Route("get_original")] + public async Task GetOriginalTweets() + { + try + { + var tweets = await TweetService.GetAsync(tweetSettings); + var json = JsonConvert.SerializeObject(tweets); + + return new OkObjectResult(json); + } + catch (Exception e) + { + logger.LogCritical(e, "Unhandled error while getting original tweets"); + return new ObjectResult(e) + { + StatusCode = StatusCodes.Status500InternalServerError + }; + } + } + } +} diff --git a/DotNetRu.AzureService/DotNetRu.AzureService.csproj b/DotNetRu.AzureService/DotNetRu.AzureService.csproj index 5f64eca1..95ea9b5c 100644 --- a/DotNetRu.AzureService/DotNetRu.AzureService.csproj +++ b/DotNetRu.AzureService/DotNetRu.AzureService.csproj @@ -7,13 +7,14 @@ + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/DotNetRu.AzureService/Startup.cs b/DotNetRu.AzureService/Startup.cs index 76d9df0b..11a29b8a 100644 --- a/DotNetRu.AzureService/Startup.cs +++ b/DotNetRu.AzureService/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace DotNetRu.AzureService { @@ -22,10 +23,14 @@ public void ConfigureServices(IServiceCollection services) var realmSettings = new RealmSettings(); Configuration.Bind("RealmOptions", realmSettings); + var tweetSettings = new TweetSettings(); + Configuration.Bind("TweetOptions", tweetSettings); + var pushSettings = new PushSettings(); Configuration.Bind("PushOptions", pushSettings); services.AddSingleton(realmSettings); + services.AddSingleton(tweetSettings); services.AddSingleton(pushSettings); services.AddScoped(); @@ -38,8 +43,10 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory logFactory) { + ApplicationLogging.LoggerFactory = logFactory; + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); diff --git a/DotNetRu.AzureService/Tweet/StringExtensions.cs b/DotNetRu.AzureService/Tweet/StringExtensions.cs new file mode 100644 index 00000000..30f314be --- /dev/null +++ b/DotNetRu.AzureService/Tweet/StringExtensions.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace DotNetRu.Azure +{ + public static class StringExtensions + { + internal static string ConvertToUsualUrl(this string input, List> replacements) + { + var returnString = new StringBuilder(input); + foreach (var replacement in replacements) + { + returnString.Replace(replacement.Key, replacement.Value); + } + + return Regex.Replace(returnString.ToString(), @"https:\/\/t\.co\/.+$", string.Empty); + } + } +} diff --git a/DotNetRu.AzureService/Tweet/Tweet.cs b/DotNetRu.AzureService/Tweet/Tweet.cs new file mode 100644 index 00000000..859ab19f --- /dev/null +++ b/DotNetRu.AzureService/Tweet/Tweet.cs @@ -0,0 +1,77 @@ +using System; + +namespace DotNetRu.Azure +{ + public class Tweet + { + public Tweet(ulong statusID) + { + StatusID = statusID; + } + + private string tweetedImage; + + public bool HasImage => !string.IsNullOrWhiteSpace(this.tweetedImage); + + public string TweetedImage + { + get => this.tweetedImage; + set => this.tweetedImage = value; + } + + public int? NumberOfLikes { get; set; } + + public int NumberOfRetweets { get; set; } + + public int NumberOfComments { get; set; } + + public ulong StatusID { get; } + + public string Text { get; set; } + + public string Image { get; set; } + + public string Url { get; set; } + + public string Name { get; set; } + + public string ScreenName { get; set; } + + public DateTime CreatedDate { get; set; } + + public string TitleDisplay => this.Name; + + public string SubtitleDisplay => "@" + this.ScreenName; + + public string DateDisplay => this.CreatedDate.ToShortDateString(); + + public Uri TweetedImageUri + { + get + { + try + { + if (string.IsNullOrWhiteSpace(this.TweetedImage)) + { + return null; + } + + return new Uri(this.TweetedImage); + } + catch + { + // TODO ignored + } + + return null; + } + } + + public bool HasAttachedImage => !string.IsNullOrWhiteSpace(this.TweetedImage); + + public override string ToString() + { + return $"[Name={Name};Text={Text};Retweets={NumberOfRetweets};Likes={NumberOfLikes}"; + } + } +} diff --git a/DotNetRu.AzureService/Tweet/TweetService.cs b/DotNetRu.AzureService/Tweet/TweetService.cs new file mode 100644 index 00000000..dffcd964 --- /dev/null +++ b/DotNetRu.AzureService/Tweet/TweetService.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DotNetRu.AzureService; +using LinqToTwitter; +using Microsoft.Extensions.Logging; + +namespace DotNetRu.Azure +{ + internal class TweetService + { + private static readonly ILogger Logger = ApplicationLogging.CreateLogger(nameof(TweetService)); + + /// + /// Returns tweets by SpdDotNet/DotNetRu (if it's retweet then original tweet is returned instead of retweet) + /// + /// + /// Returns a list of tweets. + /// + internal static async Task> GetAsync(TweetSettings tweetSettings) + { + try + { + var auth = new ApplicationOnlyAuthorizer + { + CredentialStore = + new InMemoryCredentialStore + { + ConsumerKey = + tweetSettings.ConsumerKey, + ConsumerSecret = + tweetSettings.ConsumerSecret + }, + }; + await auth.AuthorizeAsync(); + + using var twitterContext = new TwitterContext(auth); + var spbDotNetTweets = + await (from tweet in twitterContext.Status + where tweet.Type == StatusType.User && tweet.ScreenName == "spbdotnet" + && tweet.TweetMode == TweetMode.Extended + select tweet).ToListAsync(); + + var dotnetRuTweets = + await (from tweet in twitterContext.Status + where tweet.Type == StatusType.User && tweet.ScreenName == "DotNetRu" + && tweet.TweetMode == TweetMode.Extended + select tweet).ToListAsync(); + + var unitedTweets = spbDotNetTweets.Union(dotnetRuTweets).Where(tweet => !tweet.PossiblySensitive).Select(GetTweet); + + var tweetsWithoutDuplicates = unitedTweets.GroupBy(tw => tw.StatusID).Select(g => g.First()); + + var sortedTweets = tweetsWithoutDuplicates.OrderByDescending(x => x.CreatedDate).ToList(); + + return sortedTweets; + } + catch (Exception e) + { + Logger.LogError(e, "Unhandled error while getting original tweets"); + } + + return new List(); + } + + private static Tweet GetTweet(Status tweet) + { + var sourceTweet = tweet.RetweetedStatus.StatusID == 0 ? tweet : tweet.RetweetedStatus; + + var urlLinks = + sourceTweet.Entities.UrlEntities.Select(t => new KeyValuePair(t.Url, t.DisplayUrl)).ToList(); + + var profileImage = sourceTweet.User?.ProfileImageUrl.Replace("http://", "https://", StringComparison.InvariantCultureIgnoreCase); + if (profileImage != null) + { + //normal image is 48x48, bigger image is 73x73, see https://developer.twitter.com/en/docs/accounts-and-users/user-profile-images-and-banners + profileImage = Regex.Replace(profileImage, @"(.+)_normal(\..+)", "$1_bigger$2"); + } + + return new Tweet(sourceTweet.StatusID) + { + TweetedImage = + tweet.Entities?.MediaEntities.Count > 0 + ? tweet.Entities?.MediaEntities?[0].MediaUrlHttps ?? string.Empty + : string.Empty, + NumberOfLikes = sourceTweet.FavoriteCount, + NumberOfRetweets = sourceTweet.RetweetCount, + ScreenName = sourceTweet.User?.ScreenNameResponse ?? string.Empty, + Text = sourceTweet.FullText.ConvertToUsualUrl(urlLinks), + Name = sourceTweet.User?.Name, + CreatedDate = tweet.CreatedAt, + Url = $"https://twitter.com/{sourceTweet.User?.ScreenNameResponse}/status/{tweet.StatusID}", + Image = profileImage + }; + } + } +} diff --git a/DotNetRu.AzureService/appsettings.Development.json b/DotNetRu.AzureService/appsettings.Development.json index c45ef095..0afbb087 100644 --- a/DotNetRu.AzureService/appsettings.Development.json +++ b/DotNetRu.AzureService/appsettings.Development.json @@ -12,6 +12,10 @@ "RealmServerUrl": "dotnetru.de1a.cloud.realm.io", "RealmName": "dotnetru_050919" }, + "TweetOptions": { + "ConsumerKey": "ho0v2B1bimeufLqI1rA8KuLBp", + "ConsumerSecret": "RAzIHxhkzINUxilhdr98TWTtjgFKXYzkEhaGx8WJiBPh96TXNK" + }, "PushOptions": { "AppType": "beta", "AppCenterAppNames": [ "DotNetRu-Android-App", "DotNetRu-iOS-App" ],