From a9475450871735f72ef70032347da9dafc79e6ad Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Wed, 1 Jul 2020 19:25:00 -0400 Subject: [PATCH 1/6] renaming to integrate view --- ...bugController.cs => DebugingController.cs} | 4 ++-- src/BirdsiteLive/Views/Debuging/Index.cshtml | 19 +++++++++++++++++++ src/BirdsiteLive/Views/Home/Index.cshtml | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) rename src/BirdsiteLive/Controllers/{DebugController.cs => DebugingController.cs} (95%) create mode 100644 src/BirdsiteLive/Views/Debuging/Index.cshtml diff --git a/src/BirdsiteLive/Controllers/DebugController.cs b/src/BirdsiteLive/Controllers/DebugingController.cs similarity index 95% rename from src/BirdsiteLive/Controllers/DebugController.cs rename to src/BirdsiteLive/Controllers/DebugingController.cs index f187d32..47196c7 100644 --- a/src/BirdsiteLive/Controllers/DebugController.cs +++ b/src/BirdsiteLive/Controllers/DebugingController.cs @@ -13,14 +13,14 @@ using Newtonsoft.Json; namespace BirdsiteLive.Controllers { - public class DebugController : Controller + public class DebugingController : Controller { private readonly InstanceSettings _instanceSettings; private readonly ICryptoService _cryptoService; private readonly IActivityPubService _activityPubService; #region Ctor - public DebugController(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService) + public DebugingController(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService) { _instanceSettings = instanceSettings; _cryptoService = cryptoService; diff --git a/src/BirdsiteLive/Views/Debuging/Index.cshtml b/src/BirdsiteLive/Views/Debuging/Index.cshtml new file mode 100644 index 0000000..04ea0bf --- /dev/null +++ b/src/BirdsiteLive/Views/Debuging/Index.cshtml @@ -0,0 +1,19 @@ + +@{ + ViewData["Title"] = "Index"; +} + +

Debug

+ +
+ + + +
+ + +
+ + + +
\ No newline at end of file diff --git a/src/BirdsiteLive/Views/Home/Index.cshtml b/src/BirdsiteLive/Views/Home/Index.cshtml index 6ae1367..4c62fc0 100644 --- a/src/BirdsiteLive/Views/Home/Index.cshtml +++ b/src/BirdsiteLive/Views/Home/Index.cshtml @@ -10,6 +10,6 @@ @if (HtmlHelperExtensions.IsDebug()) { - Debug + Debug } From 46c18de299d6fce14b28c57b654978b8444f6db4 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Wed, 1 Jul 2020 22:45:43 -0400 Subject: [PATCH 2/6] added tweet retrieval and display --- .../Converters/ContextArrayConverter.cs | 39 +++++++++++++++++++ src/BirdsiteLive.ActivityPub/Models/Actor.cs | 37 +----------------- src/BirdsiteLive.ActivityPub/Models/Note.cs | 6 +++ src/BirdsiteLive.Domain/UserService.cs | 37 +++++++++++++++++- src/BirdsiteLive.Twitter/TwitterService.cs | 9 +++++ .../Controllers/UsersController.cs | 29 +++++++++++++- .../Properties/launchSettings.json | 2 +- src/BirdsiteLive/Views/Users/Tweet.cshtml | 8 ++++ 8 files changed, 127 insertions(+), 40 deletions(-) create mode 100644 src/BirdsiteLive.ActivityPub/Converters/ContextArrayConverter.cs create mode 100644 src/BirdsiteLive/Views/Users/Tweet.cshtml diff --git a/src/BirdsiteLive.ActivityPub/Converters/ContextArrayConverter.cs b/src/BirdsiteLive.ActivityPub/Converters/ContextArrayConverter.cs new file mode 100644 index 0000000..21e139f --- /dev/null +++ b/src/BirdsiteLive.ActivityPub/Converters/ContextArrayConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace BirdsiteLive.ActivityPub.Converters +{ + public class ContextArrayConverter : JsonConverter + { + public override bool CanWrite { get { return false; } } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var result = new List(); + + var list = serializer.Deserialize>(reader); + foreach (var l in list) + { + if (l is string s) + result.Add(s); + else + { + var str = JsonConvert.SerializeObject(l); + result.Add(str); + } + } + + return result.ToArray(); + } + + public override bool CanConvert(Type objectType) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.ActivityPub/Models/Actor.cs b/src/BirdsiteLive.ActivityPub/Models/Actor.cs index cbebba1..59ea71f 100644 --- a/src/BirdsiteLive.ActivityPub/Models/Actor.cs +++ b/src/BirdsiteLive.ActivityPub/Models/Actor.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using BirdsiteLive.ActivityPub.Converters; using Newtonsoft.Json; -using JsonConverter = Newtonsoft.Json.JsonConverter; namespace BirdsiteLive.ActivityPub { @@ -22,37 +20,4 @@ namespace BirdsiteLive.ActivityPub public Image icon { get; set; } public Image image { get; set; } } - - public class ContextArrayConverter : JsonConverter - { - public override bool CanWrite { get { return false; } } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var result = new List(); - - var list = serializer.Deserialize>(reader); - foreach (var l in list) - { - if (l is string s) - result.Add(s); - else - { - var str = JsonConvert.SerializeObject(l); - result.Add(str); - } - } - - return result.ToArray(); - } - - public override bool CanConvert(Type objectType) - { - throw new NotImplementedException(); - } - } } diff --git a/src/BirdsiteLive.ActivityPub/Models/Note.cs b/src/BirdsiteLive.ActivityPub/Models/Note.cs index 0dc9c16..cc3d561 100644 --- a/src/BirdsiteLive.ActivityPub/Models/Note.cs +++ b/src/BirdsiteLive.ActivityPub/Models/Note.cs @@ -1,10 +1,16 @@ using System; using System.Collections.Generic; +using BirdsiteLive.ActivityPub.Converters; +using Newtonsoft.Json; namespace BirdsiteLive.ActivityPub { public class Note { + [JsonProperty("@context")] + [JsonConverter(typeof(ContextArrayConverter))] + public string[] context { get; set; } = new[] { "https://www.w3.org/ns/activitystreams" }; + public string id { get; set; } public string type { get; } = "Note"; public string summary { get; set; } diff --git a/src/BirdsiteLive.Domain/UserService.cs b/src/BirdsiteLive.Domain/UserService.cs index 5c569f5..d084e97 100644 --- a/src/BirdsiteLive.Domain/UserService.cs +++ b/src/BirdsiteLive.Domain/UserService.cs @@ -10,6 +10,7 @@ using BirdsiteLive.Common.Settings; using BirdsiteLive.Cryptography; using BirdsiteLive.Twitter.Models; using Tweetinvi.Core.Exceptions; +using Tweetinvi.Models; namespace BirdsiteLive.Domain { @@ -17,6 +18,7 @@ namespace BirdsiteLive.Domain { Actor GetUser(TwitterUser twitterUser); Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity); + Note GetStatus(TwitterUser user, ITweet tweet); } public class UserService : IUserService @@ -65,6 +67,39 @@ namespace BirdsiteLive.Domain return user; } + public Note GetStatus(TwitterUser user, ITweet tweet) + { + var actor = GetUser(user); + + var actorUrl = $"{_host}/users/{user.Acct}"; + var noteId = $"{_host}/users/{user.Acct}/statuses/{tweet.Id}"; + var noteUrl = $"{_host}/@{user.Acct}/{tweet.Id}"; + + var to = $"{actor}/followers"; + var apPublic = "https://www.w3.org/ns/activitystreams#Public"; + + var note = new Note + { + id = $"{noteId}/activity", + + published = tweet.CreatedAt.ToString("s") + "Z", + url = noteUrl, + attributedTo = actorUrl, + + //to = new [] {to}, + //cc = new [] { apPublic }, + + to = new[] { apPublic }, + cc = new[] { to }, + + sensitive = false, + content = $"

{tweet.Text}

", + attachment = new string[0], + tag = new string[0] + }; + return note; + } + public async Task FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary requestHeaders, ActivityFollow activity) { // Validate @@ -91,7 +126,7 @@ namespace BirdsiteLive.Domain var result = await _activityPubService.PostDataAsync(acceptFollow, targetHost, activity.apObject); return result == HttpStatusCode.Accepted; } - + private async Task ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary requestHeaders) { var signatures = rawSig.Split(','); diff --git a/src/BirdsiteLive.Twitter/TwitterService.cs b/src/BirdsiteLive.Twitter/TwitterService.cs index 3317496..3bec34a 100644 --- a/src/BirdsiteLive.Twitter/TwitterService.cs +++ b/src/BirdsiteLive.Twitter/TwitterService.cs @@ -3,12 +3,14 @@ using System.Threading.Tasks; using BirdsiteLive.Common.Settings; using BirdsiteLive.Twitter.Models; using Tweetinvi; +using Tweetinvi.Models; namespace BirdsiteLive.Twitter { public interface ITwitterService { TwitterUser GetUser(string username); + ITweet GetTweet(long statusId); } public class TwitterService : ITwitterService @@ -40,5 +42,12 @@ namespace BirdsiteLive.Twitter ProfileBannerURL = user.ProfileBannerURL }; } + + public ITweet GetTweet(long statusId) + { + Auth.SetApplicationOnlyCredentials(_settings.ConsumerKey, _settings.ConsumerSecret, true); + var tweet = Tweet.GetTweet(statusId); + return tweet; + } } } diff --git a/src/BirdsiteLive/Controllers/UsersController.cs b/src/BirdsiteLive/Controllers/UsersController.cs index 05a6aac..b431aa0 100644 --- a/src/BirdsiteLive/Controllers/UsersController.cs +++ b/src/BirdsiteLive/Controllers/UsersController.cs @@ -33,7 +33,7 @@ namespace BirdsiteLive.Controllers { var user = _twitterService.GetUser(id); if (user == null) return NotFound(); - + var r = Request.Headers["Accept"].First(); if (r.Contains("application/activity+json")) { @@ -45,6 +45,31 @@ namespace BirdsiteLive.Controllers return View(user); } + [Route("/@{id}/{statusId}")] + [Route("/users/{id}/statuses/{statusId}")] + public IActionResult Tweet(string id, string statusId) + { + var r = Request.Headers["Accept"].First(); + if (r.Contains("application/activity+json")) + { + if (!long.TryParse(statusId, out var parsedStatusId)) + return NotFound(); + + var tweet = _twitterService.GetTweet(parsedStatusId); + if(tweet == null) + return NotFound(); + + var user = _twitterService.GetUser(id); + if (user == null) return NotFound(); + + var status = _userService.GetStatus(user, tweet); + var jsonApUser = JsonConvert.SerializeObject(status); + return Content(jsonApUser, "application/json"); + } + + return View("Tweet", statusId); + } + [Route("/users/{id}/inbox")] [HttpPost] public async Task Inbox() @@ -58,7 +83,7 @@ namespace BirdsiteLive.Controllers switch (activity.type) { - case "Follow": + case "Follow": var succeeded = await _userService.FollowRequestedAsync(r.Headers["Signature"].First(), r.Method, r.Path, r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityFollow); if (succeeded) return Accepted(); else return Unauthorized(); diff --git a/src/BirdsiteLive/Properties/launchSettings.json b/src/BirdsiteLive/Properties/launchSettings.json index c94543d..4dcf5f1 100644 --- a/src/BirdsiteLive/Properties/launchSettings.json +++ b/src/BirdsiteLive/Properties/launchSettings.json @@ -21,7 +21,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + "applicationUrl": "http://localhost:5000" }, "Docker": { "commandName": "Docker", diff --git a/src/BirdsiteLive/Views/Users/Tweet.cshtml b/src/BirdsiteLive/Views/Users/Tweet.cshtml new file mode 100644 index 0000000..a1aa7fe --- /dev/null +++ b/src/BirdsiteLive/Views/Users/Tweet.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Tweet"; +} + +
+ + +
\ No newline at end of file From bdcd5493d97e45290b8ecd773c5397aab34820c6 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Fri, 3 Jul 2020 00:39:06 -0400 Subject: [PATCH 3/6] fix content type --- src/BirdsiteLive/Controllers/UsersController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BirdsiteLive/Controllers/UsersController.cs b/src/BirdsiteLive/Controllers/UsersController.cs index b431aa0..38f87ee 100644 --- a/src/BirdsiteLive/Controllers/UsersController.cs +++ b/src/BirdsiteLive/Controllers/UsersController.cs @@ -39,7 +39,7 @@ namespace BirdsiteLive.Controllers { var apUser = _userService.GetUser(user); var jsonApUser = JsonConvert.SerializeObject(apUser); - return Content(jsonApUser, "application/json"); + return Content(jsonApUser, "application/activity+json; charset=utf-8"); } return View(user); @@ -64,7 +64,7 @@ namespace BirdsiteLive.Controllers var status = _userService.GetStatus(user, tweet); var jsonApUser = JsonConvert.SerializeObject(status); - return Content(jsonApUser, "application/json"); + return Content(jsonApUser, "application/activity+json; charset=utf-8"); } return View("Tweet", statusId); From b34385fd224c666e3c00846d83ffbb08521d776d Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Sun, 5 Jul 2020 20:22:34 -0400 Subject: [PATCH 4/6] init dataAccessLayers --- .github/workflows/dotnet-core.yml | 2 + src/BirdsiteLive.sln | 29 +++- .../BirdsiteLive.DAL.Postgres.csproj | 16 ++ .../DataAccessLayers/Base/PostgresBase.cs | 25 +++ .../DbInitializerPostgresDal.cs | 150 ++++++++++++++++++ .../DataAccessLayers/FollowersPostgresDal.cs | 9 ++ .../TwitterUserPostgresDal.cs | 9 ++ .../Settings/PostgresSettings.cs | 12 ++ .../Tools/PostgresTools.cs | 36 +++++ .../BirdsiteLive.DAL/BirdsiteLive.DAL.csproj | 7 + .../Contracts/IDbInitializerDal.cs | 15 ++ .../Contracts/IFollowersDal.cs | 7 + .../Contracts/ITwitterUserDal.cs | 7 + .../BirdsiteLive.DAL/Models/CachedTweet.cs | 10 ++ .../BirdsiteLive.DAL/Models/Follower.cs | 13 ++ .../Models/SyncTwitterUser.cs | 11 ++ .../BirdsiteLive.DAL/Tools/RandomGenerator.cs | 18 +++ .../BirdsiteLive.DAL.Postgres.Tests.csproj | 20 +++ .../DbInitializerPostgresDalTests.cs | 68 ++++++++ 19 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL.Postgres/BirdsiteLive.DAL.Postgres.csproj create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/Base/PostgresBase.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/DbInitializerPostgresDal.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/TwitterUserPostgresDal.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL.Postgres/Settings/PostgresSettings.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL.Postgres/Tools/PostgresTools.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL/BirdsiteLive.DAL.csproj create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IDbInitializerDal.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IFollowersDal.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL/Contracts/ITwitterUserDal.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL/Models/CachedTweet.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL/Models/Follower.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL/Models/SyncTwitterUser.cs create mode 100644 src/DataAccessLayers/BirdsiteLive.DAL/Tools/RandomGenerator.cs create mode 100644 src/Tests/BirdsiteLive.DAL.Postgres.Tests/BirdsiteLive.DAL.Postgres.Tests.csproj create mode 100644 src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/DbInitializerPostgresDalTests.cs diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index c6b7818..645b026 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -15,6 +15,8 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Launch Db for testing + run: docker run --name postgres -e POSTGRES_DB=mytestdb -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432:5432 postgres - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: diff --git a/src/BirdsiteLive.sln b/src/BirdsiteLive.sln index 8e1b53b..d600aa6 100644 --- a/src/BirdsiteLive.sln +++ b/src/BirdsiteLive.sln @@ -17,11 +17,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A32D3458 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Cryptography.Tests", "Tests\BirdsiteLive.Cryptography.Tests\BirdsiteLive.Cryptography.Tests.csproj", "{155D46A4-2D05-47F2-8FFC-0B7C412A7652}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.Domain", "BirdsiteLive.Domain\BirdsiteLive.Domain.csproj", "{D48450EE-D8BD-4228-9864-043AC88F7EE0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.Domain", "BirdsiteLive.Domain\BirdsiteLive.Domain.csproj", "{D48450EE-D8BD-4228-9864-043AC88F7EE0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.ActivityPub", "BirdsiteLive.ActivityPub\BirdsiteLive.ActivityPub.csproj", "{7463E1E2-9736-4A46-8507-010BDD8ECFBB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.ActivityPub", "BirdsiteLive.ActivityPub\BirdsiteLive.ActivityPub.csproj", "{7463E1E2-9736-4A46-8507-010BDD8ECFBB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.ActivityPub.Tests", "Tests\BirdsiteLive.ActivityPub.Tests\BirdsiteLive.ActivityPub.Tests.csproj", "{1D713961-9926-41FF-8D6A-8A4B8D548484}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BirdsiteLive.ActivityPub.Tests", "Tests\BirdsiteLive.ActivityPub.Tests\BirdsiteLive.ActivityPub.Tests.csproj", "{1D713961-9926-41FF-8D6A-8A4B8D548484}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataAccessLayers", "DataAccessLayers", "{CFAB3509-3931-42DB-AC97-4F91FC2D849C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.DAL", "DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj", "{47058CAB-DC43-4DD1-8F68-D3D625332905}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.DAL.Postgres", "DataAccessLayers\BirdsiteLive.DAL.Postgres\BirdsiteLive.DAL.Postgres.csproj", "{87E46519-BBF2-437C-8A5B-CF6CDE7CDAA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.DAL.Postgres.Tests", "Tests\BirdsiteLive.DAL.Postgres.Tests\BirdsiteLive.DAL.Postgres.Tests.csproj", "{CD9489BF-69C8-4705-8774-81C45F4F8FE1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -61,6 +69,18 @@ Global {1D713961-9926-41FF-8D6A-8A4B8D548484}.Debug|Any CPU.Build.0 = Debug|Any CPU {1D713961-9926-41FF-8D6A-8A4B8D548484}.Release|Any CPU.ActiveCfg = Release|Any CPU {1D713961-9926-41FF-8D6A-8A4B8D548484}.Release|Any CPU.Build.0 = Release|Any CPU + {47058CAB-DC43-4DD1-8F68-D3D625332905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47058CAB-DC43-4DD1-8F68-D3D625332905}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47058CAB-DC43-4DD1-8F68-D3D625332905}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47058CAB-DC43-4DD1-8F68-D3D625332905}.Release|Any CPU.Build.0 = Release|Any CPU + {87E46519-BBF2-437C-8A5B-CF6CDE7CDAA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87E46519-BBF2-437C-8A5B-CF6CDE7CDAA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87E46519-BBF2-437C-8A5B-CF6CDE7CDAA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87E46519-BBF2-437C-8A5B-CF6CDE7CDAA6}.Release|Any CPU.Build.0 = Release|Any CPU + {CD9489BF-69C8-4705-8774-81C45F4F8FE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD9489BF-69C8-4705-8774-81C45F4F8FE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD9489BF-69C8-4705-8774-81C45F4F8FE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD9489BF-69C8-4705-8774-81C45F4F8FE1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -73,6 +93,9 @@ Global {D48450EE-D8BD-4228-9864-043AC88F7EE0} = {4FEAD6BC-3C8E-451A-8CA1-FF1AF47D26CC} {7463E1E2-9736-4A46-8507-010BDD8ECFBB} = {4FEAD6BC-3C8E-451A-8CA1-FF1AF47D26CC} {1D713961-9926-41FF-8D6A-8A4B8D548484} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94} + {47058CAB-DC43-4DD1-8F68-D3D625332905} = {CFAB3509-3931-42DB-AC97-4F91FC2D849C} + {87E46519-BBF2-437C-8A5B-CF6CDE7CDAA6} = {CFAB3509-3931-42DB-AC97-4F91FC2D849C} + {CD9489BF-69C8-4705-8774-81C45F4F8FE1} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {69E8DCAD-4C37-4010-858F-5F94E6FBABCE} diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/BirdsiteLive.DAL.Postgres.csproj b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/BirdsiteLive.DAL.Postgres.csproj new file mode 100644 index 0000000..690c878 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/BirdsiteLive.DAL.Postgres.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/Base/PostgresBase.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/Base/PostgresBase.cs new file mode 100644 index 0000000..1c59387 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/Base/PostgresBase.cs @@ -0,0 +1,25 @@ +using BirdsiteLive.DAL.Postgres.Settings; +using Npgsql; + +namespace BirdsiteLive.DAL.Postgres.DataAccessLayers.Base +{ + public class PostgresBase + { + protected readonly PostgresSettings _settings; + + #region Ctor + protected PostgresBase(PostgresSettings settings) + { + _settings = settings; + } + #endregion + + protected NpgsqlConnection Connection + { + get + { + return new NpgsqlConnection(_settings.ConnString); + } + } + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/DbInitializerPostgresDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/DbInitializerPostgresDal.cs new file mode 100644 index 0000000..cbe37d5 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/DbInitializerPostgresDal.cs @@ -0,0 +1,150 @@ +using System; +using System.Data; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using System.Xml; +using BirdsiteLive.DAL.Contracts; +using BirdsiteLive.DAL.Postgres.DataAccessLayers.Base; +using BirdsiteLive.DAL.Postgres.Settings; +using BirdsiteLive.DAL.Postgres.Tools; +using Dapper; +using Npgsql; + +namespace BirdsiteLive.DAL.Postgres.DataAccessLayers +{ + internal class DbVersion + { + public string Type { get; set; } + public int Major { get; set; } + public int Minor { get; set; } + } + + public class DbInitializerPostgresDal : PostgresBase, IDbInitializerDal + { + private readonly PostgresTools _tools; + private readonly Version _currentVersion = new Version(1,0); + private const string DbVersionType = "db-version"; + + #region Ctor + public DbInitializerPostgresDal(PostgresSettings settings, PostgresTools tools) : base(settings) + { + _tools = tools; + } + #endregion + + public async Task GetCurrentDbVersionAsync() + { + var query = $"SELECT * FROM {_settings.DbVersionTableName} WHERE type = @type"; + + try + { + using (var dbConnection = Connection) + { + dbConnection.Open(); + + var result = (await dbConnection.QueryAsync(query, new { type = DbVersionType })).FirstOrDefault(); + + if (result == default) + return null; + + return new Version(result.Major, result.Minor); + } + } + catch (PostgresException e) + { + if (e.Message.Contains("42P01")) + return null; + + throw; + } + } + + public Version GetMandatoryDbVersion() + { + return _currentVersion; + } + + public Tuple[] GetMigrationPatterns() + { + return new Tuple[0]; + } + + public Task MigrateDbAsync(Version from, Version to) + { + throw new NotImplementedException(); + } + + public async Task InitDbAsync() + { + // Create version table + var createVersion = $@"CREATE TABLE {_settings.DbVersionTableName} + ( + type VARCHAR(128) PRIMARY KEY, + + major SMALLINT NOT NULL, + minor SMALLINT NOT NULL + );"; + await _tools.ExecuteRequestAsync(createVersion); + + // Create Twitter User table + var createTwitter = $@"CREATE TABLE {_settings.TwitterUserTableName} + ( + id SERIAL PRIMARY KEY, + acct VARCHAR(20) UNIQUE, + + lastTweetPostedId BIGINT, + lastTweetSynchronizedForAllFollowersId BIGINT + );"; + await _tools.ExecuteRequestAsync(createTwitter); + + // Create Followers table + var createFollowers = $@"CREATE TABLE {_settings.FollowersTableName} + ( + id SERIAL PRIMARY KEY, + + followings INTEGER[], + followingsSyncStatus JSONB, + + acct VARCHAR(20) UNIQUE, + host VARCHAR(20) + );"; + await _tools.ExecuteRequestAsync(createFollowers); + + // Create Cached Tweet table + var createCachedTweets = $@"CREATE TABLE {_settings.CachedTweetsTableName} + ( + id BIGINT PRIMARY KEY, + twitterUserId INTEGER, + data JSONB + );"; + await _tools.ExecuteRequestAsync(createCachedTweets); + + // Insert version to db + using (var dbConnection = Connection) + { + dbConnection.Open(); + + await dbConnection.ExecuteAsync( + $"INSERT INTO {_settings.DbVersionTableName} (type,major,minor) VALUES(@type,@major,@minor)", + new { type = DbVersionType, major = _currentVersion.Major, minor = _currentVersion.Minor }); + } + } + + public async Task DeleteAllAsync() + { + var dropsRequests = new[] + { + $@"DROP TABLE {_settings.DbVersionTableName};", + $@"DROP TABLE {_settings.TwitterUserTableName};", + $@"DROP TABLE {_settings.FollowersTableName};", + $@"DROP TABLE {_settings.CachedTweetsTableName};" + }; + + foreach (var r in dropsRequests) + { + await _tools.ExecuteRequestAsync(r); + } + } + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs new file mode 100644 index 0000000..ace1997 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs @@ -0,0 +1,9 @@ +using BirdsiteLive.DAL.Contracts; + +namespace BirdsiteLive.DAL.Postgres.DataAccessLayers +{ + public class FollowersPostgresDal : IFollowersDal + { + + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/TwitterUserPostgresDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/TwitterUserPostgresDal.cs new file mode 100644 index 0000000..2058bf3 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/TwitterUserPostgresDal.cs @@ -0,0 +1,9 @@ +using BirdsiteLive.DAL.Contracts; + +namespace BirdsiteLive.DAL.Postgres.DataAccessLayers +{ + public class TwitterUserPostgresDal : ITwitterUserDal + { + + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/Settings/PostgresSettings.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/Settings/PostgresSettings.cs new file mode 100644 index 0000000..8037a42 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/Settings/PostgresSettings.cs @@ -0,0 +1,12 @@ +namespace BirdsiteLive.DAL.Postgres.Settings +{ + public class PostgresSettings + { + public string ConnString { get; set; } + + public string DbVersionTableName { get; set; } = "db-version"; + public string TwitterUserTableName { get; set; } = "twitter-users"; + public string FollowersTableName { get; set; } = "followers"; + public string CachedTweetsTableName { get; set; } = "cached-tweets"; + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/Tools/PostgresTools.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/Tools/PostgresTools.cs new file mode 100644 index 0000000..223b1ea --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/Tools/PostgresTools.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using BirdsiteLive.DAL.Postgres.Settings; +using Npgsql; + +namespace BirdsiteLive.DAL.Postgres.Tools +{ + public class PostgresTools + { + private readonly PostgresSettings _settings; + + #region Ctor + public PostgresTools(PostgresSettings settings) + { + _settings = settings; + } + #endregion + + public async Task ExecuteRequestAsync(string request) + { + try + { + using (var conn = new NpgsqlConnection(_settings.ConnString)) + using (var cmd = new NpgsqlCommand(request, conn)) + { + await conn.OpenAsync(); + await cmd.ExecuteNonQueryAsync(); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/BirdsiteLive.DAL.csproj b/src/DataAccessLayers/BirdsiteLive.DAL/BirdsiteLive.DAL.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL/BirdsiteLive.DAL.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IDbInitializerDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IDbInitializerDal.cs new file mode 100644 index 0000000..b786386 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IDbInitializerDal.cs @@ -0,0 +1,15 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace BirdsiteLive.DAL.Contracts +{ + public interface IDbInitializerDal + { + Task GetCurrentDbVersionAsync(); + Version GetMandatoryDbVersion(); + Tuple[] GetMigrationPatterns(); + Task MigrateDbAsync(Version from, Version to); + Task InitDbAsync(); + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IFollowersDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IFollowersDal.cs new file mode 100644 index 0000000..ab8392d --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/IFollowersDal.cs @@ -0,0 +1,7 @@ +namespace BirdsiteLive.DAL.Contracts +{ + public interface IFollowersDal + { + + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/ITwitterUserDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/ITwitterUserDal.cs new file mode 100644 index 0000000..195b49b --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL/Contracts/ITwitterUserDal.cs @@ -0,0 +1,7 @@ +namespace BirdsiteLive.DAL.Contracts +{ + public interface ITwitterUserDal + { + + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Models/CachedTweet.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Models/CachedTweet.cs new file mode 100644 index 0000000..d1472b6 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL/Models/CachedTweet.cs @@ -0,0 +1,10 @@ +namespace BirdsiteLive.DAL.Models +{ + public class CachedTweet + { + public long Id { get; set; } + public long TwitterUserId { get; set; } + + public string TweetData { get; set; } + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Models/Follower.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Models/Follower.cs new file mode 100644 index 0000000..a6c0903 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL/Models/Follower.cs @@ -0,0 +1,13 @@ +namespace BirdsiteLive.DAL.Models +{ + public class Follower + { + public int Id { get; set; } + public int FollowingAccountId { get; set; } + + public string Acct { get; set; } + public string Host { get; set; } + + public long LastTweetSynchronizedId { get; set; } + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Models/SyncTwitterUser.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Models/SyncTwitterUser.cs new file mode 100644 index 0000000..8061fc8 --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL/Models/SyncTwitterUser.cs @@ -0,0 +1,11 @@ +namespace BirdsiteLive.DAL.Models +{ + public class SyncTwitterUser + { + public int Id { get; set; } + public string Acct { get; set; } + + public long LastTweetPostedId { get; set; } + public long LastTweetSynchronizedForAllFollowersId { get; set; } + } +} \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Tools/RandomGenerator.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Tools/RandomGenerator.cs new file mode 100644 index 0000000..fabbb5e --- /dev/null +++ b/src/DataAccessLayers/BirdsiteLive.DAL/Tools/RandomGenerator.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; + +namespace BirdsiteLive.DAL.Tools +{ + public static class RandomGenerator + { + private static readonly Random Random = new Random(); + + public static string GetString(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[Random.Next(s.Length)]).ToArray()); + } + + } +} \ No newline at end of file diff --git a/src/Tests/BirdsiteLive.DAL.Postgres.Tests/BirdsiteLive.DAL.Postgres.Tests.csproj b/src/Tests/BirdsiteLive.DAL.Postgres.Tests/BirdsiteLive.DAL.Postgres.Tests.csproj new file mode 100644 index 0000000..da05ef2 --- /dev/null +++ b/src/Tests/BirdsiteLive.DAL.Postgres.Tests/BirdsiteLive.DAL.Postgres.Tests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + diff --git a/src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/DbInitializerPostgresDalTests.cs b/src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/DbInitializerPostgresDalTests.cs new file mode 100644 index 0000000..5ca5968 --- /dev/null +++ b/src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/DbInitializerPostgresDalTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using BirdsiteLive.DAL.Postgres.DataAccessLayers; +using BirdsiteLive.DAL.Postgres.Settings; +using BirdsiteLive.DAL.Postgres.Tools; +using BirdsiteLive.DAL.Tools; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers +{ + [TestClass] + public class DbInitializerPostgresDalTests + { + private readonly PostgresSettings _settings; + private readonly PostgresTools _tools; + + #region Ctor + public DbInitializerPostgresDalTests() + { + _settings = new PostgresSettings + { + ConnString = "Host=127.0.0.1;Username=postgres;Password=mysecretpassword;Database=mytestdb", + DbVersionTableName = "DbVersionTableName" + RandomGenerator.GetString(4), + CachedTweetsTableName = "CachedTweetsTableName" + RandomGenerator.GetString(4), + FollowersTableName = "FollowersTableName" + RandomGenerator.GetString(4), + TwitterUserTableName = "TwitterUserTableName" + RandomGenerator.GetString(4), + }; + _tools = new PostgresTools(_settings); + } + #endregion + + [TestCleanup] + public async Task CleanUp() + { + var dal = new DbInitializerPostgresDal(_settings, _tools); + try + { + await dal.DeleteAllAsync(); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + [TestMethod] + public async Task GetCurrentDbVersionAsync_UninitializedDb() + { + var dal = new DbInitializerPostgresDal(_settings, _tools); + + var current = await dal.GetCurrentDbVersionAsync(); + Assert.IsNull(current); + } + + [TestMethod] + public async Task InitDbAsync() + { + var dal = new DbInitializerPostgresDal(_settings, _tools); + + await dal.InitDbAsync(); + var current = await dal.GetCurrentDbVersionAsync(); + var mandatory = dal.GetMandatoryDbVersion(); + Assert.IsNotNull(current); + Assert.AreEqual(mandatory.Minor, current.Minor); + Assert.AreEqual(mandatory.Major, current.Major); + } + } +} \ No newline at end of file From 774c304ff7b9e9679e125cecb90e9231cca4070d Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Sun, 5 Jul 2020 20:29:36 -0400 Subject: [PATCH 5/6] revert workflow --- .github/workflows/dotnet-core.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index 645b026..c6b7818 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -15,8 +15,6 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Launch Db for testing - run: docker run --name postgres -e POSTGRES_DB=mytestdb -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432:5432 postgres - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: From f3c210daf0cdde69afef504801b73354c36f2be0 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Sun, 5 Jul 2020 20:30:26 -0400 Subject: [PATCH 6/6] added docker for test --- .github/workflows/dotnet-core.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index c6b7818..645b026 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -15,6 +15,8 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Launch Db for testing + run: docker run --name postgres -e POSTGRES_DB=mytestdb -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432:5432 postgres - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: