R1 cleanup: collAnon and dead code gone, docs current, every peer's scenario in one sweep
Build / Build (push) Successful in 1m7s

Starts the completion program (docs/ROADMAP.md, "Owner decisions on finishing everything", 2026-10-17).

- Dead code: the commented-out collAnon blocks (discussions, seeding, admin user lists, HTTP/3, the old signature
  middleware), the empty PrivaPubWorkersConfiguration, PublicAvatarUsersService and UserPolicyType, unused helpers,
  the second IPasswordHasher registration, the PasswordGenerator package, SignUpAsync/LoginAsync's unused invitation
  parameters. "Starting PrivaPub"; a signed-in login is sent to "/", not to "/discussions".
- Stored fields nothing read: RootUser.Contacts and ResetPasswordToken*, the local Avatar.AvatarServer,
  EmailRecovery.RecoveryCode/RequestIP/CreationDate, MongoSettings.LogsDatabase, and AppConfiguration's
  upload/discussion keys, RequiresFirstTimeSetup and Version (old documents still load: MongoDB.Entities ignores extra
  elements). DmPost's indexes are no longer made. 49 collAnon keys leave FieldsNameResource in every language.
- /clientapi/data/current-version answers the build's ref. UsersIds is a list that can't throw, and the admin routes
  leave the administrator out without mutating the request.
- deploy.yml checks SecureMode on a name no persona can have, and the browser redirect on the directory's first local
  persona: @thepra is the administrator's own now.
- Town: decePub's e2e remnants (expect, client.jsonl, the decePub column) and town/tests go; the dead gaps/load
  commands too. Mobilizon no longer keeps an event for a deleted test.
- tools/pasture/sweep.sh runs every peer's scenario, batch by batch, as CLAUDE.md asks before a change to what
  PrivaPub sends.
- Docs: CLAUDE.md's repo map, route table (every frozen route) and stale lines; ROADMAP's status (setup, v1.25.0,
  v1.26.0, the program C), the 2026-10-17 decisions, and dated notes where earlier text no longer holds; FEDERATION.md's
  FEP list (7888, f228, 7628, ae0c and 8967 supported) and the deploy's persona.

Verified: dotnet build PrivaPub.sln -c Release (0 errors, no new warnings); bash -n on the touched scripts; py_compile
on the town; the pasture's gts, mastodon and lemmy scenarios on this build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-17 02:56:57 +02:00
1 parent 6727e1817a
commit ff5e635dda
71 files changed
+238 -4767

No files matched your search

