|
6 | 6 | using System.Text; |
7 | 7 | using System.Text.RegularExpressions; |
8 | 8 | using System.Threading; |
| 9 | +using System.Threading.Channels; |
9 | 10 | using System.Threading.Tasks; |
10 | 11 | using Bleatingsheep.NewHydrant.Attributions; |
11 | 12 | using Bleatingsheep.NewHydrant.Core; |
|
16 | 17 | using Sisters.WudiLib.Posts; |
17 | 18 | using MessageContext = Sisters.WudiLib.Posts.Message; |
18 | 19 |
|
19 | | -namespace Bleatingsheep.NewHydrant.Osu |
| 20 | +namespace Bleatingsheep.NewHydrant.Osu; |
| 21 | + |
| 22 | +[Component("highlight")] |
| 23 | +public sealed partial class Highlight : Service, IMessageCommand |
20 | 24 | { |
21 | | - [Component("highlight")] |
22 | | - public sealed class Highlight : Service, IMessageCommand |
| 25 | + private readonly IDbContextFactory<NewbieContext> _dbContextFactory; |
| 26 | + |
| 27 | + public Highlight(OsuApiClient osuApi, IDbContextFactory<NewbieContext> dbContextFactory) |
23 | 28 | { |
24 | | - private readonly IDbContextFactory<NewbieContext> _dbContextFactory; |
| 29 | + OsuApi = osuApi; |
| 30 | + _dbContextFactory = dbContextFactory; |
| 31 | + } |
25 | 32 |
|
26 | | - public Highlight(OsuApiClient osuApi, IDbContextFactory<NewbieContext> dbContextFactory) |
| 33 | + private OsuApiClient OsuApi { get; } |
| 34 | + |
| 35 | + public async Task ProcessAsync(MessageContext superContext, HttpApiClient api) |
| 36 | + { |
| 37 | + var context = superContext as GroupMessage; |
| 38 | + var groupMembers = await api.GetGroupMemberListAsync(context.GroupId); |
| 39 | + Logger.Info($"群 {context.GroupId} 开启今日高光,成员共 {groupMembers.Length} 名。"); |
| 40 | + |
| 41 | + var stopwatch = Stopwatch.StartNew(); |
| 42 | + |
| 43 | + await using var dbContext = _dbContextFactory.CreateDbContext(); |
| 44 | + //var bindings = await (from b in dbContext.Bindings |
| 45 | + // join mi in groupMembers on b.UserId equals mi.UserId |
| 46 | + // select new { Info = mi, Binding = b.OsuId }).ToListAsync(); |
| 47 | + ////var history = (from bi in bindings.AsQueryable() |
| 48 | + //// join ui in motherShip.Userinfo on bi.Binding equals ui.UserId into histories |
| 49 | + //// select new { bi.Info, bi.Binding, History = histories.OrderByDescending(ui => ui.QueryDate).First() }).ToList(); |
| 50 | + //var osuIds = bindings.Select(b => b.Binding).Distinct().ToList(); |
| 51 | + var qqs = groupMembers.Select(mi => mi.UserId).ToList(); |
| 52 | + var osuIds = await dbContext |
| 53 | + .Bindings.Where(bi => qqs.Contains(bi.UserId)) |
| 54 | + .Select(bi => bi.OsuId) |
| 55 | + .Distinct() |
| 56 | + .ToListAsync(); |
| 57 | + |
| 58 | + Logger.Info($"找到 {osuIds.Count} 个绑定信息,耗时 {stopwatch.ElapsedMilliseconds}ms。"); |
| 59 | + if (osuIds.Count > 100) |
27 | 60 | { |
28 | | - OsuApi = osuApi; |
29 | | - _dbContextFactory = dbContextFactory; |
| 61 | + await api.SendMessageAsync( |
| 62 | + context.Endpoint, |
| 63 | + "开始查询今日高光,本群人数较多,可能耗时较长,请耐心等待。" |
| 64 | + ); |
30 | 65 | } |
31 | 66 |
|
32 | | - private OsuApiClient OsuApi { get; } |
33 | | - |
34 | | - public async Task ProcessAsync(MessageContext superContext, HttpApiClient api) |
| 67 | + Bleatingsheep.Osu.Mode mode = 0; |
| 68 | + if (!string.IsNullOrEmpty(ModeString)) |
35 | 69 | { |
36 | | - var context = superContext as GroupMessage; |
37 | | - var groupMembers = await api.GetGroupMemberListAsync(context.GroupId); |
38 | | - Logger.Debug($"群 {context.GroupId} 开启今日高光,成员共 {groupMembers.Length} 名。"); |
39 | | - |
40 | | - var stopwatch = Stopwatch.StartNew(); |
41 | | - |
42 | | - await using var dbContext = _dbContextFactory.CreateDbContext(); |
43 | | - //var bindings = await (from b in dbContext.Bindings |
44 | | - // join mi in groupMembers on b.UserId equals mi.UserId |
45 | | - // select new { Info = mi, Binding = b.OsuId }).ToListAsync(); |
46 | | - ////var history = (from bi in bindings.AsQueryable() |
47 | | - //// join ui in motherShip.Userinfo on bi.Binding equals ui.UserId into histories |
48 | | - //// select new { bi.Info, bi.Binding, History = histories.OrderByDescending(ui => ui.QueryDate).First() }).ToList(); |
49 | | - //var osuIds = bindings.Select(b => b.Binding).Distinct().ToList(); |
50 | | - var qqs = groupMembers.Select(mi => mi.UserId).ToList(); |
51 | | - var osuIds = await dbContext.Bindings.Where(bi => qqs.Contains(bi.UserId)).Select(bi => bi.OsuId).Distinct().ToListAsync(); |
52 | | - |
53 | | - Logger.Debug($"找到 {osuIds.Count} 个绑定信息,耗时 {stopwatch.ElapsedMilliseconds}ms。"); |
54 | | - |
55 | | - Bleatingsheep.Osu.Mode mode = 0; |
56 | | - if (!string.IsNullOrEmpty(ModeString)) |
| 70 | + try |
57 | 71 | { |
58 | | - try |
59 | | - { |
60 | | - mode = Bleatingsheep.Osu.ModeExtensions.Parse(ModeString); |
61 | | - } |
62 | | - catch (FormatException) |
63 | | - { |
64 | | - // ignore |
65 | | - } |
| 72 | + mode = Bleatingsheep.Osu.ModeExtensions.Parse(ModeString); |
66 | 73 | } |
| 74 | + catch (FormatException) |
| 75 | + { |
| 76 | + // ignore |
| 77 | + } |
| 78 | + } |
67 | 79 |
|
68 | | - stopwatch = Stopwatch.StartNew(); |
69 | | - List<UserSnapshot> history = await GetHistories(osuIds, mode).ConfigureAwait(false); |
70 | | - |
71 | | - Logger.Debug($"找到 {history.Count} 个历史信息,耗时 {stopwatch.ElapsedMilliseconds}ms。"); |
72 | | - |
73 | | - // Using ConcurrentBag is enough here. ConcurrentDictionary is unnecessary and costly. |
74 | | - var nowInfos = new ConcurrentDictionary<int, UserInfo>(10, history.Count); |
75 | | - var fails = new BlockingCollection<int>(); |
76 | | - stopwatch = Stopwatch.StartNew(); |
77 | | - var fetchIds = history.Select(h => (int)h.UserId).Distinct().ToList(); |
78 | | - int completes = 0; |
79 | | - var cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token; |
80 | | - var tasks = fetchIds.Concat(fails.GetConsumingEnumerable()).Select(async bi => |
| 80 | + stopwatch = Stopwatch.StartNew(); |
| 81 | + List<UserSnapshot> history = await GetHistories(osuIds, mode).ConfigureAwait(false); |
| 82 | + |
| 83 | + Logger.Info($"找到 {history.Count} 个历史信息,耗时 {stopwatch.ElapsedMilliseconds}ms。"); |
| 84 | + |
| 85 | + var nowInfos = new ConcurrentDictionary<int, UserInfo>(-1, history.Count); |
| 86 | + var fails = Channel.CreateBounded<int>(history.Count); |
| 87 | + var (failsTx, failsRx) = (fails.Writer, fails.Reader); |
| 88 | + stopwatch = Stopwatch.StartNew(); |
| 89 | + var fetchIds = history.Select(h => h.UserId).Distinct().ToList(); |
| 90 | + int completes = 0; |
| 91 | + var cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token; |
| 92 | + var tasks = await fetchIds |
| 93 | + .ToAsyncEnumerable() |
| 94 | + .Concat(failsRx.ReadAllAsync()) |
| 95 | + .Select(async bi => |
81 | 96 | { |
82 | | - var (success, userInfo) = await OsuApi.GetCachedUserInfo(bi, (Bleatingsheep.Osu.Mode)mode).ConfigureAwait(false); |
| 97 | + var (success, userInfo) = await OsuApi |
| 98 | + .GetCachedUserInfo(bi, mode) |
| 99 | + .ConfigureAwait(false); |
83 | 100 | if (!success) |
84 | 101 | { |
85 | 102 | if (cancellationToken.IsCancellationRequested) |
86 | | - fails.CompleteAdding(); |
87 | | - if (!fails.IsAddingCompleted) |
88 | | - try |
89 | | - { |
90 | | - fails.Add(bi); |
91 | | - } |
92 | | - catch (InvalidOperationException) |
93 | | - { |
94 | | - } |
| 103 | + { |
| 104 | + Logger.Error("查询用户信息超时,取消中..."); |
| 105 | + _ = failsTx.TryComplete(); |
| 106 | + } |
| 107 | + |
| 108 | + _ = failsTx.TryWrite(bi); |
| 109 | + // 如果写入时 Channel 已关闭,则会写入失败,这种情况下不需要再写入了。 |
95 | 110 | } |
96 | 111 | else |
97 | 112 | { |
98 | 113 | Interlocked.Increment(ref completes); |
99 | 114 | if (completes == fetchIds.Count) |
100 | | - fails.CompleteAdding(); |
| 115 | + { |
| 116 | + _ = failsTx.TryComplete(); |
| 117 | + } |
| 118 | + |
101 | 119 | if (userInfo != null) |
| 120 | + { |
102 | 121 | nowInfos[bi] = userInfo; |
| 122 | + } |
103 | 123 | } |
104 | | - }).ToArray(); |
105 | | - await Task.WhenAll(tasks).ConfigureAwait(false); |
106 | | - Logger.Debug($"查询 API 花费 {stopwatch.ElapsedMilliseconds}ms,失败 {fails.Count} 个。"); |
107 | | - |
108 | | - var cps = (from his in history |
109 | | - join now in nowInfos on his.UserId equals now.Key |
110 | | - where his.UserInfo.PlayCount != now.Value.PlayCount |
111 | | - orderby now.Value.Performance - his.UserInfo.Performance descending |
112 | | - select new { Old = his.UserInfo, New = now.Value, Meta = his }).ToList(); |
| 124 | + }) |
| 125 | + .ToArrayAsync(); |
| 126 | + await Task.WhenAll(tasks).ConfigureAwait(false); |
| 127 | + Logger.Info( |
| 128 | + $"查询 API 花费 {stopwatch.ElapsedMilliseconds}ms,失败 {tasks.Length - fetchIds.Count} 个。" |
| 129 | + ); |
| 130 | + |
| 131 | + var errorMessages = new List<string>(); |
| 132 | + if (cancellationToken.IsCancellationRequested) |
| 133 | + { |
| 134 | + errorMessages.Add("查询用户信息超时,部分数据可能不完整。"); |
| 135 | + } |
| 136 | + if (tasks.Length - fetchIds.Count > 0) |
| 137 | + { |
| 138 | + errorMessages.Add($"有 {tasks.Length - fetchIds.Count} 人增量数据查询失败。"); |
| 139 | + } |
113 | 140 |
|
114 | | - if (fails.Count > 0) |
115 | | - { |
116 | | - await api.SendGroupMessageAsync(context.GroupId, $"失败了 {fails.Count} 人。"); |
117 | | - } |
118 | | - if (cps.Count == 0) |
| 141 | + var cps = ( |
| 142 | + from his in history |
| 143 | + join now in nowInfos on his.UserId equals now.Key |
| 144 | + where his.UserInfo.PlayCount != now.Value.PlayCount |
| 145 | + orderby now.Value.Performance - his.UserInfo.Performance descending |
| 146 | + select new |
119 | 147 | { |
120 | | - await api.SendMessageAsync(context.Endpoint, "你群根本没有人屙屎。"); |
121 | | - return; |
| 148 | + Old = his.UserInfo, |
| 149 | + New = now.Value, |
| 150 | + Meta = his, |
122 | 151 | } |
123 | | - else |
| 152 | + ).ToList(); |
| 153 | + |
| 154 | + if (cps.Count == 0) |
| 155 | + { |
| 156 | + var message = "你群根本没有人屙屎。"; |
| 157 | + if (errorMessages.Count > 0) |
124 | 158 | { |
125 | | - var increase = cps.Find(cp => cp.Old.Performance != 0 && cp.New.Performance != cp.Old.Performance); |
126 | | - var mostPlay = cps.OrderByDescending(cp => cp.New.TotalHits - cp.Old.TotalHits).First(); |
127 | | - var longestPlay = cps.OrderByDescending(cp => cp.New.TotalSecondsPlayed - cp.Old.TotalSecondsPlayed).First(); |
128 | | - var sb = new StringBuilder(100); |
129 | | - sb.AppendLine("最飞升:"); |
130 | | - if (increase != null) |
131 | | - // sb.AppendLine($"{increase.New.Name} 增加了 {increase.New.Performance - increase.Old.Performance:#.##} PP。") |
132 | | - sb.Append(increase.New.Name).Append(" 增加了 ").AppendFormat("{0:#.##}", increase.New.Performance - increase.Old.Performance).AppendLine(" PP。") |
133 | | - // .AppendLine($"({increase.Old.Performance:#.##} -> {increase.New.Performance:#.##})"); |
134 | | - .Append('(').AppendFormat("{0:#.##}", increase.Old.Performance).Append(" -> ").AppendFormat("{0:#.##}", increase.New.Performance).AppendLine(")"); |
135 | | - else |
136 | | - sb.AppendLine("你群没有人飞升。"); |
137 | | - sb.AppendLine("最肝:") |
138 | | - // .Append($"{mostPlay.New.Name} 打了 {mostPlay.New.TotalHits - mostPlay.Old.TotalHits} 下。"); |
139 | | - .Append(mostPlay.New.Name).Append(" 打了 ").Append(mostPlay.New.TotalHits - mostPlay.Old.TotalHits).Append(" 下。"); |
140 | | - sb.AppendLine(); |
141 | | - sb.Append($"{longestPlay.New.Name} 玩儿了 {TimeSpan.FromSeconds(longestPlay.New.TotalSecondsPlayed - longestPlay.Old.TotalSecondsPlayed).TotalHours:#.##} 小时。"); |
142 | | - |
143 | | - await api.SendMessageAsync(context.Endpoint, sb.ToString()); |
| 159 | + var noOsuSB = new StringBuilder("你群根本没有人屙屎。\r\n\r\n错误信息:\r\n"); |
| 160 | + noOsuSB.AppendJoin("\r\n", errorMessages); |
| 161 | + message = noOsuSB.ToString(); |
144 | 162 | } |
| 163 | + await api.SendMessageAsync(context.Endpoint, message); |
| 164 | + return; |
145 | 165 | } |
146 | 166 |
|
147 | | - private async Task<List<UserSnapshot>> GetHistories(List<int> osuIds, Bleatingsheep.Osu.Mode mode) |
| 167 | + var increase = cps.Find(cp => |
| 168 | + cp.Old.Performance != 0 && cp.New.Performance != cp.Old.Performance |
| 169 | + ); |
| 170 | + var mostPlay = cps.OrderByDescending(cp => cp.New.TotalHits - cp.Old.TotalHits).First(); |
| 171 | + var longestPlay = cps.OrderByDescending(cp => |
| 172 | + cp.New.TotalSecondsPlayed - cp.Old.TotalSecondsPlayed |
| 173 | + ) |
| 174 | + .First(); |
| 175 | + var sb = new StringBuilder(100); |
| 176 | + sb.AppendLine("最飞升:"); |
| 177 | + if (increase != null) |
| 178 | + { |
| 179 | + sb.AppendLine( |
| 180 | + $"{increase.New.Name} 增加了 {increase.New.Performance - increase.Old.Performance:#.##} PP。" |
| 181 | + ) |
| 182 | + .AppendLine( |
| 183 | + $"({increase.Old.Performance:#.##} -> {increase.New.Performance:#.##})" |
| 184 | + ); |
| 185 | + } |
| 186 | + else |
148 | 187 | { |
149 | | - await using var newbieContext = _dbContextFactory.CreateDbContext(); |
150 | | - var now = DateTimeOffset.UtcNow; |
151 | | - var snaps = await newbieContext.UserSnapshots |
152 | | - .Where(s => now.Subtract(TimeSpan.FromHours(36)) < s.Date && s.Mode == mode && osuIds.Contains(s.UserId)) |
153 | | - .ToListAsync().ConfigureAwait(false); |
154 | | - |
155 | | - return snaps |
156 | | - .GroupBy(s => s.UserId) |
157 | | - .Select(g => g.OrderBy(s => Utilities.DateUtility.GetError(now - TimeSpan.FromHours(24), s.Date)).First()) |
158 | | - .ToList(); |
| 188 | + sb.AppendLine("你群没有人飞升。"); |
159 | 189 | } |
160 | 190 |
|
161 | | - [Parameter("mode")] |
162 | | - private string ModeString { get; set; } |
| 191 | + sb.AppendLine("最肝:") |
| 192 | + .AppendLine( |
| 193 | + $"{mostPlay.New.Name} 打了 {mostPlay.New.TotalHits - mostPlay.Old.TotalHits} 下。" |
| 194 | + ); |
| 195 | + sb.Append( |
| 196 | + $"{longestPlay.New.Name} 玩儿了 {TimeSpan.FromSeconds(longestPlay.New.TotalSecondsPlayed - longestPlay.Old.TotalSecondsPlayed).TotalHours:#.##} 小时。" |
| 197 | + ); |
163 | 198 |
|
164 | | - private static readonly Regex s_regex = new Regex(@"^今日高光\s*(?:[,,]\s*(?<mode>.+?)\s*)?$", RegexOptions.Compiled | RegexOptions.CultureInvariant); |
| 199 | + if (errorMessages.Count > 0) |
| 200 | + { |
| 201 | + sb.Append("\r\n\r\n错误信息:\r\n"); |
| 202 | + sb.AppendJoin("\r\n", errorMessages); |
| 203 | + } |
165 | 204 |
|
166 | | - public bool ShouldResponse(MessageContext context) |
167 | | - => context is GroupMessage g |
168 | | - && g.Content.TryGetPlainText(out string text) |
169 | | - && RegexCommand(s_regex, text.Trim()); |
| 205 | + await api.SendMessageAsync(context.Endpoint, sb.ToString()); |
170 | 206 | } |
| 207 | + |
| 208 | + private async Task<List<UserSnapshot>> GetHistories( |
| 209 | + List<int> osuIds, |
| 210 | + Bleatingsheep.Osu.Mode mode |
| 211 | + ) |
| 212 | + { |
| 213 | + await using var newbieContext = _dbContextFactory.CreateDbContext(); |
| 214 | + var now = DateTimeOffset.UtcNow; |
| 215 | + var snaps = await newbieContext |
| 216 | + .UserSnapshots.Where(s => |
| 217 | + now.Subtract(TimeSpan.FromHours(36)) < s.Date |
| 218 | + && s.Mode == mode |
| 219 | + && osuIds.Contains(s.UserId) |
| 220 | + ) |
| 221 | + .ToListAsync() |
| 222 | + .ConfigureAwait(false); |
| 223 | + |
| 224 | + return snaps |
| 225 | + .GroupBy(s => s.UserId) |
| 226 | + .Select(g => |
| 227 | + g.OrderBy(s => Utilities.DateUtility.GetError(now - TimeSpan.FromHours(24), s.Date)) |
| 228 | + .First() |
| 229 | + ) |
| 230 | + .ToList(); |
| 231 | + } |
| 232 | + |
| 233 | + [Parameter("mode")] |
| 234 | + private string ModeString { get; set; } |
| 235 | + |
| 236 | + [GeneratedRegex( |
| 237 | + @"^今日高光\s*(?:[,,]\s*(?<mode>.+?)\s*)?$", |
| 238 | + RegexOptions.Compiled | RegexOptions.CultureInvariant |
| 239 | + )] |
| 240 | + private static partial Regex MatchRegex(); |
| 241 | + |
| 242 | + public bool ShouldResponse(MessageContext context) => |
| 243 | + context is GroupMessage g |
| 244 | + && g.Content.TryGetPlainText(out string text) |
| 245 | + && RegexCommand(MatchRegex(), text.Trim()); |
171 | 246 | } |
0 commit comments