isaacschemm: Drawing of myself as a snail (snail)
[personal profile] isaacschemm posting in [community profile] snailsharp
Ascreenshot of the Artwork Inbox home page

Artwork Inbox is a web app I developed for my own use a few years ago. The idea behind Artwork Inbox is to let the user (a.k.a. me) view a social media feed in a way that separates the presentation of visual art (such as drawings or photographs), from other types of posts (like journal or status updates). Instead of simply sorting all posts by date and time, the app splits the feed into pages of 200 items each, and groups those items - first by type (visual or text) and then by author/artist. The current version is built on ASP.NET Core 7, with EF Core, Identity, and Cosmos DB.

A lot of artists don't post their work to art-specific sites anymore; many of them post exclusively to social networks like Tumblr or Twitter. I often want to like the social media sites that other people use, but the way different topics and media types are mixed together in the timeline has been a major barrier. Seeing emotionally charged posts about real-world issues isn't always an issue for me, but seeing them without that already having that context in mind, and between unrelated posts in an infinitely scrolling timeline, certainly can be. With this in mind, there are a few things Artwork Inbox does to help add some cognitive distance between myself and the content:

  • Posts with visual art (drawings or photographs) are clearly separated from posts with text content on each page of results.
  • Text posts are limited to a single line in the browser viewport, with an ellipsis (...) if necessary. Like visual posts' thumbnails, they are rendered as links which let you view the rest of the post in a new browser tab.
  • Reposts (when a user you follow forwards a post from another user) can be globally hidden.
  • Feeds do not scroll indefinitely; they are split into pages of up to 200 items each, and you can mark the point in the feed at which you've "read" everything, so that the next time you view the feed, it will stop there.
A screenshot of an Artwork Inbox feed page

It's not designed to scale up; it's designed to cost me as little money as possible if I don't use it, which might explain some odd design decisions.

Providers

At its core, Artwork Inbox is built on the standard ASP.NET Core MVC template, and it uses its ASP.NET Identity account system and the corresponding OAuth integration. I've disabled local account support (username/password), so at least one external account is required to use the app, but you can hook up as many as you like, then use any to log in - almost all of that is handled by Microsoft code. The providers I currently have implemented are:

  • DeviantArt
  • Tumblr
  • Twitter
  • Reddit
  • Mastodon

(I've only added three Mastodon instances so far; the way Identity works, it's a lot easier for me to add a new Mastodon instance as a separate auth provider than to implement automatic client ID creation.)

Once you're logged in, you can also add a few additional sources:

  • FurAffinity (by extracting browser cookies; uses FAExport)
  • Weasyl (by entering an API key)
  • RSS or Atom feeds

Implementation

Each source type (DeviantArt, Tumblr, etc.) has an associated controller class which implements the base class SourceController. (If you have multiple Mastodon accounts, their feeds will be combined into one, handled by a single MastodonController; the same goes for multiple RSS or Atom feeds.) Each class needs to implement certain methods:

  • GetSourceAsync: creates an object that provides access to the source (the object's constructor usually takes a credentials object). Methods on this object include:
    • GetAuthenticatedUserAsync: fetches the name and avatar of the logged-in user.
    • GetFeedItemsAsync: fetches items from the feed using IAsyncEnumerable<T>, starting with the newest and proceeding until there are no items left (or until Artwork Inbox stops asking for them).
    • GetNotificationCountAsync: gets the number of unread notifications, if possible.
  • GetLastReadAsync: returns the date/time at which the user last clicked the Artwork Inbox "mark all as read" button. Artwork Inbox does not store feed items in its own database, but it does record this timestamp; items older than this point in time will be omitted from the feed, and the feed will end there. (This does not affect the original source website - most sites' APIs do not allow an app like this to clear notifications, and it's easier if Artwork Inbox can work with read-only API tokens anyway.)

The controller's Feed action acquires the IAsyncEnumerable<T> from GetFeedItemsAsync, applies a wrapper so its results are cached in memory, and renders the first 200 items, showing a "next page" button if necessary. This IAsyncEnumerable<T> is then placed in an IMemoryCache for 15 minutes, tied to a key placed in the browser URL (this lets Artwork Inbox handle the "next page" button smoothly without relying on copying items to the database; the same IAsyncEnumerable<T> is retrieved, the first 200 items, which have been cached, are skipped, and the next 200 items are then fetched). The downside to this approach is that the user's subsequent requests need to go to the same server, because if they hit a different server (or the server restarts), it'll have to fetch all items starting from the beginning of the feed again; this shouldn't be a huge issue, however, since even if the app were scaled out in Azure, an affinity cookie could be used for this.

Backend

Entity Framework Core is used for databse access, and besides the standard ASP.NET Identity tables, I've also added a few options to the ApplicationUser table (whether to hide reposts, and whether to skip or hide thumbnails for "mature" posts), a table to track the URLs of Atom and RSS feeds a user subscribes to, and new tables to store tokens for some of the providers to use with their APIs. In some cases, this is just an access token or API key; in others, a token and secret are both stored. For DeviantArt, a refresh token is stored and a class called DeviantArtTokenWrapper handles storing new tokens when the refresh happens, by implementing a fancy interface I made back when I wrote DeviantArtFs.

The database was a basic Azure SQL Server database for a long time, but now it's built on Azure Cosmos DB; I updated to .NET 7 and made a couple of changes to get it working with EF Core. First, I needed to tell it which fields Identity was using as concurrency stamps:

protected override void OnModelCreating(ModelBuilder builder) {
    base.OnModelCreating(builder);
    builder.Entity<IdentityRole>()
        .Property(b => b.ConcurrencyStamp)
        .IsETagConcurrency();
    builder.Entity<ApplicationUser>()
        .Property(b => b.ConcurrencyStamp)
        .IsETagConcurrency();
}

I also needed to implement a replacement for AnyAsync, which doesn't seem to be implemented (yet):

public static async Task<bool> ListAnyAsync<T>(this IQueryable<T> q) {
    var list = await q.Take(1).ToListAsync();
    return list.Any();
}

If you have any questions about Artwork Inbox, honestly the best place to get it touch with me about it is on the GitHub repository.

Tags:

Snail#

A programming blog where the gimmick is that I pretend to be a snail.

Expand Cut Tags

No cut tags

Style Credit

Page generated Jun. 14th, 2025 04:28 am
Powered by Dreamwidth Studios