Artwork Inbox
![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
![[community profile]](https://www.dreamwidth.org/img/silk/identity/community.png)

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.

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
- 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.