### Dependencies
-* Postgresql version 9.6 or newer
+* Postgresql version 9.6 or newer, including the contrib modules
* Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixir’s install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf).
* Build-essential tools
## Customization and contribution
-The [Pleroma Documentation](https://docs-develop.pleroma.social/readme.html) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project.
+The [Pleroma Documentation](https://docs-develop.pleroma.social) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project.
## Troubleshooting
participations =
Enum.map(users, fn user ->
- User.increment_unread_conversation_count(conversation, user)
+ invisible_conversation = Enum.any?(users, &User.blocks?(user, &1))
+
+ unless invisible_conversation do
+ User.increment_unread_conversation_count(conversation, user)
+ end
+
+ opts = Keyword.put(opts, :invisible_conversation, invisible_conversation)
{:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation, opts)
def create_for_user_and_conversation(user, conversation, opts \\ []) do
read = !!opts[:read]
+ invisible_conversation = !!opts[:invisible_conversation]
+
+ update_on_conflict =
+ if(invisible_conversation, do: [], else: [read: read])
+ |> Keyword.put(:updated_at, NaiveDateTime.utc_now())
%__MODULE__{}
- |> creation_cng(%{user_id: user.id, conversation_id: conversation.id, read: read})
+ |> creation_cng(%{
+ user_id: user.id,
+ conversation_id: conversation.id,
+ read: invisible_conversation || read
+ })
|> Repo.insert(
- on_conflict: [set: [read: read, updated_at: NaiveDateTime.utc_now()]],
+ on_conflict: [set: update_on_conflict],
returning: true,
conflict_target: [:user_id, :conversation_id]
)
end
end
- def mark_all_as_read(user) do
+ def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do
+ target_conversation_ids =
+ __MODULE__
+ |> where([p], p.user_id == ^target_user.id)
+ |> select([p], p.conversation_id)
+ |> Repo.all()
+
+ __MODULE__
+ |> where([p], p.user_id == ^user.id)
+ |> where([p], p.conversation_id in ^target_conversation_ids)
+ |> update([p], set: [read: true])
+ |> Repo.update_all([])
+
+ {:ok, user} = User.set_unread_conversation_count(user)
+ {:ok, user, []}
+ end
+
+ def mark_all_as_read(%User{} = user, %User{}), do: {:ok, user, []}
+
+ def mark_all_as_read(%User{} = user) do
{_, participations} =
__MODULE__
|> where([p], p.user_id == ^user.id)
|> select([p], p)
|> Repo.update_all([])
- User.set_unread_conversation_count(user)
- {:ok, participations}
+ {:ok, user} = User.set_unread_conversation_count(user)
+ {:ok, user, participations}
end
def mark_as_unread(participation) do
{:fetch_object, %Object{} = object} ->
{:ok, object}
+ {:fetch, {:error, error}} ->
+ {:error, error}
+
e ->
e
end
with {:ok, object} <- fetch_object_from_id(id, options) do
object
else
+ {:error, %Tesla.Mock.Error{}} ->
+ nil
+
e ->
Logger.error("Error while fetching #{id}: #{inspect(e)}")
nil
{:scheme, _} ->
{:error, "Unsupported URI scheme"}
+ {:error, e} ->
+ {:error, e}
+
e ->
{:error, e}
end
defdelegate following_count(user), to: FollowingRelationship
- @info_fields [
- :banner,
- :background,
- :source_data,
- :note_count,
- :follower_count,
- :following_count,
- :locked,
- :confirmation_pending,
- :password_reset_pending,
- :confirmation_token,
- :default_scope,
- :blocks,
- :domain_blocks,
- :mutes,
- :muted_reblogs,
- :muted_notifications,
- :subscribers,
- :deactivated,
- :no_rich_text,
- :ap_enabled,
- :is_moderator,
- :is_admin,
- :show_role,
- :settings,
- :magic_key,
- :uri,
- :hide_followers_count,
- :hide_follows_count,
- :hide_followers,
- :hide_follows,
- :hide_favorites,
- :unread_conversation_count,
- :pinned_activities,
- :email_notifications,
- :mascot,
- :emoji,
- :pleroma_settings_store,
- :fields,
- :raw_fields,
- :discoverable,
- :invisible,
- :skip_thread_containment,
- :notification_settings
- ]
-
- def info_fields, do: @info_fields
-
defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
end
end
- def set_unread_conversation_count(_), do: :noop
+ def set_unread_conversation_count(user), do: {:ok, user}
def increment_unread_conversation_count(conversation, %User{local: true} = user) do
unread_query =
end
end
- def increment_unread_conversation_count(_, _), do: :noop
+ def increment_unread_conversation_count(_, user), do: {:ok, user}
@spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do
if following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker)
-
+ {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
add_to_block(blocker, ap_id)
end
end
def read_conversations(%{assigns: %{user: user}} = conn, _params) do
- with {:ok, participations} <- Participation.mark_all_as_read(user) do
+ with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
conn
|> add_link_headers(participations)
|> put_view(ConversationView)
--- /dev/null
+defmodule Pleroma.Repo.Migrations.AddUsersInfoColumns do
+ use Ecto.Migration
+
+ @jsonb_array_default "'[]'::jsonb"
+
+ def change do
+ alter table(:users) do
+ add_if_not_exists(:banner, :map, default: %{})
+ add_if_not_exists(:background, :map, default: %{})
+ add_if_not_exists(:source_data, :map, default: %{})
+ add_if_not_exists(:note_count, :integer, default: 0)
+ add_if_not_exists(:follower_count, :integer, default: 0)
+ add_if_not_exists(:following_count, :integer, default: nil)
+ add_if_not_exists(:locked, :boolean, default: false, null: false)
+ add_if_not_exists(:confirmation_pending, :boolean, default: false, null: false)
+ add_if_not_exists(:password_reset_pending, :boolean, default: false, null: false)
+ add_if_not_exists(:confirmation_token, :text, default: nil)
+ add_if_not_exists(:default_scope, :string, default: "public")
+ add_if_not_exists(:blocks, {:array, :text}, default: [])
+ add_if_not_exists(:domain_blocks, {:array, :text}, default: [])
+ add_if_not_exists(:mutes, {:array, :text}, default: [])
+ add_if_not_exists(:muted_reblogs, {:array, :text}, default: [])
+ add_if_not_exists(:muted_notifications, {:array, :text}, default: [])
+ add_if_not_exists(:subscribers, {:array, :text}, default: [])
+ add_if_not_exists(:deactivated, :boolean, default: false, null: false)
+ add_if_not_exists(:no_rich_text, :boolean, default: false, null: false)
+ add_if_not_exists(:ap_enabled, :boolean, default: false, null: false)
+ add_if_not_exists(:is_moderator, :boolean, default: false, null: false)
+ add_if_not_exists(:is_admin, :boolean, default: false, null: false)
+ add_if_not_exists(:show_role, :boolean, default: true, null: false)
+ add_if_not_exists(:settings, :map, default: nil)
+ add_if_not_exists(:magic_key, :text, default: nil)
+ add_if_not_exists(:uri, :text, default: nil)
+ add_if_not_exists(:hide_followers_count, :boolean, default: false, null: false)
+ add_if_not_exists(:hide_follows_count, :boolean, default: false, null: false)
+ add_if_not_exists(:hide_followers, :boolean, default: false, null: false)
+ add_if_not_exists(:hide_follows, :boolean, default: false, null: false)
+ add_if_not_exists(:hide_favorites, :boolean, default: true, null: false)
+ add_if_not_exists(:unread_conversation_count, :integer, default: 0)
+ add_if_not_exists(:pinned_activities, {:array, :text}, default: [])
+ add_if_not_exists(:email_notifications, :map, default: %{"digest" => false})
+ add_if_not_exists(:mascot, :map, default: nil)
+ add_if_not_exists(:emoji, :map, default: fragment(@jsonb_array_default))
+ add_if_not_exists(:pleroma_settings_store, :map, default: %{})
+ add_if_not_exists(:fields, :map, default: fragment(@jsonb_array_default))
+ add_if_not_exists(:raw_fields, :map, default: fragment(@jsonb_array_default))
+ add_if_not_exists(:discoverable, :boolean, default: false, null: false)
+ add_if_not_exists(:invisible, :boolean, default: false, null: false)
+ add_if_not_exists(:notification_settings, :map, default: %{})
+ add_if_not_exists(:skip_thread_containment, :boolean, default: false, null: false)
+ end
+ end
+end
]
def change do
- alter table(:users) do
- add(:banner, :map, default: %{})
- add(:background, :map, default: %{})
- add(:source_data, :map, default: %{})
- add(:note_count, :integer, default: 0)
- add(:follower_count, :integer, default: 0)
- add(:following_count, :integer, default: nil)
- add(:locked, :boolean, default: false, null: false)
- add(:confirmation_pending, :boolean, default: false, null: false)
- add(:password_reset_pending, :boolean, default: false, null: false)
- add(:confirmation_token, :text, default: nil)
- add(:default_scope, :string, default: "public")
- add(:blocks, {:array, :text}, default: [])
- add(:domain_blocks, {:array, :text}, default: [])
- add(:mutes, {:array, :text}, default: [])
- add(:muted_reblogs, {:array, :text}, default: [])
- add(:muted_notifications, {:array, :text}, default: [])
- add(:subscribers, {:array, :text}, default: [])
- add(:deactivated, :boolean, default: false, null: false)
- add(:no_rich_text, :boolean, default: false, null: false)
- add(:ap_enabled, :boolean, default: false, null: false)
- add(:is_moderator, :boolean, default: false, null: false)
- add(:is_admin, :boolean, default: false, null: false)
- add(:show_role, :boolean, default: true, null: false)
- add(:settings, :map, default: nil)
- add(:magic_key, :text, default: nil)
- add(:uri, :text, default: nil)
- add(:hide_followers_count, :boolean, default: false, null: false)
- add(:hide_follows_count, :boolean, default: false, null: false)
- add(:hide_followers, :boolean, default: false, null: false)
- add(:hide_follows, :boolean, default: false, null: false)
- add(:hide_favorites, :boolean, default: true, null: false)
- add(:unread_conversation_count, :integer, default: 0)
- add(:pinned_activities, {:array, :text}, default: [])
- add(:email_notifications, :map, default: %{"digest" => false})
- add(:mascot, :map, default: nil)
- add(:emoji, :map, default: fragment(@jsonb_array_default))
- add(:pleroma_settings_store, :map, default: %{})
- add(:fields, :map, default: fragment(@jsonb_array_default))
- add(:raw_fields, :map, default: fragment(@jsonb_array_default))
- add(:discoverable, :boolean, default: false, null: false)
- add(:invisible, :boolean, default: false, null: false)
- add(:notification_settings, :map, default: %{})
- add(:skip_thread_containment, :boolean, default: false, null: false)
- end
-
if direction() == :up do
- for f <- @info_fields do
- set_field = "update users set #{f} ="
+ sets =
+ for f <- @info_fields do
+ set_field = "#{f} ="
- # Coercion of null::jsonb to NULL
- jsonb = "case when info->>'#{f}' IS NULL then null else info->'#{f}' end"
+ # Coercion of null::jsonb to NULL
+ jsonb = "case when info->>'#{f}' IS NULL then null else info->'#{f}' end"
- cond do
- f in @jsonb_fields ->
- execute("#{set_field} #{jsonb}")
+ cond do
+ f in @jsonb_fields ->
+ "#{set_field} #{jsonb}"
- f in @array_jsonb_fields ->
- execute("#{set_field} coalesce(#{jsonb}, #{@jsonb_array_default})")
+ f in @array_jsonb_fields ->
+ "#{set_field} coalesce(#{jsonb}, #{@jsonb_array_default})"
- f in @int_fields ->
- execute("#{set_field} (info->>'#{f}')::int")
+ f in @int_fields ->
+ "#{set_field} (info->>'#{f}')::int"
- f in @boolean_fields ->
- execute("#{set_field} coalesce((info->>'#{f}')::boolean, false)")
+ f in @boolean_fields ->
+ "#{set_field} coalesce((info->>'#{f}')::boolean, false)"
- f in @array_text_fields ->
- execute("#{set_field} ARRAY(SELECT jsonb_array_elements_text(#{jsonb}))")
+ f in @array_text_fields ->
+ "#{set_field} ARRAY(SELECT jsonb_array_elements_text(#{jsonb}))"
- true ->
- execute("#{set_field} info->>'#{f}'")
+ true ->
+ "#{set_field} info->>'#{f}'"
+ end
end
- end
+ |> Enum.join(", ")
+
+ execute("update users set " <> sets)
for index_name <- [
:users_deactivated_index,
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForActivities do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE activities
+ ALTER COLUMN data SET NOT NULL,
+ ALTER COLUMN local SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE activities
+ ALTER COLUMN data DROP NOT NULL,
+ ALTER COLUMN local DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForActivityExpirations do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE activity_expirations
+ ALTER COLUMN activity_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE activity_expirations
+ ALTER COLUMN activity_id DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForApps do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE apps
+ ALTER COLUMN client_name SET NOT NULL,
+ ALTER COLUMN redirect_uris SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE apps
+ ALTER COLUMN client_name DROP NOT NULL,
+ ALTER COLUMN redirect_uris DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForBookmarks do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE bookmarks
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN activity_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE bookmarks
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN activity_id DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForConfig do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE config
+ ALTER COLUMN key SET NOT NULL,
+ ALTER COLUMN value SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE config
+ ALTER COLUMN key DROP NOT NULL,
+ ALTER COLUMN value DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForConversationParticipationRecipientShips do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE conversation_participation_recipient_ships
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN participation_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE conversation_participation_recipient_ships
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN participation_id DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForConversationParticipations do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE conversation_participations
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN conversation_id SET NOT NULL,
+ ALTER COLUMN read SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE conversation_participations
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN conversation_id DROP NOT NULL,
+ ALTER COLUMN read DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForFilters do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE filters
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN filter_id SET NOT NULL,
+ ALTER COLUMN whole_word SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE filters
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN filter_id DROP NOT NULL,
+ ALTER COLUMN whole_word DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForInstances do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE instances
+ ALTER COLUMN host SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE instances
+ ALTER COLUMN host DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForLists do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE lists
+ ALTER COLUMN user_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE lists
+ ALTER COLUMN user_id DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForMarkers do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE markers
+ ALTER COLUMN user_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE markers
+ ALTER COLUMN user_id DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForModerationLog do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE moderation_log
+ ALTER COLUMN data SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE moderation_log
+ ALTER COLUMN data DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForNotifications do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE notifications
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN seen SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE notifications
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN seen DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForOauthAuthorizations do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE oauth_authorizations
+ ALTER COLUMN app_id SET NOT NULL,
+ ALTER COLUMN token SET NOT NULL,
+ ALTER COLUMN used SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE oauth_authorizations
+ ALTER COLUMN app_id DROP NOT NULL,
+ ALTER COLUMN token DROP NOT NULL,
+ ALTER COLUMN used DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForOauthTokens do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE oauth_tokens
+ ALTER COLUMN app_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE oauth_tokens
+ ALTER COLUMN app_id DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForObjects do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE objects
+ ALTER COLUMN data SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE objects
+ ALTER COLUMN data DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForPasswordResetTokens do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE password_reset_tokens
+ ALTER COLUMN token SET NOT NULL,
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN used SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE password_reset_tokens
+ ALTER COLUMN token DROP NOT NULL,
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN used DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForPushSubscriptions do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE push_subscriptions
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN token_id SET NOT NULL,
+ ALTER COLUMN endpoint SET NOT NULL,
+ ALTER COLUMN key_p256dh SET NOT NULL,
+ ALTER COLUMN key_auth SET NOT NULL,
+ ALTER COLUMN data SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE push_subscriptions
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN token_id DROP NOT NULL,
+ ALTER COLUMN endpoint DROP NOT NULL,
+ ALTER COLUMN key_p256dh DROP NOT NULL,
+ ALTER COLUMN key_auth DROP NOT NULL,
+ ALTER COLUMN data DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForRegistrations do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE registrations
+ ALTER COLUMN provider SET NOT NULL,
+ ALTER COLUMN uid SET NOT NULL,
+ ALTER COLUMN info SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE registrations
+ ALTER COLUMN provider DROP NOT NULL,
+ ALTER COLUMN uid DROP NOT NULL,
+ ALTER COLUMN info DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForScheduledActivities do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE scheduled_activities
+ ALTER COLUMN user_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE scheduled_activities
+ ALTER COLUMN user_id DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForThreadMutes do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE thread_mutes
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN context SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE thread_mutes
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN context DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForUserInviteTokens do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE user_invite_tokens
+ ALTER COLUMN used SET NOT NULL,
+ ALTER COLUMN uses SET NOT NULL,
+ ALTER COLUMN invite_type SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE user_invite_tokens
+ ALTER COLUMN used DROP NOT NULL,
+ ALTER COLUMN uses DROP NOT NULL,
+ ALTER COLUMN invite_type DROP NOT NULL")
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.SetNotNullForUsers do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ # irreversible
+ execute("UPDATE users SET follower_count = 0 WHERE follower_count IS NULL")
+
+ execute("ALTER TABLE users
+ ALTER COLUMN following SET NOT NULL,
+ ALTER COLUMN local SET NOT NULL,
+ ALTER COLUMN source_data SET NOT NULL,
+ ALTER COLUMN note_count SET NOT NULL,
+ ALTER COLUMN follower_count SET NOT NULL,
+ ALTER COLUMN blocks SET NOT NULL,
+ ALTER COLUMN domain_blocks SET NOT NULL,
+ ALTER COLUMN mutes SET NOT NULL,
+ ALTER COLUMN muted_reblogs SET NOT NULL,
+ ALTER COLUMN muted_notifications SET NOT NULL,
+ ALTER COLUMN subscribers SET NOT NULL,
+ ALTER COLUMN pinned_activities SET NOT NULL,
+ ALTER COLUMN emoji SET NOT NULL,
+ ALTER COLUMN fields SET NOT NULL,
+ ALTER COLUMN raw_fields SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE users
+ ALTER COLUMN following DROP NOT NULL,
+ ALTER COLUMN local DROP NOT NULL,
+ ALTER COLUMN source_data DROP NOT NULL,
+ ALTER COLUMN note_count DROP NOT NULL,
+ ALTER COLUMN follower_count DROP NOT NULL,
+ ALTER COLUMN blocks DROP NOT NULL,
+ ALTER COLUMN domain_blocks DROP NOT NULL,
+ ALTER COLUMN mutes DROP NOT NULL,
+ ALTER COLUMN muted_reblogs DROP NOT NULL,
+ ALTER COLUMN muted_notifications DROP NOT NULL,
+ ALTER COLUMN subscribers DROP NOT NULL,
+ ALTER COLUMN pinned_activities DROP NOT NULL,
+ ALTER COLUMN emoji DROP NOT NULL,
+ ALTER COLUMN fields DROP NOT NULL,
+ ALTER COLUMN raw_fields DROP NOT NULL")
+ end
+end
FULL_ARGS="$*"
ACTION="$1"
- shift
- echo "$1" | grep "^-" >/dev/null
+ if [ $# -gt 0 ]; then
+ shift
+ fi
+ echo "$1" | grep "^-" >/dev/null
if [ $? -eq 1 ]; then
SUBACTION="$1"
- shift
+ if [ $# -gt 0 ]; then
+ shift
+ fi
fi
if [ "$ACTION" = "update" ]; then
participation2 = insert(:participation, %{read: false, user: user})
participation3 = insert(:participation, %{read: false, user: other_user})
- {:ok, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user)
+ {:ok, _, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user)
assert Participation.get(participation1.id).read == true
assert Participation.get(participation2.id).read == true
assert user in participation.recipients
assert other_user in participation.recipients
end
+
+ describe "blocking" do
+ test "when the user blocks a recipient, the existing conversations with them are marked as read" do
+ blocker = insert(:user)
+ blocked = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, _direct1} =
+ CommonAPI.post(third_user, %{
+ "status" => "Hi @#{blocker.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct2} =
+ CommonAPI.post(third_user, %{
+ "status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct3} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct4} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}, @#{third_user.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] =
+ Participation.for_user(blocker)
+
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4
+
+ {:ok, blocker} = User.block(blocker, blocked)
+
+ # The conversations with the blocked user are marked as read
+ assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] =
+ Participation.for_user(blocker)
+
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 1
+
+ # The conversation is not marked as read for the blocked user
+ assert [_, _, %{read: false}] = Participation.for_user(blocked)
+ assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
+
+ # The conversation is not marked as read for the third user
+ assert [%{read: false}, _, _] = Participation.for_user(third_user)
+ assert User.get_cached_by_id(third_user.id).unread_conversation_count == 1
+ end
+
+ test "the new conversation with the blocked user is not marked as unread " do
+ blocker = insert(:user)
+ blocked = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, blocker} = User.block(blocker, blocked)
+
+ # When the blocked user is the author
+ {:ok, _direct1} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ # When the blocked user is a recipient
+ {:ok, _direct2} =
+ CommonAPI.post(third_user, %{
+ "status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert [%{read: true}, %{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ assert [%{read: false}, _] = Participation.for_user(blocked)
+ assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
+ end
+
+ test "the conversation with the blocked user is not marked as unread on a reply" do
+ blocker = insert(:user)
+ blocked = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, _direct1} =
+ CommonAPI.post(blocker, %{
+ "status" => "Hi @#{third_user.nickname}, @#{blocked.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, blocker} = User.block(blocker, blocked)
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ assert [blocked_participation] = Participation.for_user(blocked)
+
+ # When it's a reply from the blocked user
+ {:ok, _direct2} =
+ CommonAPI.post(blocked, %{
+ "status" => "reply",
+ "visibility" => "direct",
+ "in_reply_to_conversation_id" => blocked_participation.id
+ })
+
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ assert [third_user_participation] = Participation.for_user(third_user)
+
+ # When it's a reply from the third user
+ {:ok, _direct3} =
+ CommonAPI.post(third_user, %{
+ "status" => "reply",
+ "visibility" => "direct",
+ "in_reply_to_conversation_id" => third_user_participation.id
+ })
+
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ # Marked as unread for the blocked user
+ assert [%{read: false}] = Participation.for_user(blocked)
+ assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
+ end
+ end
end
assert capture_log(fn ->
:error = Transmogrifier.handle_incoming(data)
end) =~
- "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, {:error, :nxdomain}}"
+ "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}"
assert Activity.get_by_id(activity.id)
end