Merge branch 'develop' into feature/activitypub

This commit is contained in:
lain 2018-02-12 10:24:15 +01:00
commit b331cb449a
36 changed files with 119 additions and 95 deletions

View File

@ -21,4 +21,4 @@ before_script:
unit-testing:
stage: test
script:
- MIX_ENV=test mix test
- MIX_ENV=test mix test --trace

View File

@ -32,7 +32,7 @@ def update_stats do
domain_count = Enum.count(peers)
status_query = from(u in User.local_user_query,
select: fragment("sum((?->>'note_count')::int)", u.info))
status_count = Repo.one(status_query) |> IO.inspect
status_count = Repo.one(status_query)
user_count = Repo.aggregate(User.local_user_query, :count, :id)
Agent.update(__MODULE__, fn _ ->
{peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}}

View File

@ -10,6 +10,8 @@ def to_simple_form(user, activities, _users) do
h = fn(str) -> [to_charlist(str)] end
last_activity = List.last(activities)
entries = activities
|> Enum.map(fn(activity) ->
{:entry, ActivityRepresenter.to_simple_form(activity, user)}
@ -32,7 +34,15 @@ def to_simple_form(user, activities, _users) do
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'], []},
{:author, UserRepresenter.to_simple_form(user)},
] ++ entries
] ++
if last_activity do
[{:link, [rel: 'next',
href: to_charlist(OStatus.feed_path(user)) ++ '?max_id=' ++ to_charlist(last_activity.id),
type: 'application/atom+xml'], []}]
else
[]
end
++ entries
}]
end
end

View File

@ -19,7 +19,7 @@ def feed_redirect(conn, %{"nickname" => nickname} = params) do
end
end
def feed(conn, %{"nickname" => nickname}) do
def feed(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname)
query = from activity in Activity,
where: fragment("?->>'actor' = ?", activity.data, ^user.ap_id),
@ -27,6 +27,7 @@ def feed(conn, %{"nickname" => nickname}) do
order_by: [desc: :id]
activities = query
|> restrict_max(params)
|> Repo.all
response = user
@ -56,6 +57,11 @@ defp decode_or_retry(body) do
end
end
defp restrict_max(query, %{"max_id" => max_id}) do
from activity in query, where: activity.id < ^max_id
end
defp restrict_max(query, _), do: query
def salmon_incoming(conn, _) do
{:ok, body, _conn} = read_body(conn)
{:ok, doc} = decode_or_retry(body)

View File

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.b3deb1dd44970d86cc6b368f36fd09d9.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.15dfe939c498cca9840c.js></script><script type=text/javascript src=/static/js/vendor.409059e5a814f448f5bc.js></script><script type=text/javascript src=/static/js/app.30c01d7540d43b760f03.js></script></body></html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.b3deb1dd44970d86cc6b368f36fd09d9.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.f8637fb66085728d1fbd.js></script><script type=text/javascript src=/static/js/vendor.6ceb4478f308829a2829.js></script><script type=text/javascript src=/static/js/app.23c1437e78f655a31c4a.js></script></body></html>

View File

@ -0,0 +1,4 @@
<div style="margin-left:12px; margin-right:12px">
<p>This is a <a href="https://pleroma.social" target="_blank">Pleroma</a> instance.</p>
</div>

View File

@ -3,5 +3,6 @@
"background": "/static/bg.jpg",
"logo": "/static/logo.png",
"defaultPath": "/main/all",
"chatDisabled": false
"chatDisabled": false,
"showInstanceSpecificPanel": false
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -50,7 +50,7 @@ test "it returns error if the notification doesn't belong to the user" do
{:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, notification} = Notification.get(user, notification.id)
{:error, _notification} = Notification.get(user, notification.id)
end
end
@ -72,7 +72,7 @@ test "it returns error if the notification doesn't belong to the user" do
{:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, notification} = Notification.dismiss(user, notification.id)
{:error, _notification} = Notification.dismiss(user, notification.id)
end
end

