diff options
19 files changed, 498 insertions, 1 deletions
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex new file mode 100644 index 000000000..b61bc4c0e --- /dev/null +++ b/lib/pleroma/chat.ex @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Chat do + use Ecto.Schema + import Ecto.Changeset + + alias Pleroma.User + alias Pleroma.Repo + + @moduledoc """ + Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet). + + It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages. + """ + + schema "chats" do + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + field(:recipient, :string) + field(:unread, :integer, default: 0, read_after_writes: true) + + timestamps() + end + + def creation_cng(struct, params) do + struct + |> cast(params, [:user_id, :recipient, :unread]) + |> validate_required([:user_id, :recipient]) + |> unique_constraint(:user_id, name: :chats_user_id_recipient_index) + end + + def get(user_id, recipient) do + __MODULE__ + |> Repo.get_by(user_id: user_id, recipient: recipient) + end + + def bump_or_create(user_id, recipient) do + %__MODULE__{} + |> creation_cng(%{user_id: user_id, recipient: recipient, unread: 1}) + |> Repo.insert( + on_conflict: [set: [updated_at: NaiveDateTime.utc_now()], inc: [unread: 1]], + conflict_target: [:user_id, :recipient] + ) + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 19286fd01..0b4892501 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -397,6 +397,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end + # TODO: Is this even used now? # TODO: This is weird, maybe we shouldn't check here if we can make the activity. @spec like(User.t(), Object.t(), String.t() | nil, boolean()) :: {:ok, Activity.t(), Object.t()} | {:error, any()} diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 429a510b8..f0a6c1e1b 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -10,6 +10,28 @@ defmodule Pleroma.Web.ActivityPub.Builder do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + def create(actor, object_id, recipients) do + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "actor" => actor.ap_id, + "to" => recipients, + "object" => object_id, + "type" => "Create" + }, []} + end + + def chat_message(actor, recipient, content) do + {:ok, + %{ + "id" => Utils.generate_object_id(), + "actor" => actor.ap_id, + "type" => "ChatMessage", + "to" => [recipient], + "content" => content + }, []} + end + @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()} def like(actor, object) do object_actor = User.get_cached_by_ap_id(object.data["actor"]) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index dc4bce059..49cc72561 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -12,18 +12,46 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) def validate(%{"type" => "Like"} = object, meta) do with {:ok, object} <- - object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do + object + |> LikeValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object |> Map.from_struct()) {:ok, object, meta} end end + def validate(%{"type" => "ChatMessage"} = object, meta) do + with {:ok, object} <- + object + |> ChatMessageValidator.cast_and_apply() do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def validate(%{"type" => "Create"} = object, meta) do + with {:ok, object} <- + object + |> CreateChatMessageValidator.cast_and_apply() do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def stringify_keys(%{__struct__: _} = object) do + object + |> Map.from_struct() + |> stringify_keys + end + def stringify_keys(object) do object |> Map.new(fn {key, val} -> {to_string(key), val} end) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex new file mode 100644 index 000000000..ab5be3596 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -0,0 +1,58 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do + use Ecto.Schema + + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + + @primary_key false + @derive Jason.Encoder + + embedded_schema do + field(:id, Types.ObjectID, primary_key: true) + field(:to, Types.Recipients, default: []) + field(:type, :string) + field(:content, :string) + field(:actor, Types.ObjectID) + field(:published, Types.DateTime) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + def fix(data) do + data + |> Map.put_new("actor", data["attributedTo"]) + end + + def changeset(struct, data) do + data = fix(data) + + struct + |> cast(data, __schema__(:fields)) + end + + def validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["ChatMessage"]) + |> validate_required([:id, :actor, :to, :type, :content]) + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex new file mode 100644 index 000000000..659311480 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +# NOTES +# - Can probably be a generic create validator +# - doesn't embed, will only get the object id +# - object has to be validated first, maybe with some meta info from the surrounding create +defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do + use Ecto.Schema + + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + + @primary_key false + + embedded_schema do + field(:id, Types.ObjectID, primary_key: true) + field(:actor, Types.ObjectID) + field(:type, :string) + field(:to, Types.Recipients, default: []) + field(:object, Types.ObjectID) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_data(data) do + cast(%__MODULE__{}, data, __schema__(:fields)) + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex index 926804ce7..926804ce7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex new file mode 100644 index 000000000..5a3040842 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex @@ -0,0 +1,23 @@ +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do + use Ecto.Type + + def type, do: {:array, :string} + + def cast(object) when is_binary(object) do + cast([object]) + end + + def cast([_ | _] = data), do: {:ok, data} + + def cast(_) do + :error + end + + def dump(data) do + {:ok, data} + end + + def load(data) do + {:ok, data} + end +end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 666a4e310..594f32700 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -5,8 +5,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do liked object, a `Follow` activity will add the user to the follower collection, and so on. """ + alias Pleroma.Chat alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils def handle(object, meta \\ []) @@ -21,8 +23,35 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do {:ok, object, meta} end + def handle(%{data: %{"type" => "Create", "object" => object_id}} = activity, meta) do + object = Object.get_by_ap_id(object_id) + + {:ok, _object} = handle_object_creation(object) + + {:ok, activity, meta} + end + # Nothing to do def handle(object, meta) do {:ok, object, meta} end + + def handle_object_creation(%{data: %{"type" => "ChatMessage"}} = object) do + actor = User.get_cached_by_ap_id(object.data["actor"]) + recipient = User.get_cached_by_ap_id(hd(object.data["to"])) + + [[actor, recipient], [recipient, actor]] + |> Enum.each(fn [user, other_user] -> + if user.local do + Chat.bump_or_create(user.id, other_user.ap_id) + end + end) + + {:ok, object} + end + + # Nothing to do + def handle_object_creation(object) do + {:ok, object} + end end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 0a8ad62ad..becc35ea3 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.Pipeline + alias Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Federator @@ -612,6 +613,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> handle_incoming(options) end + def handle_incoming( + %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, + options + ), + do: ChatMessageHandling.handle_incoming(data, options) + def handle_incoming(%{"type" => "Like"} = data, _options) do with {_, {:ok, cast_data_sym}} <- {:casting_data, diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex new file mode 100644 index 000000000..b5843736f --- /dev/null +++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling do + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator + alias Pleroma.Web.ActivityPub.Pipeline + + def handle_incoming( + %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object_data} = data, + _options + ) do + with {_, {:ok, cast_data_sym}} <- + {:casting_data, data |> CreateChatMessageValidator.cast_and_apply()}, + cast_data = ObjectValidator.stringify_keys(cast_data_sym), + {_, {:ok, object_cast_data_sym}} <- + {:casting_object_data, object_data |> ChatMessageValidator.cast_and_apply()}, + object_cast_data = ObjectValidator.stringify_keys(object_cast_data_sym), + {_, {:ok, validated_object, _meta}} <- + {:validate_object, ObjectValidator.validate(object_cast_data, %{})}, + {_, {:ok, _created_object}} <- {:persist_object, Object.create(validated_object)}, + {_, {:ok, activity, _meta}} <- + {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do + {:ok, activity} + end + end +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 636cf3301..39e15adbf 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Conversation.Participation alias Pleroma.FollowingRelationship alias Pleroma.Object + alias Pleroma.Repo alias Pleroma.ThreadMute alias Pleroma.User alias Pleroma.UserRelationship @@ -23,6 +24,28 @@ defmodule Pleroma.Web.CommonAPI do require Pleroma.Constants require Logger + def post_chat_message(user, recipient, content) do + transaction = + Repo.transaction(fn -> + with {_, {:ok, chat_message_data, _meta}} <- + {:build_object, Builder.chat_message(user, recipient.ap_id, content)}, + {_, {:ok, chat_message_object}} <- + {:create_object, Object.create(chat_message_data)}, + {_, {:ok, create_activity_data, _meta}} <- + {:build_create_activity, + Builder.create(user, chat_message_object.data["id"], [recipient.ap_id])}, + {_, {:ok, %Activity{} = activity, _meta}} <- + {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do + {:ok, activity} + end + end) + + case transaction do + {:ok, value} -> value + error -> error + end + end + def follow(follower, followed) do timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) diff --git a/priv/repo/migrations/20200309123730_create_chats.exs b/priv/repo/migrations/20200309123730_create_chats.exs new file mode 100644 index 000000000..715d798ea --- /dev/null +++ b/priv/repo/migrations/20200309123730_create_chats.exs @@ -0,0 +1,16 @@ +defmodule Pleroma.Repo.Migrations.CreateChats do + use Ecto.Migration + + def change do + create table(:chats) do + add(:user_id, references(:users, type: :uuid)) + # Recipient is an ActivityPub id, to future-proof for group support. + add(:recipient, :string) + add(:unread, :integer, default: 0) + timestamps() + end + + # There's only one chat between a user and a recipient. + create(index(:chats, [:user_id, :recipient], unique: true)) + end +end diff --git a/test/chat_test.exs b/test/chat_test.exs new file mode 100644 index 000000000..bb2b46d51 --- /dev/null +++ b/test/chat_test.exs @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ChatTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Chat + + import Pleroma.Factory + + describe "creation and getting" do + test "it creates a chat for a user and recipient" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + + assert chat.id + end + + test "it returns a chat for a user and recipient if it already exists" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) + + assert chat.id == chat_two.id + end + + test "a returning chat will have an updated `update_at` field and an incremented unread count" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + assert chat.unread == 1 + :timer.sleep(1500) + {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) + assert chat_two.unread == 2 + + assert chat.id == chat_two.id + assert chat.updated_at != chat_two.updated_at + end + end +end diff --git a/test/fixtures/create-chat-message.json b/test/fixtures/create-chat-message.json new file mode 100644 index 000000000..4aa17f4a5 --- /dev/null +++ b/test/fixtures/create-chat-message.json @@ -0,0 +1,19 @@ +{ + "actor": "http://2hu.gensokyo/users/raymoo", + "id": "http://2hu.gensokyo/objects/1", + "object": { + "attributedTo": "http://2hu.gensokyo/users/raymoo", + "content": "You expected a cute girl? Too bad.", + "id": "http://2hu.gensokyo/objects/2", + "published": "2020-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "ChatMessage" + }, + "published": "2018-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "Create" +} diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs new file mode 100644 index 000000000..2f9218774 --- /dev/null +++ b/test/web/activity_pub/object_validators/types/recipients_test.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do + alias Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients + use Pleroma.DataCase + + test "it works with a list" do + list = ["https://lain.com/users/lain"] + assert {:ok, list} == Recipients.cast(list) + end + + test "it turns a single string into a list" do + recipient = "https://lain.com/users/lain" + + assert {:ok, [recipient]} == Recipients.cast(recipient) + end +end diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index b67bd14b3..b629d0d5d 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do use Pleroma.DataCase + alias Pleroma.Chat alias Pleroma.Object alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder @@ -31,4 +32,49 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do assert user.ap_id in object.data["likes"] end end + + describe "creation of ChatMessages" do + test "it creates a Chat for the local users and bumps the unread count" do + author = insert(:user, local: false) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + {:ok, chat_message_object} = Object.create(chat_message_data) + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_object.data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = SideEffects.handle(create_activity) + + # The remote user won't get a chat + chat = Chat.get(author.id, recipient.ap_id) + refute chat + + # The local user will get a chat + chat = Chat.get(recipient.id, author.ap_id) + assert chat + + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + {:ok, chat_message_object} = Object.create(chat_message_data) + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_object.data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = SideEffects.handle(create_activity) + + # Both users are local and get the chat + chat = Chat.get(author.id, recipient.ap_id) + assert chat + + chat = Chat.get(recipient.id, author.ap_id) + assert chat + end + end end diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs new file mode 100644 index 000000000..aed62c520 --- /dev/null +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + + describe "handle_incoming" do + test "it insert it" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + author = insert(:user, ap_id: data["actor"], local: false) + recipient = insert(:user, ap_id: List.first(data["to"]), local: false) + + {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) + + assert activity.actor == author.ap_id + assert activity.recipients == [recipient.ap_id, author.ap_id] + + %Object{} = object = Object.get_by_ap_id(activity.data["object"]) + assert object + end + end +end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index f46ad0272..1aea06d24 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.CommonAPITest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Chat alias Pleroma.Conversation.Participation alias Pleroma.Object alias Pleroma.User @@ -21,6 +22,26 @@ defmodule Pleroma.Web.CommonAPITest do setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :max_pinned_statuses]) + describe "posting chat messages" do + test "it posts a chat message" do + author = insert(:user) + recipient = insert(:user) + + {:ok, activity} = CommonAPI.post_chat_message(author, recipient, "a test message") + + assert activity.data["type"] == "Create" + assert activity.local + object = Object.normalize(activity) + + assert object.data["type"] == "ChatMessage" + assert object.data["to"] == [recipient.ap_id] + assert object.data["content"] == "a test message" + + assert Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) + end + end + test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) |