The Mastodon client API: accounts, statuses, timelines, notifications, search
With a token for a persona, a Mastodon client can now:
- accounts: verify/update credentials (display name, note, fields, locked,
bot, discoverable, indexable, hide collections, posting defaults; the
change federates as Update{Person}), get, lookup, statuses (paged, by
visibility to the viewer), relationships, search, follow and unfollow,
follow requests (authorize and reject answer remote followers with
Accept or Reject), remove from followers. Followers and following lists
are shown to their owner only.
- statuses: post (plain text, mentions, hashtags, replies, content
warnings, the four visibilities, Idempotency-Key), get, edit (PUT),
delete returning the source for redrafting, context, history, source,
favourite, reblog and their undos, favourited_by and reblogged_by.
- timelines: home, public (local or remote), tag; favourites;
conversations; markers.
- notifications: list with types and exclude_types, get, dismiss, clear,
unread count.
- /api/v2/search, resolving a handle or a URL to an account or a post.
Media, polls, pins, bookmarks, mutes, blocks, lists, filters, trends and
push answer empty lists or a 422 saying they are not supported yet, so
clients degrade instead of failing. Public reads work without a token.
Persona settings (locked, bot, indexable, discoverable) now also shape
the ActivityPub actor.
Fixed on the way: three conditional expressions whose `default` was the
value type's, so a missing limit became 1, a missing flag became false and
an attachment without dimensions became 0x0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
b54cdf78b2
commit
0028e96e76
17 files changed
+1290
-73
No files matched your search
@@ -0,0 +1,82 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
using PrivaPub.Api.Mastodon.Auth;
|
||||
using PrivaPub.Api.Mastodon.Infrastructure;
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace PrivaPub.Tests.Api
|
||||
{
|
||||
public class MastodonParamsTests
|
||||
{
|
||||
static async Task<MastodonParams> Read(string query = "", string contentType = default, string body = default)
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.QueryString = new QueryString(query);
|
||||
if (body != default)
|
||||
{
|
||||
context.Request.ContentType = contentType;
|
||||
var bytes = Encoding.UTF8.GetBytes(body);
|
||||
context.Request.Body = new MemoryStream(bytes);
|
||||
context.Request.ContentLength = bytes.Length;
|
||||
}
|
||||
return await MastodonParams.Read(context.Request, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Absent_values_are_null_not_false_or_zero()
|
||||
{
|
||||
var parameters = await Read();
|
||||
|
||||
Assert.Null(parameters.Bool("sensitive"));
|
||||
Assert.Null(parameters.Int("limit"));
|
||||
Assert.Null(parameters.Get("status"));
|
||||
Assert.Empty(parameters.List("media_ids"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reads_rails_style_arrays_from_the_query()
|
||||
{
|
||||
var parameters = await Read("?id[]=1&id[]=2&limit=5&local=true");
|
||||
|
||||
Assert.Equal(new[] { "1", "2" }, parameters.List("id"));
|
||||
Assert.Equal(5, parameters.Int("limit"));
|
||||
Assert.True(parameters.Bool("local"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reads_forms_and_nested_keys()
|
||||
{
|
||||
var parameters = await Read(contentType: "application/x-www-form-urlencoded",
|
||||
body: "status=hi&media_ids[]=a&media_ids[]=b&poll[options][]=x&poll[options][]=y&sensitive=0");
|
||||
|
||||
Assert.Equal("hi", parameters.Get("status"));
|
||||
Assert.Equal(new[] { "a", "b" }, parameters.List("media_ids"));
|
||||
Assert.Equal(new[] { "x", "y" }, parameters.List("poll[options]"));
|
||||
Assert.False(parameters.Bool("sensitive"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reads_json_bodies_the_same_way()
|
||||
{
|
||||
var parameters = await Read(contentType: "application/json",
|
||||
body: """{"status":"hi","media_ids":["a","b"],"poll":{"options":["x","y"],"expires_in":300},"sensitive":true,"source":{"privacy":"unlisted"}}""");
|
||||
|
||||
Assert.Equal(new[] { "a", "b" }, parameters.List("media_ids"));
|
||||
Assert.Equal(new[] { "x", "y" }, parameters.List("poll[options]"));
|
||||
Assert.Equal(300, parameters.Int("poll[expires_in]"));
|
||||
Assert.True(parameters.Bool("sensitive"));
|
||||
Assert.Equal("unlisted", parameters.Get("source[privacy]"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new[] { "read" }, "read:statuses", true)]
|
||||
[InlineData(new[] { "read:statuses" }, "read:statuses", true)]
|
||||
[InlineData(new[] { "read:accounts" }, "read:statuses", false)]
|
||||
[InlineData(new[] { "write" }, "read:statuses", false)]
|
||||
[InlineData(new[] { "follow" }, "write:follows", true)]
|
||||
[InlineData(new[] { "follow" }, "write:statuses", false)]
|
||||
public void Scopes_grant_like_mastodons(string[] granted, string required, bool expected) =>
|
||||
Assert.Equal(expected, MastodonScopes.Grants(granted, required));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user