@@ -14,8 +14,9 @@ namespace PrivaPub.Services.ClientToServer.Public
AppConfigurationService = appConfigurationService;
}
// the build's ref (its tag, or the branch a manual deploy ran from), as /build.json and NodeInfo name it
public string GetCurrentVersion() =>
AppConfigurationService.AppConfiguration.Version;
string.IsNullOrEmpty(BuildInfo.Ref) ? BuildInfo.Commit : BuildInfo.Ref;
public async Task<IEnumerable<Language>> GetLanguages(CancellationToken cancellationToken)
{
@@ -23,15 +24,5 @@ namespace PrivaPub.Services.ClientToServer.Public
.Match(l => AppConfigurationService.AppConfiguration.SupportedLanguages.Contains(l.International2Code))
.ExecuteAsync(cancellationToken)).OrderBy(l => l.NativeName).ToArray();
}
public async Task<bool> IsValidLanguageId(string languageId)
{
var anyLanguageWithId = await DbEntities.Languages.Match(l => l.ID == languageId).ExecuteAnyAsync();
if (!anyLanguageWithId)
return false;
var language2Code = (await DbEntities.Languages
.Match(l => l.ID == languageId).ExecuteFirstAsync())?.International2Code;
return AppConfigurationService.AppConfiguration.SupportedLanguages.Contains(language2Code);
}
}
}
@@ -7,7 +7,5 @@ namespace PrivaPub.Services.ClientToServer.Public
string GetCurrentVersion();
Task<IEnumerable<Language>> GetLanguages(CancellationToken cancellationToken);
Task<bool> IsValidLanguageId(string languageId);
}
}
@@ -1,174 +0,0 @@
using Microsoft.Extensions.Localization;
using PrivaPub.ClientModels;
using PrivaPub.Models.User;
using PrivaPub.Resources;
using PrivaPub.StaticServices;
namespace PrivaPub.Services.ClientToServer.Public
{
public interface IPublicAvatarUsersService
{
Task<WebResult> GetLocalAvatar(UserPolicyType userPolicyType, string actor, CancellationToken token);
Task<IEnumerable<Avatar>> GetLocalAvatars(UserPolicyType userPolicyType, CancellationToken token);
Task<WebResult> GetForeignAvatar(UserPolicyType userPolicyType, string actor, CancellationToken token);
Task<IEnumerable<ForeignAvatar>> GetForeignAvatars(UserPolicyType userPolicyType, CancellationToken token);
Task<IEnumerable<Avatar>> GetRootAvatars(string rootUserId, CancellationToken token);
}
public class PublicAvatarUsersService : IPublicAvatarUsersService
{
readonly DbEntities _dbEntities;
readonly IStringLocalizer<GenericRes> _localizer;
readonly ILogger<PublicAvatarUsersService> _logger;
public PublicAvatarUsersService(DbEntities dbEntities,
IStringLocalizer<GenericRes> localizer,
ILogger<PublicAvatarUsersService> logger)
{
_dbEntities = dbEntities;
_localizer = localizer;
_logger = logger;
}
public async Task<WebResult> GetLocalAvatar(UserPolicyType userPolicyType, string actor, CancellationToken token)
{
var result = new WebResult();
try
{
var query = _dbEntities.Avatars;
switch (userPolicyType)
{
case UserPolicyType.IsUser:
query.Match(a => !a.DeletionAt.HasValue && a.UserName == actor);
break;
case UserPolicyType.IsModerator:
query.Match(a => !a.DeletionAt.HasValue && a.UserName == actor);
break;
case UserPolicyType.IsAdmin:
query.Match(a => a.UserName == actor);
break;
}
var avatar = await query.ExecuteFirstAsync(token);
if (avatar == default)
return result.Invalidate(_localizer["User '{0}' not found.", actor], StatusCodes.Status404NotFound);
result.Data = avatar;
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetLocalAvatar)}");
return result.Invalidate(_localizer["Error: {0}", ex.ToString()], exception: ex);
}
}
public async Task<IEnumerable<Avatar>> GetLocalAvatars(UserPolicyType userPolicyType, CancellationToken token)
{
try
{
var query = _dbEntities.Avatars;
switch (userPolicyType)
{
case UserPolicyType.IsUser:
query.Match(a => !a.DeletionAt.HasValue);
break;
case UserPolicyType.IsModerator:
query.Match(a => !a.DeletionAt.HasValue);
break;
case UserPolicyType.IsAdmin:
break;
}
var avatars = await query.ExecuteAsync(token);
return avatars;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetLocalAvatars)}");
return Enumerable.Empty<Avatar>();
}
}
public async Task<WebResult> GetForeignAvatar(UserPolicyType userPolicyType, string actor, CancellationToken token)
{
var result = new WebResult();
try
{
var query = _dbEntities.ForeignAvatars;
switch (userPolicyType)
{
case UserPolicyType.IsUser:
query.Match(a => !a.DeletionAt.HasValue && a.UserName == actor);
break;
case UserPolicyType.IsModerator:
query.Match(a => !a.DeletionAt.HasValue && a.UserName == actor);
break;
case UserPolicyType.IsAdmin:
query.Match(a => a.UserName == actor);
break;
}
var avatar = await query.ExecuteFirstAsync(token);
if (avatar == default)
return result.Invalidate(_localizer["User '{0}' not found.", actor], StatusCodes.Status404NotFound);
result.Data = avatar;
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetForeignAvatar)}");
return result.Invalidate(_localizer["Error: {0}", ex.ToString()], exception: ex);
}
}
public async Task<IEnumerable<ForeignAvatar>> GetForeignAvatars(UserPolicyType userPolicyType, CancellationToken token)
{
try
{
var query = _dbEntities.ForeignAvatars;
switch (userPolicyType)
{
case UserPolicyType.IsUser:
query.Match(a => !a.DeletionAt.HasValue);
break;
case UserPolicyType.IsModerator:
query.Match(a => !a.DeletionAt.HasValue);
break;
case UserPolicyType.IsAdmin:
break;
}
var foreignAvatars = await query.ExecuteAsync(token);
return foreignAvatars;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetForeignAvatars)}");
return Enumerable.Empty<ForeignAvatar>();
}
}
public async Task<IEnumerable<Avatar>> GetRootAvatars(string rootUserId, CancellationToken token)
{
try
{
var rootUser = await _dbEntities.RootUsers.MatchID(rootUserId).ExecuteFirstAsync(token);
if (rootUser == default)
return Enumerable.Empty<Avatar>();
var rootToAvatars = await _dbEntities.RootToAvatars.Match(ra => ra.RootId == rootUserId).ExecuteAsync(token);
if (rootToAvatars.Count == 0)
return Enumerable.Empty<Avatar>();
var avatarIds = rootToAvatars.Select(ra => ra.AvatarId).ToArray();
var avatars = await _dbEntities.Avatars
.Match(a => avatarIds.Contains(a.ID))
.ProjectExcluding(a => a.Settings)
.ExecuteAsync(token);
return avatars;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(PublicAvatarUsersService)}.{nameof(GetRootAvatars)}");
return Enumerable.Empty<Avatar>();
}
}
}
}
-22
View File
@@ -18,7 +18,6 @@ namespace PrivaPub.Services
{
public interface IGroupUsersService
{
Task<WebResult> GetGroup(string actor, CancellationToken token);
Task<WebResult> GetGroups(string rootUserId, string avatarId, CancellationToken token);
Task<WebResult> InsertGroup(string rootUserId, InsertGroupForm form, CancellationToken token);
Task<WebResult> UpdateGroup(string rootUserId, UpdateGroupForm form, CancellationToken token);
@@ -58,27 +57,6 @@ namespace PrivaPub.Services
_logger = logger;
}
public async Task<WebResult> GetGroup(string actor, CancellationToken token)
{
var result = new WebResult();
try
{
var userName = actor?.ToLowerInvariant();
var group = await _dbEntities.Groups
.Match(g => g.UserName == userName && !g.DeletionAt.HasValue)
.ExecuteFirstAsync(token);
if (group == default)
return result.Invalidate(_localizer["Group '{0}' not found.", actor], StatusCodes.Status404NotFound);
result.Data = group;
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(GroupUsersService)}.{nameof(GetGroup)}");
return result.Invalidate(_localizer["Something went wrong."], exception: ex);
}
}
public async Task<WebResult> GetGroups(string rootUserId, string avatarId, CancellationToken token)
{
var result = new WebResult();
+2 -2
View File
@@ -7,9 +7,9 @@ namespace PrivaPub.Services
{
public interface IRootUsersService
{
Task<WebResult> LoginAsync(LoginForm loginForm, string invitationCode = default, bool isPasswordRequired = false);
Task<WebResult> LoginAsync(LoginForm loginForm);
Task<WebResult> SignUpAsync(LoginForm signUpForm, string invitationCode = default, bool isPasswordRequired = false);
Task<WebResult> SignUpAsync(LoginForm signUpForm);
Task<WebResult> BanUserAsync(UsersIds usersIds);
+3 -48
View File
@@ -1,14 +1,8 @@
using MailKit.Net.Smtp;
using Microsoft.Extensions.Localization;
using MimeKit;
using MongoDB.Driver;
using MongoDB.Entities;
using PasswordGenerator;
using PrivaPub.ClientModels;
using PrivaPub.ClientModels.User;
using PrivaPub.Models;
@@ -33,7 +27,6 @@ namespace PrivaPub.Services
readonly IStringLocalizer Localizer;
readonly ILogger<RootUsersService> Logger;
readonly AppConfigurationService AppConfigurationService;
readonly AuthTokenManager AuthTokenManager;
readonly IJobQueue Jobs;
readonly IRootSessions Sessions;
readonly IRootRemoval Removal;
@@ -46,14 +39,12 @@ namespace PrivaPub.Services
ILogger<RootUsersService> logger,
IPasswordHasher passwordHasher,
DbEntities dbEntities,
AppConfigurationService appConfigurationService,
AuthTokenManager authTokenManager)
AppConfigurationService appConfigurationService)
{
Jobs = jobs;
Sessions = sessions;
Removal = removal;
DbEntities = dbEntities;
AuthTokenManager = authTokenManager;
PasswordHasher = passwordHasher;
Localizer = localizer;
Logger = logger;
@@ -66,8 +57,7 @@ namespace PrivaPub.Services
// a hash nobody's password matches, so an unknown login costs the same hashing as a wrong password
string Decoy => _decoy ??= PasswordHasher.Hash(Convert.ToHexString(System.Security.Cryptography.RandomNumberGenerator.GetBytes(16)));
public async Task<WebResult> SignUpAsync(LoginForm signUpForm, string invitationCode = default,
bool isPasswordRequired = false)
public async Task<WebResult> SignUpAsync(LoginForm signUpForm)
{
var result = new WebResult();
try
@@ -98,23 +88,6 @@ namespace PrivaPub.Services
};
await DB.Default.SaveAsync(newUser);
//if (!string.IsNullOrEmpty(invitationCode))
//{
// if (isPasswordRequired)
// result = await DiscussionService.InviteUserToDiscussion(new PwDiscussionPreviewForm
// {
// InvitationCode = invitationCode,
// Password = signUpForm.InvitationPassword
// }, newUser.ID);
// else
// result = await DiscussionService.InviteUserToDiscussion(new NoPwDiscussionPreviewForm
// {
// InvitationCode = invitationCode,
// }, newUser.ID);
// if (!result.IsValid)
// return result;
//}
result.Data = (newUser, ToViewSettings(newUser.Settings));
return result;
@@ -126,8 +99,7 @@ namespace PrivaPub.Services
}
}
public async Task<WebResult> LoginAsync(LoginForm loginForm, string invitationCode = default,
bool isPasswordRequired = false)
public async Task<WebResult> LoginAsync(LoginForm loginForm)
{
var result = new WebResult();
try
@@ -148,23 +120,6 @@ namespace PrivaPub.Services
var userSettingsResult = await GetUserSettingsAsync(user.ID, loginForm);
var userSettings = (ViewUserSettings)userSettingsResult.Data;
//if (!string.IsNullOrEmpty(invitationCode))
//{
// if (isPasswordRequired)
// result = await DiscussionService.InviteUserToDiscussion(new PwDiscussionPreviewForm
// {
// InvitationCode = invitationCode,
// Password = loginForm.InvitationPassword
// }, user.ID);
// else
// result = await DiscussionService.InviteUserToDiscussion(new NoPwDiscussionPreviewForm
// {
// InvitationCode = invitationCode,
// }, user.ID);
// if (!result.IsValid)
// return result;
//}
result.Data = (user, userSettings);
return result;