public static PKError AccountInOtherSystem(PKSystem system) => new PKError($"The mentioned account is already linked to another system (see `pk;system {system.Hid}`).");
public async Task SystemFrontHistory(Context ctx, PKSystem system) { if (system == null) { throw Errors.NoSystemError; } ctx.CheckSystemPrivacy(system, system.FrontHistoryPrivacy); // Gotta be careful here: if we dispose of the connection while the IAE is alive, boom await using var conn = await _db.Obtain(); var totalSwitches = await _repo.GetSwitchCount(conn, system.Id); if (totalSwitches == 0) { throw Errors.NoRegisteredSwitches; } var sws = _repo.GetSwitches(conn, system.Id) .Scan(new FrontHistoryEntry(null, null), (lastEntry, newSwitch) => new FrontHistoryEntry(lastEntry.ThisSwitch?.Timestamp, newSwitch)); var embedTitle = system.Name != null ? $"Front history of {system.Name} (`{system.Hid}`)" : $"Front history of `{system.Hid}`"; await ctx.Paginate( sws, totalSwitches, 10, embedTitle, async (builder, switches) => { foreach (var entry in switches) { var lastSw = entry.LastTime; var sw = entry.ThisSwitch; // Fetch member list and format await using var conn = await _db.Obtain(); var members = await _db.Execute(c => _repo.GetSwitchMembers(c, sw.Id)).ToListAsync(); var membersStr = members.Any() ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "no fronter"; var switchSince = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp; // If this isn't the latest switch, we also show duration string stringToAdd; if (lastSw != null) { // Calculate the time between the last switch (that we iterated - ie. the next one on the timeline) and the current one var switchDuration = lastSw.Value - sw.Timestamp; stringToAdd = $"**{membersStr}** ({sw.Timestamp.FormatZoned(system.Zone)}, {switchSince.FormatDuration()} ago, for {switchDuration.FormatDuration()})\n"; } else { stringToAdd = $"**{membersStr}** ({sw.Timestamp.FormatZoned(system.Zone)}, {switchSince.FormatDuration()} ago)\n"; } try // Unfortunately the only way to test DiscordEmbedBuilder.Description max length is this { builder.Description += stringToAdd; } catch (ArgumentException) { break; } // TODO: Make sure this works } } ); }
public async Task <ImportResult> ImportSystem(DataFileSystem data, PKSystem system, ulong accountId) { // TODO: make atomic, somehow - we'd need to obtain one IDbConnection and reuse it // which probably means refactoring SystemStore.Save and friends etc var result = new ImportResult { AddedNames = new List <string>(), ModifiedNames = new List <string>(), Success = true // Assume success unless indicated otherwise }; var dataFileToMemberMapping = new Dictionary <string, PKMember>(); var unmappedMembers = new List <DataFileMember>(); // If we don't already have a system to save to, create one if (system == null) { system = await _data.CreateSystem(data.Name); } result.System = system; // Apply system info system.Name = data.Name; if (data.Description != null) { system.Description = data.Description; } if (data.Tag != null) { system.Tag = data.Tag; } if (data.AvatarUrl != null) { system.AvatarUrl = data.AvatarUrl; } if (data.TimeZone != null) { system.UiTz = data.TimeZone ?? "UTC"; } await _data.SaveSystem(system); // Make sure to link the sender account, too await _data.AddAccount(system, accountId); // Determine which members already exist and which ones need to be created var existingMembers = await _data.GetSystemMembers(system); foreach (var d in data.Members) { // Try to look up the member with the given ID var match = existingMembers.FirstOrDefault(m => m.Hid.Equals(d.Id)); if (match == null) { match = existingMembers.FirstOrDefault(m => m.Name.Equals(d.Name)); // Try with the name instead } if (match != null) { dataFileToMemberMapping.Add(d.Id, match); // Relate the data file ID to the PKMember for importing switches result.ModifiedNames.Add(d.Name); } else { unmappedMembers.Add(d); // Track members that weren't found so we can create them all result.AddedNames.Add(d.Name); } } // If creating the unmatched members would put us over the member limit, abort before creating any members // new total: # in the system + (# in the file - # in the file that already exist) if (data.Members.Count - dataFileToMemberMapping.Count + existingMembers.Count() > Limits.MaxMemberCount) { result.Success = false; result.Message = $"Import would exceed the maximum number of members ({Limits.MaxMemberCount})."; result.AddedNames.Clear(); result.ModifiedNames.Clear(); return(result); } // Create all unmapped members in one transaction // These consist of members from another PluralKit system or another framework (e.g. Tupperbox) var membersToCreate = new Dictionary <string, string>(); unmappedMembers.ForEach(x => membersToCreate.Add(x.Id, x.Name)); var newMembers = await _data.CreateMembersBulk(system, membersToCreate); foreach (var member in newMembers) { dataFileToMemberMapping.Add(member.Key, member.Value); } // Update members with data file properties // TODO: parallelize? foreach (var dataMember in data.Members) { dataFileToMemberMapping.TryGetValue(dataMember.Id, out PKMember member); if (member == null) { continue; } // Apply member info member.Name = dataMember.Name; if (dataMember.DisplayName != null) { member.DisplayName = dataMember.DisplayName; } if (dataMember.Description != null) { member.Description = dataMember.Description; } if (dataMember.Color != null) { member.Color = dataMember.Color; } if (dataMember.AvatarUrl != null) { member.AvatarUrl = dataMember.AvatarUrl; } if (dataMember.Prefix != null || dataMember.Suffix != null) { member.Prefix = dataMember.Prefix; member.Suffix = dataMember.Suffix; } if (dataMember.Birthday != null) { var birthdayParse = Formats.DateExportFormat.Parse(dataMember.Birthday); member.Birthday = birthdayParse.Success ? (LocalDate?)birthdayParse.Value : null; } await _data.SaveMember(member); } // Re-map the switch members in the likely case IDs have changed var mappedSwitches = new List <ImportedSwitch>(); foreach (var sw in data.Switches) { var timestamp = InstantPattern.ExtendedIso.Parse(sw.Timestamp).Value; var swMembers = new List <PKMember>(); swMembers.AddRange(sw.Members.Select(x => dataFileToMemberMapping.FirstOrDefault(y => y.Key.Equals(x)).Value)); mappedSwitches.Add(new ImportedSwitch { Timestamp = timestamp, Members = swMembers }); } // Import switches if (mappedSwitches.Any()) { await _data.AddSwitchesBulk(system, mappedSwitches); } _logger.Information("Imported system {System}", system.Hid); return(result); }
public async Task <DiscordEmbed> CreateMemberEmbed(PKSystem system, PKMember member, DiscordGuild guild, LookupContext ctx) { // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); var name = member.NameFor(ctx); if (system.Name != null) { name = $"{name} ({system.Name})"; } DiscordColor color; try { color = member.Color?.ToDiscordColor() ?? DiscordUtils.Gray; } catch (ArgumentException) { // Bad API use can cause an invalid color string // TODO: fix that in the API // for now we just default to a blank color, yolo color = DiscordUtils.Gray; } var guildSettings = guild != null ? await _db.Execute(c => c.QueryOrInsertMemberGuildConfig(guild.Id, member.Id)) : null; var guildDisplayName = guildSettings?.DisplayName; var avatar = guildSettings?.AvatarUrl ?? member.AvatarFor(ctx); var proxyTagsStr = string.Join('\n', member.ProxyTags.Select(t => $"`` {t.ProxyString}``")); var eb = new DiscordEmbedBuilder() // TODO: add URL of website when that's up .WithAuthor(name, iconUrl: DiscordUtils.WorkaroundForUrlBug(avatar)) // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray) .WithColor(color) .WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(system)}":"")}"); var description = ""; if (member.MemberVisibility == PrivacyLevel.Private) { description += "*(this member is hidden)*\n"; } if (guildSettings?.AvatarUrl != null) { if (member.AvatarFor(ctx) != null) { description += $"*(this member has a server-specific avatar set; [click here]({member.AvatarUrl}) to see the global avatar)*\n"; } else { description += "*(this member has a server-specific avatar set)*\n"; } } if (description != "") { eb.WithDescription(description); } if (avatar != null) { eb.WithThumbnail(avatar); } if (!member.DisplayName.EmptyOrNull() && member.NamePrivacy.CanAccess(ctx)) { eb.AddField("Display Name", member.DisplayName.Truncate(1024), true); } if (guild != null && guildDisplayName != null) { eb.AddField($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true); } if (member.BirthdayFor(ctx) != null) { eb.AddField("Birthdate", member.BirthdayString, true); } if (member.PronounsFor(ctx) is {} pronouns&& !string.IsNullOrWhiteSpace(pronouns)) { eb.AddField("Pronouns", pronouns.Truncate(1024), true); } if (member.MessageCountFor(ctx) is {} count&& count > 0) { eb.AddField("Message Count", member.MessageCount.ToString(), true); } if (member.HasProxyTags) { eb.AddField("Proxy Tags", string.Join('\n', proxyTagsStr).Truncate(1024), true); } // --- For when this gets added to the member object itself or however they get added // if (member.LastMessage != null && member.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last message:" FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value))); // if (member.LastSwitchTime != null && m.MetadataPrivacy.CanAccess(ctx)) eb.AddField("Last switched in:", FormatTimestamp(member.LastSwitchTime.Value)); // if (!member.Color.EmptyOrNull() && member.ColorPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true); if (!member.Color.EmptyOrNull()) { eb.AddField("Color", $"#{member.Color}", true); } if (member.DescriptionFor(ctx) is {} desc) { eb.AddField("Description", member.Description.NormalizeLineEndSpacing(), false); } return(eb.Build()); }