View File

@ -16,7 +16,7 @@ test "it ensures uniqueness of the id" do
cs = Object.change(%Object{}, %{data: %{id: object.data["id"]}})
assert cs.valid?
{:error, result} = Repo.insert(cs)
{:error, _result} = Repo.insert(cs)
end
end
end

View File

@ -0,0 +1,6 @@
defmodule Pleroma.Web.OStatusMock do
import Pleroma.Factory
def handle_incoming(_doc) do
insert(:note_activity)
end
end

View File

@ -326,7 +326,7 @@ test "get recipients from activity" do
assert [addressed] == User.get_recipients_from_activity(activity)
{:ok, user} = User.follow(user, actor)
{:ok, user_two} = User.follow(user_two, actor)
{:ok, _user_two} = User.follow(user_two, actor)
recipients = User.get_recipients_from_activity(activity)
assert length(recipients) == 2
assert user in recipients

View File

@ -123,7 +123,7 @@ test "doesn't return blocked activities" do
describe "public fetch activities" do
test "retrieves public activities" do
activities = ActivityPub.fetch_public_activities
_activities = ActivityPub.fetch_public_activities
%{public: public} = ActivityBuilder.public_and_non_public

View File

@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Pleroma.Web.{OStatus, CommonAPI}
import Pleroma.Factory
import ExUnit.CaptureLog
test "the home timeline", %{conn: conn} do
user = insert(:user)
@ -31,23 +32,25 @@ test "the home timeline", %{conn: conn} do
test "the public timeline", %{conn: conn} do
following = insert(:user)
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"})
{:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
capture_log fn ->
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"})
{:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn = conn
|> get("/api/v1/timelines/public", %{"local" => "False"})
conn = conn
|> get("/api/v1/timelines/public", %{"local" => "False"})
assert length(json_response(conn, 200)) == 2
assert length(json_response(conn, 200)) == 2
conn = build_conn()
|> get("/api/v1/timelines/public", %{"local" => "True"})
conn = build_conn()
|> get("/api/v1/timelines/public", %{"local" => "True"})
assert [%{"content" => "test"}] = json_response(conn, 200)
assert [%{"content" => "test"}] = json_response(conn, 200)
conn = build_conn()
|> get("/api/v1/timelines/public", %{"local" => "1"})
conn = build_conn()
|> get("/api/v1/timelines/public", %{"local" => "1"})
assert [%{"content" => "test"}] = json_response(conn, 200)
assert [%{"content" => "test"}] = json_response(conn, 200)
end
end
test "posting a status", %{conn: conn} do
@ -144,7 +147,7 @@ test "list of notifications", %{conn: conn} do
other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, [_notification]} = Notification.create_notifications(activity)
conn = conn
|> assign(:user, user)
@ -190,7 +193,7 @@ test "clearing all notifications", %{conn: conn} do
other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, [_notification]} = Notification.create_notifications(activity)
conn = conn
|> assign(:user, user)
@ -338,15 +341,16 @@ test "media upload", %{conn: conn} do
test "hashtag timeline", %{conn: conn} do
following = insert(:user)
{:ok, activity} = TwitterAPI.create_status(following, %{"status" => "test #2hu"})
{:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
capture_log fn ->
{:ok, activity} = TwitterAPI.create_status(following, %{"status" => "test #2hu"})
{:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn = conn
|> get("/api/v1/timelines/tag/2hu")
conn = conn
|> get("/api/v1/timelines/tag/2hu")
assert [%{"id" => id}] = json_response(conn, 200)
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(activity.id)
assert id == to_string(activity.id)
end
end
test "getting followers", %{conn: conn} do
@ -381,14 +385,14 @@ test "following / unfollowing a user", %{conn: conn} do
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/follow")
assert %{"id" => id, "following" => true} = json_response(conn, 200)
assert %{"id" => _id, "following" => true} = json_response(conn, 200)
user = Repo.get(User, user.id)
conn = build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unfollow")
assert %{"id" => id, "following" => false} = json_response(conn, 200)
assert %{"id" => _id, "following" => false} = json_response(conn, 200)
user = Repo.get(User, user.id)
conn = build_conn()
@ -407,14 +411,14 @@ test "blocking / unblocking a user", %{conn: conn} do
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/block")
assert %{"id" => id, "blocking" => true} = json_response(conn, 200)
assert %{"id" => _id, "blocking" => true} = json_response(conn, 200)
user = Repo.get(User, user.id)
conn = build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unblock")
assert %{"id" => id, "blocking" => false} = json_response(conn, 200)
assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
end
test "getting a list of blocks", %{conn: conn} do
@ -461,7 +465,7 @@ test "unimplemented mutes, follow_requests, blocks, domain blocks" do
test "account search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
_user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
conn = conn
@ -495,12 +499,14 @@ test "search", %{conn: conn} do
end
test "search fetches remote statuses", %{conn: conn} do
conn = conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
assert results = json_response(conn, 200)
capture_log fn ->
conn = conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
assert results = json_response(conn, 200)
[status] = results["statuses"]
assert status["uri"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
[status] = results["statuses"]
assert status["uri"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
end
end
test "search fetches remote accounts", %{conn: conn} do
@ -530,7 +536,7 @@ test "returns the favorites of a user", %{conn: conn} do
end
describe "updating credentials" do
test "updates the user's bio" do
test "updates the user's bio", %{conn: conn} do
user = insert(:user)
conn = conn
@ -541,7 +547,7 @@ test "updates the user's bio" do
assert user["note"] == "I drink #cofe"
end
test "updates the user's name" do
test "updates the user's name", %{conn: conn} do
user = insert(:user)
conn = conn
@ -552,7 +558,7 @@ test "updates the user's name" do
assert user["display_name"] == "markorepairs"
end
test "updates the user's avatar" do
test "updates the user's avatar", %{conn: conn} do
user = insert(:user)
new_avatar = %Plug.Upload{content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg"}
@ -565,7 +571,7 @@ test "updates the user's avatar" do
assert user["avatar"] != "https://placehold.it/48x48"
end
test "updates the user's banner" do
test "updates the user's banner", %{conn: conn} do
user = insert(:user)
new_header = %Plug.Upload{content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg"}
@ -579,7 +585,7 @@ test "updates the user's banner" do
end
end
test "get instance information" do
test "get instance information", %{conn: conn} do
insert(:user, %{local: true})
user = insert(:user, %{local: true})
insert(:user, %{local: false})

View File

@ -96,7 +96,7 @@ test "an announce activity" do
user = insert(:user)
object = Object.get_cached_by_ap_id(note.data["object"]["id"])
{:ok, announce, object} = ActivityPub.announce(user, object)
{:ok, announce, _object} = ActivityPub.announce(user, object)
announce = Repo.get(Activity, announce.id)

View File

@ -33,6 +33,7 @@ test "returns a feed of the last 20 items of the user" do
<author>
#{user_xml}
</author>
<link rel="next" href="#{OStatus.feed_path(user)}?max_id=#{note_activity.id}" type="application/atom+xml" />
<entry>
#{entry_xml}
</entry>

View File

@ -12,7 +12,7 @@ test "it removes the mentioned activity" do
user = insert(:user)
object = Object.get_by_ap_id(note.data["object"]["id"])
{:ok, like, object} = Pleroma.Web.ActivityPub.ActivityPub.like(user, object)
{:ok, like, _object} = Pleroma.Web.ActivityPub.ActivityPub.like(user, object)
incoming = File.read!("test/fixtures/delete.xml")
|> String.replace("tag:mastodon.sdf.org,2017-06-10:objectId=310513:objectType=Status", note.data["object"]["id"])

View File

@ -84,10 +84,3 @@ test "gets a notice", %{conn: conn} do
assert response(conn, 200)
end
end
defmodule Pleroma.Web.OStatusMock do
import Pleroma.Factory
def handle_incoming(_doc) do
insert(:note_activity)
end
end

View File

@ -4,6 +4,7 @@ defmodule Pleroma.Web.OStatusTest do
alias Pleroma.Web.XML
alias Pleroma.{Object, Repo, User, Activity}
import Pleroma.Factory
import ExUnit.CaptureLog
test "don't insert create notes twice" do
incoming = File.read!("test/fixtures/incoming_note_activity.xml")
@ -91,7 +92,7 @@ test "handle incoming notes - Mastodon, with CW" do
test "handle incoming retweets - Mastodon, with CW" do
incoming = File.read!("test/fixtures/cw_retweet.xml")
{:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
{:ok, [[_activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
assert retweeted_activity.data["object"]["summary"] == "Hey."
end
@ -168,19 +169,21 @@ test "handle incoming retweets - Mastodon, salmon" do
end
test "handle incoming favorites - GS, websub" do
incoming = File.read!("test/fixtures/favorite.xml")
{:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming)
capture_log fn ->
incoming = File.read!("test/fixtures/favorite.xml")
{:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming)
assert activity.data["type"] == "Like"
assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
assert activity.data["object"] == favorited_activity.data["object"]["id"]
assert activity.data["id"] == "tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00"
assert activity.data["type"] == "Like"
assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
assert activity.data["object"] == favorited_activity.data["object"]["id"]
assert activity.data["id"] == "tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00"
refute activity.local
assert favorited_activity.data["type"] == "Create"
assert favorited_activity.data["actor"] == "https://shitposter.club/user/1"
assert favorited_activity.data["object"]["id"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
refute favorited_activity.local
refute activity.local
assert favorited_activity.data["type"] == "Create"
assert favorited_activity.data["actor"] == "https://shitposter.club/user/1"
assert favorited_activity.data["object"]["id"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
refute favorited_activity.local
end
end
test "handle conversation references" do
@ -335,11 +338,13 @@ test "it works with the uri" do
describe "fetching a status by it's HTML url" do
test "it builds a missing status from an html url" do
url = "https://shitposter.club/notice/2827873"
{:ok, [activity] } = OStatus.fetch_activity_from_url(url)
capture_log fn ->
url = "https://shitposter.club/notice/2827873"
{:ok, [activity] } = OStatus.fetch_activity_from_url(url)
assert activity.data["actor"] == "https://shitposter.club/user/1"
assert activity.data["object"]["id"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
assert activity.data["actor"] == "https://shitposter.club/user/1"
assert activity.data["object"]["id"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
end
end
test "it works for atom notes, too" do

View File

@ -35,7 +35,7 @@ test "it encodes a magic key from a public key" do
end
test "it decodes a friendica public key" do
key = Salmon.decode_key(@magickey_friendica)
_key = Salmon.decode_key(@magickey_friendica)
end
test "returns a public and private key from a pem" do
@ -90,7 +90,7 @@ test "it pushes an activity to remote accounts it's addressed to" do
user = Repo.get_by(User, ap_id: activity.data["actor"])
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
poster = fn (url, data, headers, options) ->
poster = fn (url, _data, _headers, _options) ->
assert url == "http://example.org/salmon"
end
Salmon.publish(user, activity, poster)

View File

@ -147,7 +147,7 @@ test "an undo for a follow" do
follower = insert(:user)
followed = insert(:user)
{:ok, follow} = ActivityPub.follow(follower, followed)
{:ok, _follow} = ActivityPub.follow(follower, followed)
{:ok, unfollow} = ActivityPub.unfollow(follower, followed)
map = ActivityRepresenter.to_map(unfollow, %{user: follower})

View File

@ -504,7 +504,7 @@ test "it returns a user's followers", %{conn: conn} do
user = insert(:user)
follower_one = insert(:user)
follower_two = insert(:user)
not_follower = insert(:user)
_not_follower = insert(:user)
{:ok, follower_one} = User.follow(follower_one, user)
{:ok, follower_two} = User.follow(follower_two, user)
@ -522,7 +522,7 @@ test "it returns the logged in user's friends", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
@ -538,7 +538,7 @@ test "it returns a given user's friends with user_id", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
@ -553,7 +553,7 @@ test "it returns a given user's friends with screen_name", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
@ -570,7 +570,7 @@ test "it returns a user's friends", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
@ -585,7 +585,7 @@ test "it returns a user's friends", %{conn: conn} do
end
describe "POST /api/account/update_profile.json" do
test "it updates a user's profile" do
test "it updates a user's profile", %{conn: conn} do
user = insert(:user)
conn = conn
@ -627,7 +627,7 @@ test "it returns search results", %{conn: conn} do
end
describe "GET /api/statusnet/tags/timeline/:tag.json" do
test "it returns the tags timeline" do
test "it returns the tags timeline", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})

View File

@ -247,7 +247,7 @@ test "Unblock another user using user_id" do
user = insert(:user)
User.block(user, unblocked)
{:ok, user, unblocked} = TwitterAPI.unblock(user, %{"user_id" => unblocked.id})
{:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"user_id" => unblocked.id})
assert user.info["blocks"] == []
end
@ -256,7 +256,7 @@ test "Unblock another user using screen_name" do
user = insert(:user)
User.block(user, unblocked)
{:ok, user, unblocked} = TwitterAPI.unblock(user, %{"screen_name" => unblocked.nickname})
{:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"screen_name" => unblocked.nickname})
assert user.info["blocks"] == []
end

View File

@ -130,7 +130,7 @@ test "A user that follows you", %{user: user} do
assert represented == UserView.render("show.json", %{user: follower, for: user})
end
test "A blocked user for the blocker", %{user: user} do
test "A blocked user for the blocker" do
user = insert(:user)
blocker = insert(:user)
User.block(blocker, user)

View File

@ -42,8 +42,7 @@ test "returns the info for a user" do
test "it works for friendica" do
user = "lain@squeet.me"
{:ok, data} = WebFinger.finger(user)
{:ok, _data} = WebFinger.finger(user)
end
test "it gets the xrd endpoint" do

View File

@ -74,10 +74,3 @@ test "rejects incoming feed updates with the wrong signature", %{conn: conn} do
assert length(Repo.all(Activity)) == 0
end
end
defmodule Pleroma.Web.OStatusMock do
import Pleroma.Factory
def handle_incoming(_doc) do
insert(:note_activity)
end
end

View File

@ -174,7 +174,7 @@ test "sign a text" do
signed = Websub.sign("secret", "text")
assert signed == "B8392C23690CCF871F37EC270BE1582DEC57A503" |> String.downcase
signed = Websub.sign("secret", [[""], ['']])
_signed = Websub.sign("secret", [[""], ['']])
end
describe "renewing subscriptions" do
@ -184,7 +184,7 @@ test "it renews subscriptions that have less than a day of time left" do
still_good = insert(:websub_client_subscription, %{valid_until: NaiveDateTime.add(now, 2 * day), topic: "http://example.org/still_good", state: "accepted"})
needs_refresh = insert(:websub_client_subscription, %{valid_until: NaiveDateTime.add(now, day - 100), topic: "http://example.org/needs_refresh", state: "accepted"})
refresh = Websub.refresh_subscriptions()
_refresh = Websub.refresh_subscriptions()
assert still_good == Repo.get(WebsubClientSubscription, still_good.id)
refute needs_refresh == Repo.get(WebsubClientSubscription, needs_refresh.id)