wip 1
This commit is contained in:
parent
ed3faab924
commit
6b2579db50
23 changed files with 226 additions and 201 deletions
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
||||||
<PackageReference Include="TweetinviAPI" Version="4.0.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -2,129 +2,109 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using BirdsiteLive.Twitter.Models;
|
using BirdsiteLive.Twitter.Models;
|
||||||
using Tweetinvi.Models;
|
|
||||||
using Tweetinvi.Models.Entities;
|
|
||||||
|
|
||||||
namespace BirdsiteLive.Twitter.Extractors
|
namespace BirdsiteLive.Twitter.Extractors
|
||||||
{
|
{
|
||||||
public interface ITweetExtractor
|
public interface ITweetExtractor
|
||||||
{
|
{
|
||||||
ExtractedTweet Extract(ITweet tweet);
|
ExtractedTweet Extract(JsonDocument tweet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TweetExtractor : ITweetExtractor
|
public class TweetExtractor : ITweetExtractor
|
||||||
{
|
{
|
||||||
public ExtractedTweet Extract(ITweet tweet)
|
public ExtractedTweet Extract(JsonDocument tweet)
|
||||||
{
|
{
|
||||||
var extractedTweet = new ExtractedTweet
|
var extractedTweet = new ExtractedTweet
|
||||||
{
|
{
|
||||||
Id = tweet.Id,
|
Id = tweet.RootElement.GetProperty("data").GetProperty("id").GetInt64(),
|
||||||
InReplyToStatusId = tweet.InReplyToStatusId,
|
InReplyToStatusId = tweet.RootElement.GetProperty("data").GetProperty("in_reply_to_status_id").GetInt64(),
|
||||||
InReplyToAccount = tweet.InReplyToScreenName,
|
InReplyToAccount = tweet.RootElement.GetProperty("data").GetProperty("in_reply_to_status_id").GetString(),
|
||||||
MessageContent = ExtractMessage(tweet),
|
MessageContent = ExtractMessage(tweet),
|
||||||
Media = ExtractMedia(tweet),
|
Media = ExtractMedia(tweet),
|
||||||
CreatedAt = tweet.CreatedAt.ToUniversalTime(),
|
CreatedAt = tweet.RootElement.GetProperty("data").GetProperty("in_reply_to_status_id").GetDateTime(),
|
||||||
IsReply = tweet.InReplyToUserId != null,
|
IsReply = false,
|
||||||
IsThread = tweet.InReplyToUserId != null && tweet.InReplyToUserId == tweet.CreatedBy.Id,
|
IsThread = false,
|
||||||
IsRetweet = tweet.IsRetweet || tweet.QuotedStatusId != null,
|
IsRetweet = false,
|
||||||
RetweetUrl = ExtractRetweetUrl(tweet)
|
RetweetUrl = ExtractRetweetUrl(tweet)
|
||||||
};
|
};
|
||||||
|
|
||||||
return extractedTweet;
|
return extractedTweet;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ExtractRetweetUrl(ITweet tweet)
|
private string ExtractRetweetUrl(JsonDocument tweet)
|
||||||
{
|
{
|
||||||
if (tweet.IsRetweet)
|
var retweetId = "123";
|
||||||
{
|
|
||||||
if (tweet.RetweetedTweet != null)
|
|
||||||
{
|
|
||||||
return tweet.RetweetedTweet.Url;
|
|
||||||
}
|
|
||||||
if (tweet.FullText.Contains("https://t.co/"))
|
|
||||||
{
|
|
||||||
var retweetId = tweet.FullText.Split(new[] { "https://t.co/" }, StringSplitOptions.RemoveEmptyEntries).Last();
|
|
||||||
return $"https://t.co/{retweetId}";
|
return $"https://t.co/{retweetId}";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
private string ExtractMessage(JsonDocument tweet)
|
||||||
}
|
|
||||||
|
|
||||||
public string ExtractMessage(ITweet tweet)
|
|
||||||
{
|
{
|
||||||
var message = tweet.FullText;
|
return "hello world";
|
||||||
var tweetUrls = tweet.Media.Select(x => x.URL).Distinct();
|
//var message = tweet.FullText;
|
||||||
|
//var tweetUrls = tweet.Media.Select(x => x.URL).Distinct();
|
||||||
|
|
||||||
if (tweet.IsRetweet && message.StartsWith("RT") && tweet.RetweetedTweet != null)
|
//if (tweet.IsRetweet && message.StartsWith("RT") && tweet.RetweetedTweet != null)
|
||||||
{
|
//{
|
||||||
message = tweet.RetweetedTweet.FullText;
|
// message = tweet.RetweetedTweet.FullText;
|
||||||
tweetUrls = tweet.RetweetedTweet.Media.Select(x => x.URL).Distinct();
|
// tweetUrls = tweet.RetweetedTweet.Media.Select(x => x.URL).Distinct();
|
||||||
|
//}
|
||||||
|
|
||||||
|
//foreach (var tweetUrl in tweetUrls)
|
||||||
|
//{
|
||||||
|
// if(tweet.IsRetweet)
|
||||||
|
// message = tweet.RetweetedTweet.FullText.Replace(tweetUrl, string.Empty).Trim();
|
||||||
|
// else
|
||||||
|
// message = message.Replace(tweetUrl, string.Empty).Trim();
|
||||||
|
//}
|
||||||
|
|
||||||
|
//if (tweet.QuotedTweet != null) message = $"[Quote {{RT}}]{Environment.NewLine}{message}";
|
||||||
|
//if (tweet.IsRetweet)
|
||||||
|
//{
|
||||||
|
// if (tweet.RetweetedTweet != null && !message.StartsWith("RT"))
|
||||||
|
// message = $"[{{RT}} @{tweet.RetweetedTweet.CreatedBy.ScreenName}]{Environment.NewLine}{message}";
|
||||||
|
// else if (tweet.RetweetedTweet != null && message.StartsWith($"RT @{tweet.RetweetedTweet.CreatedBy.ScreenName}:"))
|
||||||
|
// message = message.Replace($"RT @{tweet.RetweetedTweet.CreatedBy.ScreenName}:", $"[{{RT}} @{tweet.RetweetedTweet.CreatedBy.ScreenName}]{Environment.NewLine}");
|
||||||
|
// else
|
||||||
|
// message = message.Replace("RT", "[{{RT}}]");
|
||||||
|
//}
|
||||||
|
|
||||||
|
//// Expand URLs
|
||||||
|
//foreach (var url in tweet.Urls.OrderByDescending(x => x.URL.Length))
|
||||||
|
// message = message.Replace(url.URL, url.ExpandedURL);
|
||||||
|
|
||||||
|
//return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var tweetUrl in tweetUrls)
|
private ExtractedMedia[] ExtractMedia(JsonDocument tweet)
|
||||||
{
|
{
|
||||||
if(tweet.IsRetweet)
|
//var media = tweet.Media;
|
||||||
message = tweet.RetweetedTweet.FullText.Replace(tweetUrl, string.Empty).Trim();
|
//if (tweet.IsRetweet && tweet.RetweetedTweet != null)
|
||||||
else
|
// media = tweet.RetweetedTweet.Media;
|
||||||
message = message.Replace(tweetUrl, string.Empty).Trim();
|
|
||||||
|
//var result = new List<ExtractedMedia>();
|
||||||
|
//foreach (var m in media)
|
||||||
|
//{
|
||||||
|
// var mediaUrl = GetMediaUrl(m);
|
||||||
|
// var mediaType = GetMediaType(m.MediaType, mediaUrl);
|
||||||
|
// if (mediaType == null) continue;
|
||||||
|
|
||||||
|
// var att = new ExtractedMedia
|
||||||
|
// {
|
||||||
|
// MediaType = mediaType,
|
||||||
|
// Url = mediaUrl
|
||||||
|
// };
|
||||||
|
// result.Add(att);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//return result.ToArray();
|
||||||
|
return Array.Empty<ExtractedMedia>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tweet.QuotedTweet != null) message = $"[Quote {{RT}}]{Environment.NewLine}{message}";
|
|
||||||
if (tweet.IsRetweet)
|
|
||||||
{
|
|
||||||
if (tweet.RetweetedTweet != null && !message.StartsWith("RT"))
|
|
||||||
message = $"[{{RT}} @{tweet.RetweetedTweet.CreatedBy.ScreenName}]{Environment.NewLine}{message}";
|
|
||||||
else if (tweet.RetweetedTweet != null && message.StartsWith($"RT @{tweet.RetweetedTweet.CreatedBy.ScreenName}:"))
|
|
||||||
message = message.Replace($"RT @{tweet.RetweetedTweet.CreatedBy.ScreenName}:", $"[{{RT}} @{tweet.RetweetedTweet.CreatedBy.ScreenName}]{Environment.NewLine}");
|
|
||||||
else
|
|
||||||
message = message.Replace("RT", "[{{RT}}]");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand URLs
|
private string GetMediaType(string mediaType, string mediaUrl)
|
||||||
foreach (var url in tweet.Urls.OrderByDescending(x => x.URL.Length))
|
|
||||||
message = message.Replace(url.URL, url.ExpandedURL);
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExtractedMedia[] ExtractMedia(ITweet tweet)
|
|
||||||
{
|
|
||||||
var media = tweet.Media;
|
|
||||||
if (tweet.IsRetweet && tweet.RetweetedTweet != null)
|
|
||||||
media = tweet.RetweetedTweet.Media;
|
|
||||||
|
|
||||||
var result = new List<ExtractedMedia>();
|
|
||||||
foreach (var m in media)
|
|
||||||
{
|
|
||||||
var mediaUrl = GetMediaUrl(m);
|
|
||||||
var mediaType = GetMediaType(m.MediaType, mediaUrl);
|
|
||||||
if (mediaType == null) continue;
|
|
||||||
|
|
||||||
var att = new ExtractedMedia
|
|
||||||
{
|
|
||||||
MediaType = mediaType,
|
|
||||||
Url = mediaUrl
|
|
||||||
};
|
|
||||||
result.Add(att);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetMediaUrl(IMediaEntity media)
|
|
||||||
{
|
|
||||||
switch (media.MediaType)
|
|
||||||
{
|
|
||||||
case "photo": return media.MediaURLHttps;
|
|
||||||
case "animated_gif": return media.VideoDetails.Variants[0].URL;
|
|
||||||
case "video": return media.VideoDetails.Variants.OrderByDescending(x => x.Bitrate).First().URL;
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetMediaType(string mediaType, string mediaUrl)
|
|
||||||
{
|
{
|
||||||
switch (mediaType)
|
switch (mediaType)
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,13 +3,16 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BirdsiteLive.Common.Settings;
|
using BirdsiteLive.Common.Settings;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Tweetinvi;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace BirdsiteLive.Twitter.Tools
|
namespace BirdsiteLive.Twitter.Tools
|
||||||
{
|
{
|
||||||
public interface ITwitterAuthenticationInitializer
|
public interface ITwitterAuthenticationInitializer
|
||||||
{
|
{
|
||||||
void EnsureAuthenticationIsInitialized();
|
String Token { get; }
|
||||||
|
Task EnsureAuthenticationIsInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TwitterAuthenticationInitializer : ITwitterAuthenticationInitializer
|
public class TwitterAuthenticationInitializer : ITwitterAuthenticationInitializer
|
||||||
|
@ -17,7 +20,11 @@ namespace BirdsiteLive.Twitter.Tools
|
||||||
private readonly TwitterSettings _settings;
|
private readonly TwitterSettings _settings;
|
||||||
private readonly ILogger<TwitterAuthenticationInitializer> _logger;
|
private readonly ILogger<TwitterAuthenticationInitializer> _logger;
|
||||||
private static bool _initialized;
|
private static bool _initialized;
|
||||||
private readonly SemaphoreSlim _semaphoregate = new SemaphoreSlim(1);
|
private readonly HttpClient _httpClient = new HttpClient();
|
||||||
|
private String _token;
|
||||||
|
public String Token {
|
||||||
|
get { return _token; }
|
||||||
|
}
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public TwitterAuthenticationInitializer(TwitterSettings settings, ILogger<TwitterAuthenticationInitializer> logger)
|
public TwitterAuthenticationInitializer(TwitterSettings settings, ILogger<TwitterAuthenticationInitializer> logger)
|
||||||
|
@ -27,36 +34,42 @@ namespace BirdsiteLive.Twitter.Tools
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public void EnsureAuthenticationIsInitialized()
|
public async Task EnsureAuthenticationIsInitialized()
|
||||||
{
|
{
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
_semaphoregate.Wait();
|
|
||||||
|
|
||||||
try
|
await InitTwitterCredentials();
|
||||||
{
|
|
||||||
if (_initialized) return;
|
|
||||||
InitTwitterCredentials();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_semaphoregate.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitTwitterCredentials()
|
private async Task InitTwitterCredentials()
|
||||||
{
|
{
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Auth.SetApplicationOnlyCredentials(_settings.ConsumerKey, _settings.ConsumerSecret, true);
|
|
||||||
|
using (var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.twitter.com/oauth2/token"))
|
||||||
|
{
|
||||||
|
var base64authorization = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(_settings.ConsumerKey + ":" + _settings.ConsumerSecret));
|
||||||
|
request.Headers.TryAddWithoutValidation("Authorization", $"Basic {base64authorization}");
|
||||||
|
|
||||||
|
request.Content = new StringContent("grant_type=client_credentials");
|
||||||
|
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
var httpResponse = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
|
var c = await httpResponse.Content.ReadAsStringAsync();
|
||||||
|
httpResponse.EnsureSuccessStatusCode();
|
||||||
|
var doc = JsonDocument.Parse(c);
|
||||||
|
_token = doc.RootElement.GetProperty("access_token").GetString();
|
||||||
|
}
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, "Twitter Authentication Failed");
|
_logger.LogError(e, "Twitter Authentication Failed");
|
||||||
Thread.Sleep(250);
|
await Task.Delay(3600*1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using BirdsiteLive.Common.Settings;
|
using BirdsiteLive.Common.Settings;
|
||||||
using BirdsiteLive.Statistics.Domain;
|
using BirdsiteLive.Statistics.Domain;
|
||||||
using BirdsiteLive.Twitter.Extractors;
|
using BirdsiteLive.Twitter.Extractors;
|
||||||
using BirdsiteLive.Twitter.Models;
|
using BirdsiteLive.Twitter.Models;
|
||||||
using BirdsiteLive.Twitter.Tools;
|
using BirdsiteLive.Twitter.Tools;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Tweetinvi;
|
|
||||||
using Tweetinvi.Models;
|
|
||||||
using Tweetinvi.Parameters;
|
|
||||||
|
|
||||||
namespace BirdsiteLive.Twitter
|
namespace BirdsiteLive.Twitter
|
||||||
{
|
{
|
||||||
|
@ -26,6 +26,7 @@ namespace BirdsiteLive.Twitter
|
||||||
private readonly ITwitterStatisticsHandler _statisticsHandler;
|
private readonly ITwitterStatisticsHandler _statisticsHandler;
|
||||||
private readonly ITwitterUserService _twitterUserService;
|
private readonly ITwitterUserService _twitterUserService;
|
||||||
private readonly ILogger<TwitterTweetsService> _logger;
|
private readonly ILogger<TwitterTweetsService> _logger;
|
||||||
|
private HttpClient _httpClient = new HttpClient();
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public TwitterTweetsService(ITwitterAuthenticationInitializer twitterAuthenticationInitializer, ITweetExtractor tweetExtractor, ITwitterStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService, ILogger<TwitterTweetsService> logger)
|
public TwitterTweetsService(ITwitterAuthenticationInitializer twitterAuthenticationInitializer, ITweetExtractor tweetExtractor, ITwitterStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService, ILogger<TwitterTweetsService> logger)
|
||||||
|
@ -38,15 +39,27 @@ namespace BirdsiteLive.Twitter
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
public ExtractedTweet GetTweet(long statusId)
|
public ExtractedTweet GetTweet(long statusId)
|
||||||
|
{
|
||||||
|
return GetTweetAsync(statusId).Result;
|
||||||
|
}
|
||||||
|
public async Task<ExtractedTweet> GetTweetAsync(long statusId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
await _twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
||||||
ExceptionHandler.SwallowWebExceptions = false;
|
JsonDocument tweet;
|
||||||
TweetinviConfig.CurrentThreadSettings.TweetMode = TweetMode.Extended;
|
using (var request = new HttpRequestMessage(new HttpMethod("GET"), "https://api.twitter.com/2/tweets?ids=" + statusId))
|
||||||
|
{
|
||||||
|
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + _twitterAuthenticationInitializer.Token);
|
||||||
|
|
||||||
|
var httpResponse = await _httpClient.SendAsync(request);
|
||||||
|
httpResponse.EnsureSuccessStatusCode();
|
||||||
|
var c = await httpResponse.Content.ReadAsStringAsync();
|
||||||
|
tweet = JsonDocument.Parse(c);
|
||||||
|
}
|
||||||
|
|
||||||
var tweet = Tweet.GetTweet(statusId);
|
|
||||||
_statisticsHandler.CalledTweetApi();
|
_statisticsHandler.CalledTweetApi();
|
||||||
if (tweet == null) return null; //TODO: test this
|
if (tweet == null) return null; //TODO: test this
|
||||||
return _tweetExtractor.Extract(tweet);
|
return _tweetExtractor.Extract(tweet);
|
||||||
|
@ -60,34 +73,41 @@ namespace BirdsiteLive.Twitter
|
||||||
|
|
||||||
public ExtractedTweet[] GetTimeline(string username, int nberTweets, long fromTweetId = -1)
|
public ExtractedTweet[] GetTimeline(string username, int nberTweets, long fromTweetId = -1)
|
||||||
{
|
{
|
||||||
var tweets = new List<ITweet>();
|
return GetTimelineAsync(username, nberTweets, fromTweetId).Result;
|
||||||
|
}
|
||||||
|
public async Task<ExtractedTweet[]> GetTimelineAsync(string username, int nberTweets, long fromTweetId = -1)
|
||||||
|
{
|
||||||
|
|
||||||
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
await _twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
||||||
ExceptionHandler.SwallowWebExceptions = false;
|
|
||||||
TweetinviConfig.CurrentThreadSettings.TweetMode = TweetMode.Extended;
|
|
||||||
|
|
||||||
var user = _twitterUserService.GetUser(username);
|
var user = _twitterUserService.GetUser(username);
|
||||||
if (user == null || user.Protected) return new ExtractedTweet[0];
|
if (user == null || user.Protected) return new ExtractedTweet[0];
|
||||||
|
|
||||||
if (fromTweetId == -1)
|
JsonDocument tweets;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var timeline = Timeline.GetUserTimeline(user.Id, nberTweets);
|
using (var request = new HttpRequestMessage(new HttpMethod("GET"), "https://api.twitter.com/2/users/" + user + "/tweets?expansions=in_reply_to_user_id,attachments.media_keys,entities.mentions.username,referenced_tweets.id.author_id&tweet.fields=id"))
|
||||||
_statisticsHandler.CalledTimelineApi();
|
|
||||||
if (timeline != null) tweets.AddRange(timeline);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
var timelineRequestParameters = new UserTimelineParameters
|
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + _twitterAuthenticationInitializer.Token);
|
||||||
{
|
|
||||||
SinceId = fromTweetId,
|
var httpResponse = await _httpClient.SendAsync(request);
|
||||||
MaximumNumberOfTweetsToRetrieve = nberTweets
|
httpResponse.EnsureSuccessStatusCode();
|
||||||
};
|
var c = await httpResponse.Content.ReadAsStringAsync();
|
||||||
var timeline = Timeline.GetUserTimeline(user.Id, timelineRequestParameters);
|
tweets = JsonDocument.Parse(c);
|
||||||
_statisticsHandler.CalledTimelineApi();
|
|
||||||
if (timeline != null) tweets.AddRange(timeline);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tweets.Select(_tweetExtractor.Extract).ToArray();
|
_statisticsHandler.CalledTweetApi();
|
||||||
|
if (tweets == null) return null; //TODO: test this
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Error retrieving timeline ", username);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Array.Empty<ExtractedTweet>();
|
||||||
|
//return tweets.RootElement.GetProperty("data").Select(_tweetExtractor.Extract).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using BirdsiteLive.Common.Settings;
|
using BirdsiteLive.Common.Settings;
|
||||||
using BirdsiteLive.Statistics.Domain;
|
using BirdsiteLive.Statistics.Domain;
|
||||||
using BirdsiteLive.Twitter.Models;
|
using BirdsiteLive.Twitter.Models;
|
||||||
using BirdsiteLive.Twitter.Tools;
|
using BirdsiteLive.Twitter.Tools;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Tweetinvi;
|
|
||||||
using Tweetinvi.Exceptions;
|
|
||||||
using Tweetinvi.Models;
|
|
||||||
|
|
||||||
namespace BirdsiteLive.Twitter
|
namespace BirdsiteLive.Twitter
|
||||||
{
|
{
|
||||||
|
@ -22,6 +22,7 @@ namespace BirdsiteLive.Twitter
|
||||||
private readonly ITwitterAuthenticationInitializer _twitterAuthenticationInitializer;
|
private readonly ITwitterAuthenticationInitializer _twitterAuthenticationInitializer;
|
||||||
private readonly ITwitterStatisticsHandler _statisticsHandler;
|
private readonly ITwitterStatisticsHandler _statisticsHandler;
|
||||||
private readonly ILogger<TwitterUserService> _logger;
|
private readonly ILogger<TwitterUserService> _logger;
|
||||||
|
private HttpClient _httpClient = new HttpClient();
|
||||||
|
|
||||||
#region Ctor
|
#region Ctor
|
||||||
public TwitterUserService(ITwitterAuthenticationInitializer twitterAuthenticationInitializer, ITwitterStatisticsHandler statisticsHandler, ILogger<TwitterUserService> logger)
|
public TwitterUserService(ITwitterAuthenticationInitializer twitterAuthenticationInitializer, ITwitterStatisticsHandler statisticsHandler, ILogger<TwitterUserService> logger)
|
||||||
|
@ -33,38 +34,50 @@ namespace BirdsiteLive.Twitter
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public TwitterUser GetUser(string username)
|
public TwitterUser GetUser(string username)
|
||||||
|
{
|
||||||
|
return GetUserAsync(username).Result;
|
||||||
|
}
|
||||||
|
public async Task<TwitterUser> GetUserAsync(string username)
|
||||||
{
|
{
|
||||||
//Check if API is saturated
|
//Check if API is saturated
|
||||||
if (IsUserApiRateLimited()) throw new RateLimitExceededException();
|
if (IsUserApiRateLimited()) throw new RateLimitExceededException();
|
||||||
|
|
||||||
//Proceed to account retrieval
|
//Proceed to account retrieval
|
||||||
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
await _twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
||||||
ExceptionHandler.SwallowWebExceptions = false;
|
|
||||||
RateLimit.RateLimitTrackerMode = RateLimitTrackerMode.TrackOnly;
|
|
||||||
|
|
||||||
IUser user;
|
JsonDocument res;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
user = User.GetUserFromScreenName(username);
|
using (var request = new HttpRequestMessage(new HttpMethod("GET"), "https://api.twitter.com/2/users/by/username/"+ username + "?user.fields=name,username,protected,profile_image_url,url,description"))
|
||||||
}
|
|
||||||
catch (TwitterException e)
|
|
||||||
{
|
{
|
||||||
if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User has been suspended".ToLowerInvariant())))
|
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + _twitterAuthenticationInitializer.Token);
|
||||||
{
|
|
||||||
throw new UserHasBeenSuspendedException();
|
var httpResponse = await _httpClient.SendAsync(request);
|
||||||
|
httpResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var c = await httpResponse.Content.ReadAsStringAsync();
|
||||||
|
res = JsonDocument.Parse(c);
|
||||||
}
|
}
|
||||||
else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User not found".ToLowerInvariant())))
|
|
||||||
{
|
|
||||||
throw new UserNotFoundException();
|
|
||||||
}
|
}
|
||||||
else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("Rate limit exceeded".ToLowerInvariant())))
|
catch (HttpRequestException e)
|
||||||
{
|
|
||||||
throw new RateLimitExceededException();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
//if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User has been suspended".ToLowerInvariant())))
|
||||||
|
//{
|
||||||
|
// throw new UserHasBeenSuspendedException();
|
||||||
|
//}
|
||||||
|
//else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User not found".ToLowerInvariant())))
|
||||||
|
//{
|
||||||
|
// throw new UserNotFoundException();
|
||||||
|
//}
|
||||||
|
//else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("Rate limit exceeded".ToLowerInvariant())))
|
||||||
|
//{
|
||||||
|
// throw new RateLimitExceededException();
|
||||||
|
//}
|
||||||
|
//else
|
||||||
|
//{
|
||||||
|
// throw;
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -77,49 +90,50 @@ namespace BirdsiteLive.Twitter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand URLs
|
// Expand URLs
|
||||||
var description = user.Description;
|
//var description = user.Description;
|
||||||
foreach (var descriptionUrl in user.Entities?.Description?.Urls?.OrderByDescending(x => x.URL.Length))
|
//foreach (var descriptionUrl in user.Entities?.Description?.Urls?.OrderByDescending(x => x.URL.Length))
|
||||||
description = description.Replace(descriptionUrl.URL, descriptionUrl.ExpandedURL);
|
// description = description.Replace(descriptionUrl.URL, descriptionUrl.ExpandedURL);
|
||||||
|
|
||||||
return new TwitterUser
|
return new TwitterUser
|
||||||
{
|
{
|
||||||
Id = user.Id,
|
Id = long.Parse(res.RootElement.GetProperty("data").GetProperty("id").GetString()),
|
||||||
Acct = username,
|
Acct = res.RootElement.GetProperty("data").GetProperty("username").GetString(),
|
||||||
Name = user.Name,
|
Name = res.RootElement.GetProperty("data").GetProperty("name").GetString(),
|
||||||
Description = description,
|
Description = res.RootElement.GetProperty("data").GetProperty("description").GetString(),
|
||||||
Url = $"https://twitter.com/{username}",
|
Url = res.RootElement.GetProperty("data").GetProperty("url").GetString(),
|
||||||
ProfileImageUrl = user.ProfileImageUrlFullSize.Replace("http://", "https://"),
|
ProfileImageUrl = res.RootElement.GetProperty("data").GetProperty("profile_image_url").GetString(),
|
||||||
ProfileBackgroundImageUrl = user.ProfileBackgroundImageUrlHttps,
|
ProfileBackgroundImageUrl = res.RootElement.GetProperty("data").GetProperty("profile_image_url").GetString(), //for now
|
||||||
ProfileBannerURL = user.ProfileBannerURL,
|
ProfileBannerURL = res.RootElement.GetProperty("data").GetProperty("profile_image_url").GetString(), //for now
|
||||||
Protected = user.Protected
|
Protected = res.RootElement.GetProperty("data").GetProperty("protected").GetBoolean(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsUserApiRateLimited()
|
public bool IsUserApiRateLimited()
|
||||||
{
|
{
|
||||||
// Retrieve limit from tooling
|
// Retrieve limit from tooling
|
||||||
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
//_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
|
||||||
ExceptionHandler.SwallowWebExceptions = false;
|
//ExceptionHandler.SwallowWebExceptions = false;
|
||||||
RateLimit.RateLimitTrackerMode = RateLimitTrackerMode.TrackOnly;
|
//RateLimit.RateLimitTrackerMode = RateLimitTrackerMode.TrackOnly;
|
||||||
|
|
||||||
try
|
//try
|
||||||
{
|
//{
|
||||||
var queryRateLimits = RateLimit.GetQueryRateLimit("https://api.twitter.com/1.1/users/show.json?screen_name=mastodon");
|
// var queryRateLimits = RateLimit.GetQueryRateLimit("https://api.twitter.com/1.1/users/show.json?screen_name=mastodon");
|
||||||
|
|
||||||
if (queryRateLimits != null)
|
// if (queryRateLimits != null)
|
||||||
{
|
// {
|
||||||
return queryRateLimits.Remaining <= 0;
|
// return queryRateLimits.Remaining <= 0;
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
catch (Exception e)
|
//catch (Exception e)
|
||||||
{
|
//{
|
||||||
_logger.LogError(e, "Error retrieving rate limits");
|
// _logger.LogError(e, "Error retrieving rate limits");
|
||||||
}
|
//}
|
||||||
|
|
||||||
// Fallback
|
//// Fallback
|
||||||
var currentCalls = _statisticsHandler.GetCurrentUserCalls();
|
//var currentCalls = _statisticsHandler.GetCurrentUserCalls();
|
||||||
var maxCalls = _statisticsHandler.GetStatistics().UserCallsMax;
|
//var maxCalls = _statisticsHandler.GetStatistics().UserCallsMax;
|
||||||
return currentCalls >= maxCalls;
|
//return currentCalls >= maxCalls;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
<UserSecretsId>d21486de-a812-47eb-a419-05682bb68856</UserSecretsId>
|
<UserSecretsId>d21486de-a812-47eb-a419-05682bb68856</UserSecretsId>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<Version>0.20.0</Version>
|
<Version>0.20.0</Version>
|
||||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Runtime.InteropServices.WindowsRuntime;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
Loading…
Add table
Reference in a new issue