Owner decision 2026-10-04: fix the mismatches and every other mismatch of the same kind.
- One counting rule (Domain/Privacy/Counted), Mastodon's. It is used for a persona's statuses_count, its outbox
totalItems, NodeInfo localPosts and the instance status_count, which used to count four different things. It
counts every post that is neither deleted nor a DM, boosts included, and circle and located posts too (owner
decision). A group's count includes its remote members' posts.
- Users. Personas of banned or deleted roots no longer count, and are not found in search. NodeInfo now gives
activeMonth and activeHalfyear, and the v2 instance gives active_month instead of a constant 0.
- replies_count counts only public and unlisted replies, so it no longer tells anyone that a private reply exists.
Migration _012 recounts it.
- A remote account that deletes itself takes everything out of every count (GoneActors): its likes, downvotes,
reactions and poll votes go and their counters come back, as do its boosts', replies' and quotes' counts, and its
notifications. Lookups, account lists, search and favourited_by no longer show it. Migration _012 applies this to
accounts already gone.
- Deleting a post also deletes its pins and the local boosts of it.
- /stalking gives the same total as following_count. Members are still never listed, and hide_collections is now
always true, since the setting never did anything.
- Joining a community by invitation is following it, so /flock and /groupies agree; leaving unfollows.
- Search. Anyone may search, as on Mastodon; resolve and offset need a sign-in, offset pages, and deleted accounts
are never found.
- notifications/unread_count counts what the list shows, and the owner's follower and following lists page with
Link.
- The instance API advertises what is enforced:
- max_characters, now enforced with a 422;
- max_pinned_statuses = MaxPins;
- the media types and limits MediaService and MediaOptions accept;
- PollService's limits;
- the configured languages;
- no streaming URL until streaming exists.
domain_count counts the servers we have exchanged with; which ones stays unpublished (peers is empty).
- Routes Mastodon answers now answer instead of 404:
- directory, tags/{name}, timelines/link and identity_proofs;
- instance/languages, translation_languages, domain_blocks and privacy_policy;
- the v1 and v2 notification policy, and notification requests.
Also, from phase 3: a recovered password ends /clientapi sessions through a per-root SessionStamp claim instead of
comparing the JWT's whole-second nbf with the change time. That comparison let a token issued in the same second
survive, which made a test flaky.
671 tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
155 lines
5.4 KiB
C#
155 lines
5.4 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Infrastructure.Ids;
|
|
|
|
namespace PrivaPub.Models.User
|
|
{
|
|
public class Avatar : Entity
|
|
{
|
|
public string Url { get; set; }//url
|
|
public string Name { get; set; }//name
|
|
public string UserName { get; set; }//preferredUsername
|
|
public string Biography { get; set; }//summary
|
|
|
|
public Dictionary<string, string> SharedPersonalContacts { get; set; } = new();
|
|
public string PrivateKey { get; set; }
|
|
public string PublicKey { get; set; }
|
|
public AvatarAccountState AccountState { get; set; } = AvatarAccountState.Normal;
|
|
public AvatarSettings Settings { get; set; } = new();
|
|
public Dictionary<string, string> Fields { get; set; } = new();
|
|
|
|
public string Domain { get; set; }
|
|
public string PersonalNote { get; set; }
|
|
public string ModerationNote { get; set; }
|
|
|
|
public string InboxURL { get; set; }
|
|
public string OutboxURL { get; set; }
|
|
public string MovedToURL { get; set; }
|
|
public string PictureURL { get; set; }//icon
|
|
public string ThumbnailURL { get; set; }//image
|
|
public AvatarType AvatarType { get; set; } = AvatarType.Person;
|
|
public AvatarServer AvatarServer { get; set; } = AvatarServer.PrivaPub;
|
|
|
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
public DateTime PublishedOn { get; set; } = PrivacyIds.PublishedDay(DateTime.UtcNow);//the actor's published: a few days before CreatedAt, so same-day personas differ
|
|
public DateTime? SilencedAt { get; set; }
|
|
public DateTime? SuspendedAt { get; set; }
|
|
public DateTime? BannedAt { get; set; }
|
|
public DateTime? DeletionAt { get; set; }
|
|
|
|
public override object GenerateNewID() => PrivacyIds.ForDay(PublishedOn);
|
|
}
|
|
|
|
public class ForeignAvatar : Entity
|
|
{
|
|
public string Url { get; set; }//url
|
|
public string Name { get; set; }//name
|
|
public string UserName { get; set; }//preferredUsername
|
|
public string Biography { get; set; }//summary
|
|
|
|
public Dictionary<string, string> SharedPersonalContacts { get; set; } = new();
|
|
public string PrivateKey { get; set; }
|
|
public string PublicKey { get; set; }
|
|
public AvatarAccountState AccountState { get; set; } = AvatarAccountState.Normal;
|
|
public AvatarSettings Settings { get; set; } = new();
|
|
public Dictionary<string, string> Fields { get; set; } = new();
|
|
|
|
public string Domain { get; set; }
|
|
public string PersonalNote { get; set; }
|
|
public string ModerationNote { get; set; }
|
|
public bool IsDiscoverable { get; set; } = true;
|
|
public bool IsIndexable { get; set; }
|
|
public bool IsLocked { get; set; }//manuallyApprovesFollowers
|
|
public bool IsMemorial { get; set; }
|
|
public List<PrivaPub.Models.Post.CustomEmoji> Emojis { get; set; } = new();
|
|
public string PictureDescription { get; set; }
|
|
public string HeaderDescription { get; set; }
|
|
public DateTime? Published { get; set; }//the actor's own published
|
|
public List<string> Features { get; set; }//protocol extensions its document uses, for statistics (ActorFeatures)
|
|
|
|
public string ActorURI { get; set; }//id
|
|
public string PublicKeyId { get; set; }
|
|
public string FollowersURL { get; set; }
|
|
public string FollowingURL { get; set; }
|
|
public string SharedInboxURL { get; set; }
|
|
public string InboxURL { get; set; }
|
|
public string OutboxURL { get; set; }
|
|
public string MovedToURL { get; set; }
|
|
public string PictureURL { get; set; }//icon
|
|
public string ThumbnailURL { get; set; }//image
|
|
public AvatarType AvatarType { get; set; } = AvatarType.Person;
|
|
public AvatarServer AvatarServer { get; set; } = AvatarServer.Unknown;
|
|
|
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
public DateTime? SilencedAt { get; set; }
|
|
public DateTime? SuspendedAt { get; set; }
|
|
public DateTime? BannedAt { get; set; }
|
|
public DateTime? DeletionAt { get; set; }
|
|
public DateTime? ForgottenAt { get; set; }//its likes, votes, boosts and replies were taken out of our counts (GoneActors)
|
|
}
|
|
|
|
public class AvatarSettings
|
|
{
|
|
public string LanguageCode { get; set; } = "en-GB";
|
|
|
|
public bool IsDefault { get; set; } = true;
|
|
|
|
public short IconsThemeIndexColour { get; set; } = 25;
|
|
public short LightThemeIndexColour { get; set; } = 25;
|
|
public short DarkThemeIndexColour { get; set; } = 215;
|
|
public bool PreferSystemTheming { get; set; } = true;
|
|
public bool ThemeIsDarkMode { get; set; } = false;
|
|
public bool ThemeIsDarkGray { get; set; } = false;
|
|
public bool ThemeIsLightGray { get; set; } = true;
|
|
|
|
public bool IsLocked { get; set; }
|
|
public bool IsBot { get; set; }
|
|
public bool IsDiscoverable { get; set; } = true;
|
|
public bool IsIndexable { get; set; }
|
|
public bool HideCollections { get; set; } = true;
|
|
public string DefaultVisibility { get; set; } = "public";
|
|
public bool DefaultSensitive { get; set; }
|
|
public string DefaultLanguage { get; set; }
|
|
public string QuotePolicy { get; set; } = QuotePolicies.Public;//owner decision: anyone may quote a public post, automatically
|
|
}
|
|
|
|
public static class QuotePolicies
|
|
{
|
|
public const string Public = "public";
|
|
public const string Followers = "followers";
|
|
public const string Nobody = "nobody";
|
|
|
|
public static bool IsKnown(string value) => value is Public or Followers or Nobody;
|
|
}
|
|
|
|
public enum AvatarServer
|
|
{
|
|
Unknown,
|
|
Pleroma,
|
|
Mastodon,
|
|
Akkoma,
|
|
Misskey,
|
|
PrivaPub
|
|
}
|
|
|
|
public enum AvatarType
|
|
{
|
|
Application,
|
|
Group,
|
|
Organization,
|
|
Person,
|
|
Service
|
|
}
|
|
|
|
public enum AvatarAccountState
|
|
{
|
|
Normal,
|
|
Silenced,
|
|
Suspended,
|
|
Banned,
|
|
Deleted
|
|
}
|
|
}
|