diff options
author | crupest <crupest@outlook.com> | 2020-08-11 17:59:44 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-08-11 17:59:44 +0800 |
commit | d447950255e39a00a16466d7fe6e6d42a7c91eea (patch) | |
tree | e5627e37430adff1dff17d0d27976798e1ed4e0b | |
parent | ef734ef7e819bb6aa038e435e8468b75caba20a4 (diff) | |
download | timeline-d447950255e39a00a16466d7fe6e6d42a7c91eea.tar.gz timeline-d447950255e39a00a16466d7fe6e6d42a7c91eea.tar.bz2 timeline-d447950255e39a00a16466d7fe6e6d42a7c91eea.zip |
Post list modified since now consider username change. And make all datetime utc.
-rw-r--r-- | Timeline.Tests/Helpers/TestClock.cs | 4 | ||||
-rw-r--r-- | Timeline.Tests/IntegratedTests/TimelineTest.cs | 4 | ||||
-rw-r--r-- | Timeline/Entities/DatabaseContext.cs | 2 | ||||
-rw-r--r-- | Timeline/Entities/UtcDateAnnotation.cs | 44 | ||||
-rw-r--r-- | Timeline/Helpers/DateTimeExtensions.cs | 14 | ||||
-rw-r--r-- | Timeline/Models/Converters/JsonDateTimeConverter.cs | 7 | ||||
-rw-r--r-- | Timeline/Models/Converters/MyDateTimeConverter.cs | 51 | ||||
-rw-r--r-- | Timeline/Services/Clock.cs | 2 | ||||
-rw-r--r-- | Timeline/Services/TimelineService.cs | 12 | ||||
-rw-r--r-- | Timeline/Services/UserTokenManager.cs | 3 | ||||
-rw-r--r-- | Timeline/Startup.cs | 3 |
11 files changed, 136 insertions, 10 deletions
diff --git a/Timeline.Tests/Helpers/TestClock.cs b/Timeline.Tests/Helpers/TestClock.cs index 0cbf236d..ed2d65a6 100644 --- a/Timeline.Tests/Helpers/TestClock.cs +++ b/Timeline.Tests/Helpers/TestClock.cs @@ -12,7 +12,7 @@ namespace Timeline.Tests.Helpers public DateTime GetCurrentTime()
{
- return _currentTime ?? DateTime.Now;
+ return _currentTime ?? DateTime.UtcNow;
}
public void SetCurrentTime(DateTime? mockTime)
@@ -22,7 +22,7 @@ namespace Timeline.Tests.Helpers public DateTime SetMockCurrentTime()
{
- var time = new DateTime(2000, 1, 1, 1, 1, 1);
+ var time = new DateTime(3000, 1, 1, 1, 1, 1, DateTimeKind.Utc);
_currentTime = time;
return time;
}
diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 49672f29..16b3c7e4 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -952,7 +952,7 @@ namespace Timeline.Tests.IntegratedTests .Which.Should().NotBeNull().And.BeEquivalentTo(createRes);
}
const string mockContent2 = "bbb";
- var mockTime2 = DateTime.Now.AddDays(-1);
+ var mockTime2 = DateTime.UtcNow.AddDays(-1);
TimelinePostInfo createRes2;
{
var res = await client.PostAsJsonAsync(generator(1, "posts"),
@@ -1009,7 +1009,7 @@ namespace Timeline.Tests.IntegratedTests .Which.Id;
}
- var now = DateTime.Now;
+ var now = DateTime.UtcNow;
var id0 = await CreatePost(now.AddDays(1));
var id1 = await CreatePost(now.AddDays(-1));
var id2 = await CreatePost(now);
diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index ba2566bd..ecadd703 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -19,6 +19,8 @@ namespace Timeline.Entities modelBuilder.Entity<UserEntity>().Property(e => e.LastModified).HasDefaultValueSql("datetime('now', 'utc')");
modelBuilder.Entity<DataEntity>().HasIndex(e => e.Tag).IsUnique();
modelBuilder.Entity<TimelineEntity>().Property(e => e.UniqueId).HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ modelBuilder.ApplyUtcDateTimeConverter();
}
public DbSet<UserEntity> Users { get; set; } = default!;
diff --git a/Timeline/Entities/UtcDateAnnotation.cs b/Timeline/Entities/UtcDateAnnotation.cs new file mode 100644 index 00000000..6600e701 --- /dev/null +++ b/Timeline/Entities/UtcDateAnnotation.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using System;
+
+namespace Timeline.Entities
+{
+ // Copied from https://github.com/dotnet/efcore/issues/4711#issuecomment-589842988
+ public static class UtcDateAnnotation
+ {
+ private const string IsUtcAnnotation = "IsUtc";
+ private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
+ new ValueConverter<DateTime, DateTime>(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
+
+ public static PropertyBuilder<TProperty> IsUtc<TProperty>(this PropertyBuilder<TProperty> builder, bool isUtc = true) =>
+ builder.HasAnnotation(IsUtcAnnotation, isUtc);
+
+ public static bool IsUtc(this IMutableProperty property) =>
+ ((bool?)property.FindAnnotation(IsUtcAnnotation)?.Value) ?? true;
+
+ /// <summary>
+ /// Make sure this is called after configuring all your entities.
+ /// </summary>
+ public static void ApplyUtcDateTimeConverter(this ModelBuilder builder)
+ {
+ foreach (var entityType in builder.Model.GetEntityTypes())
+ {
+ foreach (var property in entityType.GetProperties())
+ {
+ if (!property.IsUtc())
+ {
+ continue;
+ }
+
+ if (property.ClrType == typeof(DateTime))
+ {
+ property.SetValueConverter(UtcConverter);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Timeline/Helpers/DateTimeExtensions.cs b/Timeline/Helpers/DateTimeExtensions.cs new file mode 100644 index 00000000..374f3bc9 --- /dev/null +++ b/Timeline/Helpers/DateTimeExtensions.cs @@ -0,0 +1,14 @@ +using System;
+
+namespace Timeline.Helpers
+{
+ public static class DateTimeExtensions
+ {
+ public static DateTime MyToUtc(this DateTime dateTime)
+ {
+ if (dateTime.Kind == DateTimeKind.Utc) return dateTime;
+ if (dateTime.Kind == DateTimeKind.Local) return dateTime.ToUniversalTime();
+ return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
+ }
+ }
+}
diff --git a/Timeline/Models/Converters/JsonDateTimeConverter.cs b/Timeline/Models/Converters/JsonDateTimeConverter.cs index ef129a01..865b6251 100644 --- a/Timeline/Models/Converters/JsonDateTimeConverter.cs +++ b/Timeline/Models/Converters/JsonDateTimeConverter.cs @@ -3,7 +3,8 @@ using System.Diagnostics; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; - +using Timeline.Helpers;
+
namespace Timeline.Models.Converters { public class JsonDateTimeConverter : JsonConverter<DateTime> @@ -11,12 +12,12 @@ namespace Timeline.Models.Converters public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { Debug.Assert(typeToConvert == typeof(DateTime)); - return DateTime.Parse(reader.GetString(), CultureInfo.InvariantCulture); + return DateTime.Parse(reader.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToUniversalTime().ToString("s", CultureInfo.InvariantCulture) + "Z"); + writer.WriteStringValue(value.MyToUtc().ToString("s", CultureInfo.InvariantCulture) + "Z"); } } } diff --git a/Timeline/Models/Converters/MyDateTimeConverter.cs b/Timeline/Models/Converters/MyDateTimeConverter.cs new file mode 100644 index 00000000..f125cd5c --- /dev/null +++ b/Timeline/Models/Converters/MyDateTimeConverter.cs @@ -0,0 +1,51 @@ +using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Timeline.Models.Converters
+{
+ public class MyDateTimeConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
+ }
+
+ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
+ {
+ return base.CanConvertTo(context, destinationType);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ if (value is string text)
+ {
+ text = text.Trim();
+ if (text.Length == 0)
+ {
+ return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
+ }
+
+ return DateTime.Parse(text, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
+ }
+
+ return base.ConvertFrom(context, culture, value);
+ }
+
+ public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
+ {
+ if (destinationType == typeof(string) && value is DateTime)
+ {
+ DateTime dt = (DateTime)value;
+ if (dt == DateTime.MinValue)
+ {
+ return string.Empty;
+ }
+
+ return dt.ToString("s", CultureInfo.InvariantCulture) + "Z";
+ }
+
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+ }
+}
diff --git a/Timeline/Services/Clock.cs b/Timeline/Services/Clock.cs index 040f9304..4395edcd 100644 --- a/Timeline/Services/Clock.cs +++ b/Timeline/Services/Clock.cs @@ -23,7 +23,7 @@ namespace Timeline.Services public DateTime GetCurrentTime()
{
- return DateTime.Now;
+ return DateTime.UtcNow;
}
}
}
diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 283938fb..0070fe3e 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -565,11 +565,13 @@ namespace Timeline.Services public async Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false)
{
+ modifiedSince = modifiedSince?.MyToUtc();
+
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
var timelineId = await FindTimelineId(timelineName);
- var query = _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId);
+ IQueryable<TimelinePostEntity> query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId);
if (!includeDeleted)
{
@@ -578,9 +580,11 @@ namespace Timeline.Services if (modifiedSince.HasValue)
{
- query = query.Where(p => p.LastUpdated >= modifiedSince);
+ query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince));
}
+ query = query.OrderBy(p => p.Time);
+
var postEntities = await query.ToListAsync();
var posts = new List<TimelinePost>();
@@ -663,6 +667,8 @@ namespace Timeline.Services public async Task<TimelinePost> CreateTextPost(string timelineName, long authorId, string text, DateTime? time)
{
+ time = time?.MyToUtc();
+
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
if (text == null)
@@ -704,6 +710,8 @@ namespace Timeline.Services public async Task<TimelinePost> CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time)
{
+ time = time?.MyToUtc();
+
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
if (data == null)
diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs index a016ff96..813dae67 100644 --- a/Timeline/Services/UserTokenManager.cs +++ b/Timeline/Services/UserTokenManager.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
+using Timeline.Helpers;
using Timeline.Models;
using Timeline.Services.Exceptions;
@@ -57,6 +58,8 @@ namespace Timeline.Services public async Task<UserTokenCreateResult> CreateToken(string username, string password, DateTime? expireAt = null)
{
+ expireAt = expireAt?.MyToUtc();
+
if (username == null)
throw new ArgumentNullException(nameof(username));
if (password == null)
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 7a813ac7..be2377b9 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using System;
+using System.ComponentModel;
using System.Text.Json.Serialization;
using Timeline.Auth;
using Timeline.Configs;
@@ -40,6 +41,8 @@ namespace Timeline // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
+ TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(MyDateTimeConverter)));
+
services.AddControllers(setup =>
{
setup.InputFormatters.Add(new StringInputFormatter());
|