Handle remote update activities.
This commit is contained in:
parent
dd97193311
commit
e3629af4da
|
@ -99,7 +99,7 @@ def update_changeset(struct, params \\ %{}) do
|
||||||
|> cast(params, [:bio, :name])
|
|> cast(params, [:bio, :name])
|
||||||
|> unique_constraint(:nickname)
|
|> unique_constraint(:nickname)
|
||||||
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|
||||||
|> validate_length(:bio, min: 1, max: 1000)
|
|> validate_length(:bio, min: 1, max: 5000)
|
||||||
|> validate_length(:name, min: 1, max: 100)
|
|> validate_length(:name, min: 1, max: 100)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -108,8 +108,8 @@ def upgrade_changeset(struct, params \\ %{}) do
|
||||||
|> cast(params, [:bio, :name, :info, :follower_address, :avatar])
|
|> cast(params, [:bio, :name, :info, :follower_address, :avatar])
|
||||||
|> unique_constraint(:nickname)
|
|> unique_constraint(:nickname)
|
||||||
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|
||||||
|> validate_length(:bio, min: 1, max: 1000)
|
|> validate_length(:bio, max: 5000)
|
||||||
|> validate_length(:name, min: 1, max: 100)
|
|> validate_length(:name, max: 100)
|
||||||
end
|
end
|
||||||
|
|
||||||
def password_update_changeset(struct, params) do
|
def password_update_changeset(struct, params) do
|
||||||
|
@ -218,6 +218,11 @@ def update_and_set_cache(changeset) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def invalidate_cache(user) do
|
||||||
|
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
|
||||||
|
Cachex.del(:user_cache, "nickname:#{user.nickname}")
|
||||||
|
end
|
||||||
|
|
||||||
def get_cached_by_ap_id(ap_id) do
|
def get_cached_by_ap_id(ap_id) do
|
||||||
key = "ap_id:#{ap_id}"
|
key = "ap_id:#{ap_id}"
|
||||||
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end)
|
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end)
|
||||||
|
|
|
@ -62,6 +62,16 @@ def accept(%{to: to, actor: actor, object: object} = params) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
|
||||||
|
local = !(params[:local] == false) # only accept false as false value
|
||||||
|
|
||||||
|
with data <- %{"to" => to, "cc" => cc, "type" => "Update", "actor" => actor, "object" => object},
|
||||||
|
{:ok, activity} <- insert(data, local),
|
||||||
|
:ok <- maybe_federate(activity) do
|
||||||
|
{:ok, activity}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
|
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
|
||||||
def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
|
def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
|
||||||
with nil <- get_existing_like(ap_id, object),
|
with nil <- get_existing_like(ap_id, object),
|
||||||
|
@ -260,34 +270,38 @@ def upload(file) do
|
||||||
Repo.insert(%Object{data: data})
|
Repo.insert(%Object{data: data})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_data_from_user_object(data) do
|
||||||
|
avatar = data["icon"]["url"] && %{
|
||||||
|
"type" => "Image",
|
||||||
|
"url" => [%{"href" => data["icon"]["url"]}]
|
||||||
|
}
|
||||||
|
|
||||||
|
banner = data["image"]["url"] && %{
|
||||||
|
"type" => "Image",
|
||||||
|
"url" => [%{"href" => data["image"]["url"]}]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_data = %{
|
||||||
|
ap_id: data["id"],
|
||||||
|
info: %{
|
||||||
|
"ap_enabled" => true,
|
||||||
|
"source_data" => data,
|
||||||
|
"banner" => banner
|
||||||
|
},
|
||||||
|
avatar: avatar,
|
||||||
|
nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",
|
||||||
|
name: data["name"],
|
||||||
|
follower_address: data["followers"],
|
||||||
|
bio: data["summary"]
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, user_data}
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_and_prepare_user_from_ap_id(ap_id) do
|
def fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||||
with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]),
|
with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]),
|
||||||
{:ok, data} <- Poison.decode(body)
|
{:ok, data} <- Poison.decode(body) do
|
||||||
do
|
user_data_from_user_object(data)
|
||||||
avatar = %{
|
|
||||||
"type" => "Image",
|
|
||||||
"url" => [%{"href" => data["icon"]["url"]}]
|
|
||||||
}
|
|
||||||
|
|
||||||
banner = %{
|
|
||||||
"type" => "Image",
|
|
||||||
"url" => [%{"href" => data["image"]["url"]}]
|
|
||||||
}
|
|
||||||
|
|
||||||
user_data = %{
|
|
||||||
ap_id: data["id"],
|
|
||||||
info: %{
|
|
||||||
"ap_enabled" => true,
|
|
||||||
"source_data" => data,
|
|
||||||
"banner" => banner
|
|
||||||
},
|
|
||||||
avatar: avatar,
|
|
||||||
nickname: "#{data["preferredUsername"]}@#{URI.parse(ap_id).host}",
|
|
||||||
name: data["name"],
|
|
||||||
follower_address: data["followers"],
|
|
||||||
}
|
|
||||||
|
|
||||||
{:ok, user_data}
|
|
||||||
else
|
else
|
||||||
e -> Logger.error("Could not user at fetch #{ap_id}, #{inspect(e)}")
|
e -> Logger.error("Could not user at fetch #{ap_id}, #{inspect(e)}")
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,7 +24,7 @@ def fix_object(object) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) when not is_nil(in_reply_to_id) do
|
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) when not is_nil(in_reply_to_id) do
|
||||||
case ActivityPub.fetch_object_from_id(object["inReplyToAtomUri"] || in_reply_to_id) do
|
case ActivityPub.fetch_object_from_id(in_reply_to_id) do
|
||||||
{:ok, replied_object} ->
|
{:ok, replied_object} ->
|
||||||
activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"])
|
activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"])
|
||||||
object
|
object
|
||||||
|
@ -117,6 +117,28 @@ def handle_incoming(%{"type" => "Announce", "object" => object_id, "actor" => ac
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_incoming(%{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} = data) do
|
||||||
|
with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
|
||||||
|
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
|
||||||
|
|
||||||
|
banner = new_user_data[:info]["banner"]
|
||||||
|
update_data = new_user_data
|
||||||
|
|> Map.take([:name, :bio, :avatar])
|
||||||
|
|> Map.put(:info, Map.merge(actor.info, %{"banner" => banner}))
|
||||||
|
|
||||||
|
actor
|
||||||
|
|> User.upgrade_changeset(update_data)
|
||||||
|
|> Repo.update
|
||||||
|
|
||||||
|
User.invalidate_cache(actor)
|
||||||
|
ActivityPub.update(%{local: false, to: data["to"] || [], cc: data["cc"] || [], object: object, actor: actor_id})
|
||||||
|
else
|
||||||
|
e ->
|
||||||
|
Logger.error(e)
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
# Accept
|
# Accept
|
||||||
# Undo
|
# Undo
|
||||||
|
|
|
@ -60,9 +60,9 @@ def ensure_keys_present(user) do
|
||||||
else
|
else
|
||||||
{:ok, pem} = Salmon.generate_rsa_pem
|
{:ok, pem} = Salmon.generate_rsa_pem
|
||||||
info = Map.put(info, "keys", pem)
|
info = Map.put(info, "keys", pem)
|
||||||
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
|
res = Repo.update(Ecto.Changeset.change(user, info: info))
|
||||||
Cachex.del(:user_cache, "nickname:#{user.nickname}")
|
User.invalidate_cache(user)
|
||||||
Repo.update(Ecto.Changeset.change(user, info: info))
|
res
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"http://mastodon.example.org/users/admin","type":"Person","following":"http://mastodon.example.org/users/admin/following","followers":"http://mastodon.example.org/users/admin/followers","inbox":"http://mastodon.example.org/users/admin/inbox","outbox":"http://mastodon.example.org/users/admin/outbox","preferredUsername":"admin","name":null,"summary":"\u003cp\u003e\u003c/p\u003e","url":"http://mastodon.example.org/@admin","manuallyApprovesFollowers":false,"publicKey":{"id":"http://mastodon.example.org/users/admin#main-key","owner":"http://mastodon.example.org/users/admin","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"http://mastodon.example.org/inbox"}}
|
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"http://mastodon.example.org/users/admin","type":"Person","following":"http://mastodon.example.org/users/admin/following","followers":"http://mastodon.example.org/users/admin/followers","inbox":"http://mastodon.example.org/users/admin/inbox","outbox":"http://mastodon.example.org/users/admin/outbox","preferredUsername":"admin","name":null,"summary":"\u003cp\u003e\u003c/p\u003e","url":"http://mastodon.example.org/@admin","manuallyApprovesFollowers":false,"publicKey":{"id":"http://mastodon.example.org/users/admin#main-key","owner":"http://mastodon.example.org/users/admin","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"http://mastodon.example.org/inbox"},"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}}
|
||||||
|
|
43
test/fixtures/mastodon-update.json
vendored
Normal file
43
test/fixtures/mastodon-update.json
vendored
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"type": "Update",
|
||||||
|
"object": {
|
||||||
|
"url": "http://mastodon.example.org/@gargron",
|
||||||
|
"type": "Person",
|
||||||
|
"summary": "<p>Some bio</p>",
|
||||||
|
"publicKey": {
|
||||||
|
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n",
|
||||||
|
"owner": "http://mastodon.example.org/users/gargron",
|
||||||
|
"id": "http://mastodon.example.org/users/gargron#main-key"
|
||||||
|
},
|
||||||
|
"preferredUsername": "gargron",
|
||||||
|
"outbox": "http://mastodon.example.org/users/gargron/outbox",
|
||||||
|
"name": "gargle",
|
||||||
|
"manuallyApprovesFollowers": false,
|
||||||
|
"inbox": "http://mastodon.example.org/users/gargron/inbox",
|
||||||
|
"id": "http://mastodon.example.org/users/gargron",
|
||||||
|
"following": "http://mastodon.example.org/users/gargron/following",
|
||||||
|
"followers": "http://mastodon.example.org/users/gargron/followers",
|
||||||
|
"endpoints": {
|
||||||
|
"sharedInbox": "http://mastodon.example.org/inbox"
|
||||||
|
},
|
||||||
|
"icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}
|
||||||
|
},
|
||||||
|
"id": "http://mastodon.example.org/users/gargron#updates/1519563538",
|
||||||
|
"actor": "http://mastodon.example.org/users/gargron",
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"ostatus": "http://ostatus.org#",
|
||||||
|
"movedTo": "as:movedTo",
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||||
|
"conversation": "ostatus:conversation",
|
||||||
|
"atomUri": "ostatus:atomUri",
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"Emoji": "toot:Emoji"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -41,24 +41,6 @@ test "it fetches replied-to activities if we don't have them" do
|
||||||
assert returned_activity.data["object"]["inReplyToStatusId"] == activity.id
|
assert returned_activity.data["object"]["inReplyToStatusId"] == activity.id
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works if the activity id isn't an url but an atom-uri is given" do
|
|
||||||
data = File.read!("test/fixtures/mastodon-post-activity.json")
|
|
||||||
|> Poison.decode!
|
|
||||||
|
|
||||||
object = data["object"]
|
|
||||||
|> Map.put("inReplyToAtomUri", "https://shitposter.club/notice/2827873")
|
|
||||||
|> Map.put("inReplyTo", "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment")
|
|
||||||
|
|
||||||
data = data
|
|
||||||
|> Map.put("object", object)
|
|
||||||
|
|
||||||
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
|
|
||||||
|
|
||||||
assert activity = Activity.get_create_activity_by_object_ap_id("tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment")
|
|
||||||
assert returned_activity.data["object"]["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
|
|
||||||
assert returned_activity.data["object"]["inReplyToStatusId"] == activity.id
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it works for incoming notices" do
|
test "it works for incoming notices" do
|
||||||
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!
|
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!
|
||||||
|
|
||||||
|
@ -144,6 +126,28 @@ test "it works for incoming announces with an existing activity" do
|
||||||
|
|
||||||
assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id
|
assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it works for incoming update activities" do
|
||||||
|
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!
|
||||||
|
|
||||||
|
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||||
|
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!
|
||||||
|
object = update_data["object"]
|
||||||
|
|> Map.put("actor", data["actor"])
|
||||||
|
|> Map.put("id", data["actor"])
|
||||||
|
|
||||||
|
update_data = update_data
|
||||||
|
|> Map.put("actor", data["actor"])
|
||||||
|
|> Map.put("object", object)
|
||||||
|
|
||||||
|
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(data["actor"])
|
||||||
|
assert user.name == "gargle"
|
||||||
|
assert user.avatar["url"] == [%{"href" => "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"}]
|
||||||
|
assert user.info["banner"]["url"] == [%{"href" => "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}]
|
||||||
|
assert user.bio == "<p>Some bio</p>"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "prepare outgoing" do
|
describe "prepare outgoing" do
|
||||||
|
|
Loading…
Reference in a new issue