diff options
Diffstat (limited to 'priv/repo/migrations')
8 files changed, 472 insertions, 5 deletions
diff --git a/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs b/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs index f3928a149..99102117f 100644 --- a/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs +++ b/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs @@ -8,10 +8,10 @@ defmodule Pleroma.Repo.Migrations.MigrateOldBookmarks do    def up do      query = -      from(u in User, +      from(u in "users",          where: u.local == true, -        where: fragment("array_length(bookmarks, 1)") > 0, -        select: %{id: u.id, bookmarks: fragment("bookmarks")} +        where: fragment("array_length(?, 1)", u.bookmarks) > 0, +        select: %{id: u.id, bookmarks: u.bookmarks}        )      Repo.stream(query) diff --git a/priv/repo/migrations/20191007073319_create_following_relationships.exs b/priv/repo/migrations/20191007073319_create_following_relationships.exs new file mode 100644 index 000000000..d49e24ee4 --- /dev/null +++ b/priv/repo/migrations/20191007073319_create_following_relationships.exs @@ -0,0 +1,149 @@ +defmodule Pleroma.Repo.Migrations.CreateFollowingRelationships do +  use Ecto.Migration + +  def change do +    create_if_not_exists table(:following_relationships) do +      add(:follower_id, references(:users, type: :uuid, on_delete: :delete_all), null: false) +      add(:following_id, references(:users, type: :uuid, on_delete: :delete_all), null: false) +      add(:state, :string, null: false) + +      timestamps() +    end + +    create_if_not_exists(index(:following_relationships, :follower_id)) +    create_if_not_exists(unique_index(:following_relationships, [:follower_id, :following_id])) + +    execute(update_thread_visibility(), restore_thread_visibility()) +  end + +  # The only difference between the original version: `actor_user` replaced with `actor_user_following` +  def update_thread_visibility do +    """ +    CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$ +    DECLARE +      public varchar := 'https://www.w3.org/ns/activitystreams#Public'; +      child objects%ROWTYPE; +      activity activities%ROWTYPE; +      author_fa varchar; +      valid_recipients varchar[]; +      actor_user_following varchar[]; +    BEGIN +      --- Fetch actor following +      SELECT array_agg(following.follower_address) INTO actor_user_following FROM following_relationships +      JOIN users ON users.id = following_relationships.follower_id +      JOIN users AS following ON following.id = following_relationships.following_id +      WHERE users.ap_id = actor; + +      --- Fetch our initial activity. +      SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id; + +      LOOP +        --- Ensure that we have an activity before continuing. +        --- If we don't, the thread is not satisfiable. +        IF activity IS NULL THEN +          RETURN false; +        END IF; + +        --- We only care about Create activities. +        IF activity.data->>'type' != 'Create' THEN +          RETURN true; +        END IF; + +        --- Normalize the child object into child. +        SELECT * INTO child FROM objects +        INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' +        WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id'; + +        --- Fetch the author's AS2 following collection. +        SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor; + +        --- Prepare valid recipients array. +        valid_recipients := ARRAY[actor, public]; +        IF ARRAY[author_fa] && actor_user_following THEN +          valid_recipients := valid_recipients || author_fa; +        END IF; + +        --- Check visibility. +        IF NOT valid_recipients && activity.recipients THEN +          --- activity not visible, break out of the loop +          RETURN false; +        END IF; + +        --- If there's a parent, load it and do this all over again. +        IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN +          SELECT * INTO activity FROM activities +          INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' +          WHERE child.data->>'inReplyTo' = objects.data->>'id'; +        ELSE +          RETURN true; +        END IF; +      END LOOP; +    END; +    $$ LANGUAGE plpgsql IMMUTABLE; +    """ +  end + +  # priv/repo/migrations/20190515222404_add_thread_visibility_function.exs +  def restore_thread_visibility do +    """ +    CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$ +    DECLARE +      public varchar := 'https://www.w3.org/ns/activitystreams#Public'; +      child objects%ROWTYPE; +      activity activities%ROWTYPE; +      actor_user users%ROWTYPE; +      author_fa varchar; +      valid_recipients varchar[]; +    BEGIN +      --- Fetch our actor. +      SELECT * INTO actor_user FROM users WHERE users.ap_id = actor; + +      --- Fetch our initial activity. +      SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id; + +      LOOP +        --- Ensure that we have an activity before continuing. +        --- If we don't, the thread is not satisfiable. +        IF activity IS NULL THEN +          RETURN false; +        END IF; + +        --- We only care about Create activities. +        IF activity.data->>'type' != 'Create' THEN +          RETURN true; +        END IF; + +        --- Normalize the child object into child. +        SELECT * INTO child FROM objects +        INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' +        WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id'; + +        --- Fetch the author's AS2 following collection. +        SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor; + +        --- Prepare valid recipients array. +        valid_recipients := ARRAY[actor, public]; +        IF ARRAY[author_fa] && actor_user.following THEN +          valid_recipients := valid_recipients || author_fa; +        END IF; + +        --- Check visibility. +        IF NOT valid_recipients && activity.recipients THEN +          --- activity not visible, break out of the loop +          RETURN false; +        END IF; + +        --- If there's a parent, load it and do this all over again. +        IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN +          SELECT * INTO activity FROM activities +          INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' +          WHERE child.data->>'inReplyTo' = objects.data->>'id'; +        ELSE +          RETURN true; +        END IF; +      END LOOP; +    END; +    $$ LANGUAGE plpgsql IMMUTABLE; +    """ +  end +end diff --git a/priv/repo/migrations/20191008132217_migrate_following_relationships.exs b/priv/repo/migrations/20191008132217_migrate_following_relationships.exs new file mode 100644 index 000000000..9d5c2648f --- /dev/null +++ b/priv/repo/migrations/20191008132217_migrate_following_relationships.exs @@ -0,0 +1,89 @@ +defmodule Pleroma.Repo.Migrations.MigrateFollowingRelationships do +  use Ecto.Migration + +  def change do +    execute(import_following_from_users(), "") +    execute(import_following_from_activities(), restore_following_column()) +  end + +  defp import_following_from_users do +    """ +    INSERT INTO following_relationships (follower_id, following_id, state, inserted_at, updated_at) +    SELECT +        relations.follower_id, +        following.id, +        'accept', +        now(), +        now() +    FROM ( +        SELECT +            users.id AS follower_id, +            unnest(users.following) AS following_ap_id +        FROM +            users +        WHERE +            users.following != '{}' +            AND users.local = false OR users.local = true AND users.email IS NOT NULL -- Exclude `internal/fetch` and `relay` +    ) AS relations +        JOIN users AS "following" ON "following".follower_address = relations.following_ap_id + +        WHERE relations.follower_id != following.id +    ON CONFLICT DO NOTHING +    """ +  end + +  defp import_following_from_activities do +    """ +    INSERT INTO +        following_relationships ( +            follower_id, +            following_id, +            state, +            inserted_at, +            updated_at +        ) +    SELECT +        followers.id, +        following.id, +        activities.data ->> 'state', +        (activities.data ->> 'published') :: timestamp, +        now() +    FROM +        activities +        JOIN users AS followers ON (activities.actor = followers.ap_id) +        JOIN users AS following ON (activities.data ->> 'object' = following.ap_id) +    WHERE +        activities.data ->> 'type' = 'Follow' +        AND activities.data ->> 'state' IN ('accept', 'pending', 'reject') +    ORDER BY activities.updated_at DESC +    ON CONFLICT DO NOTHING +    """ +  end + +  defp restore_following_column do +    """ +    UPDATE +        users +    SET +        following = following_query.following_array, +        updated_at = now() +    FROM ( +        SELECT +            follower.id AS follower_id, +            CASE follower.local +            WHEN TRUE THEN +                array_prepend(follower.follower_address, array_agg(following.follower_address)) +            ELSE +                array_agg(following.follower_address) +            END AS following_array +        FROM +            following_relationships +            JOIN users AS follower ON follower.id = following_relationships.follower_id +            JOIN users AS following ON following.id = following_relationships.following_id +        GROUP BY +            follower.id) AS following_query +    WHERE +        following_query.follower_id = users.id +    """ +  end +end diff --git a/priv/repo/migrations/20191008132427_drop_users_following.exs b/priv/repo/migrations/20191008132427_drop_users_following.exs new file mode 100644 index 000000000..21c0af9f4 --- /dev/null +++ b/priv/repo/migrations/20191008132427_drop_users_following.exs @@ -0,0 +1,16 @@ +defmodule Pleroma.Repo.Migrations.DropUsersFollowing do +  use Ecto.Migration + +  # had to disable these to be able to restore `following` index concurrently +  # https://hexdocs.pm/ecto_sql/Ecto.Migration.html#index/3-adding-dropping-indexes-concurrently +  @disable_ddl_transaction true +  @disable_migration_lock true + +  def change do +    drop(index(:users, [:following], concurrently: true, using: :gin)) + +    alter table(:users) do +      remove(:following, {:array, :string}, default: []) +    end +  end +end diff --git a/priv/repo/migrations/20191025143434_add_defaults_to_tables.exs b/priv/repo/migrations/20191025143434_add_defaults_to_tables.exs new file mode 100644 index 000000000..a5bc82335 --- /dev/null +++ b/priv/repo/migrations/20191025143434_add_defaults_to_tables.exs @@ -0,0 +1,68 @@ +defmodule Pleroma.Repo.Migrations.AddDefaultsToTables do +  use Ecto.Migration + +  def up do +    execute("ALTER TABLE activities +    ALTER COLUMN recipients SET DEFAULT ARRAY[]::character varying[]") + +    execute("ALTER TABLE filters +    ALTER COLUMN whole_word SET DEFAULT true") + +    execute("ALTER TABLE push_subscriptions +    ALTER COLUMN data SET DEFAULT '{}'::jsonb") + +    execute(~s(ALTER TABLE users +    ALTER COLUMN tags SET DEFAULT ARRAY[]::character varying[], +    ALTER COLUMN notification_settings SET DEFAULT +      '{"followers": true, "follows": true, "non_follows": true, "non_followers": true}'::jsonb)) + +    # irreversible updates + +    execute( +      "UPDATE activities SET recipients = ARRAY[]::character varying[] WHERE recipients IS NULL" +    ) + +    execute("UPDATE filters SET whole_word = true WHERE whole_word IS NULL") + +    execute("UPDATE push_subscriptions SET data = '{}'::jsonb WHERE data IS NULL") + +    execute("UPDATE users SET source_data = '{}'::jsonb where source_data IS NULL") +    execute("UPDATE users SET note_count = 0 where note_count IS NULL") +    execute("UPDATE users SET background = '{}'::jsonb where background IS NULL") +    execute("UPDATE users SET follower_count = 0 where follower_count IS NULL") + +    execute( +      "UPDATE users SET unread_conversation_count = 0 where unread_conversation_count IS NULL" +    ) + +    execute( +      ~s(UPDATE users SET email_notifications = '{"digest": false}'::jsonb where email_notifications IS NULL) +    ) + +    execute("UPDATE users SET default_scope = 'public' where default_scope IS NULL") + +    execute( +      "UPDATE users SET pleroma_settings_store = '{}'::jsonb where pleroma_settings_store IS NULL" +    ) + +    execute("UPDATE users SET tags = ARRAY[]::character varying[] WHERE tags IS NULL") +    execute(~s(UPDATE users SET notification_settings = +      '{"followers": true, "follows": true, "non_follows": true, "non_followers": true}'::jsonb +      WHERE notification_settings = '{}'::jsonb)) +  end + +  def down do +    execute("ALTER TABLE activities +    ALTER COLUMN recipients DROP DEFAULT") + +    execute("ALTER TABLE filters +    ALTER COLUMN whole_word DROP DEFAULT") + +    execute("ALTER TABLE push_subscriptions +    ALTER COLUMN data DROP DEFAULT") + +    execute("ALTER TABLE users +    ALTER COLUMN tags DROP DEFAULT, +    ALTER COLUMN notification_settings SET DEFAULT '{}'::jsonb") +  end +end diff --git a/priv/repo/migrations/20191026191910_set_not_null_for_users.exs b/priv/repo/migrations/20191026191910_set_not_null_for_users.exs index f145a89ab..9d8d0ccf8 100644 --- a/priv/repo/migrations/20191026191910_set_not_null_for_users.exs +++ b/priv/repo/migrations/20191026191910_set_not_null_for_users.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Repo.Migrations.SetNotNullForUsers do      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, @@ -27,7 +26,6 @@ defmodule Pleroma.Repo.Migrations.SetNotNullForUsers do    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, diff --git a/priv/repo/migrations/20191029101340_migrate_missing_follow_requests.exs b/priv/repo/migrations/20191029101340_migrate_missing_follow_requests.exs new file mode 100644 index 000000000..90b18efc8 --- /dev/null +++ b/priv/repo/migrations/20191029101340_migrate_missing_follow_requests.exs @@ -0,0 +1,35 @@ +defmodule Pleroma.Repo.Migrations.MigrateMissingFollowingRelationships do +  use Ecto.Migration + +  def change do +    execute(import_pending_follows_from_activities(), "") +  end + +  defp import_pending_follows_from_activities do +    """ +    INSERT INTO +        following_relationships ( +            follower_id, +            following_id, +            state, +            inserted_at, +            updated_at +        ) +    SELECT +        followers.id, +        following.id, +        activities.data ->> 'state', +        (activities.data ->> 'published') :: timestamp, +        now() +    FROM +        activities +        JOIN users AS followers ON (activities.actor = followers.ap_id) +        JOIN users AS following ON (activities.data ->> 'object' = following.ap_id) +    WHERE +        activities.data ->> 'type' = 'Follow' +        AND activities.data ->> 'state' = 'pending' +    ORDER BY activities.updated_at DESC +    ON CONFLICT DO NOTHING +    """ +  end +end diff --git a/priv/repo/migrations/20191029172832_fix_blocked_follows.exs b/priv/repo/migrations/20191029172832_fix_blocked_follows.exs new file mode 100644 index 000000000..71f8f1330 --- /dev/null +++ b/priv/repo/migrations/20191029172832_fix_blocked_follows.exs @@ -0,0 +1,112 @@ +defmodule Pleroma.Repo.Migrations.FixBlockedFollows do +  use Ecto.Migration + +  import Ecto.Query +  alias Pleroma.Config +  alias Pleroma.Repo + +  def up do +    unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) + +    if unfollow_blocked do +      "activities" +      |> where([activity], fragment("? ->> 'type' = 'Block'", activity.data)) +      |> distinct([activity], [ +        activity.actor, +        fragment( +          "coalesce((?)->'object'->>'id', (?)->>'object')", +          activity.data, +          activity.data +        ) +      ]) +      |> order_by([activity], [fragment("? desc nulls last", activity.id)]) +      |> select([activity], %{ +        blocker: activity.actor, +        blocked: +          fragment("coalesce((?)->'object'->>'id', (?)->>'object')", activity.data, activity.data), +        created_at: activity.id +      }) +      |> Repo.stream() +      |> Enum.map(&unfollow_if_blocked/1) +      |> Enum.uniq() +      |> Enum.each(&update_follower_count/1) +    end +  end + +  def down do +  end + +  def unfollow_if_blocked(%{blocker: blocker_id, blocked: blocked_id, created_at: blocked_at}) do +    query = +      from( +        activity in "activities", +        where: fragment("? ->> 'type' = 'Follow'", activity.data), +        where: activity.actor == ^blocked_id, +        # this is to use the index +        where: +          fragment( +            "coalesce((?)->'object'->>'id', (?)->>'object') = ?", +            activity.data, +            activity.data, +            ^blocker_id +          ), +        where: activity.id > ^blocked_at, +        where: fragment("(?)->>'state' = 'accept'", activity.data), +        order_by: [fragment("? desc nulls last", activity.id)] +      ) + +    unless Repo.exists?(query) do +      blocker = "users" |> select([:id, :local]) |> Repo.get_by(ap_id: blocker_id) +      blocked = "users" |> select([:id]) |> Repo.get_by(ap_id: blocked_id) + +      if !is_nil(blocker) && !is_nil(blocked) do +        unfollow(blocked, blocker) +      end +    end +  end + +  def unfollow(%{id: follower_id}, %{id: followed_id} = followed) do +    following_relationship = +      "following_relationships" +      |> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept") +      |> select([:id]) +      |> Repo.one() + +    case following_relationship do +      nil -> +        {:ok, nil} + +      %{id: following_relationship_id} -> +        "following_relationships" +        |> where(id: ^following_relationship_id) +        |> Repo.delete_all() + +        followed +    end +  end + +  def update_follower_count(%{id: user_id} = user) do +    if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do +      follower_count_query = +        "users" +        |> where([u], u.id != ^user_id) +        |> where([u], u.deactivated != ^true) +        |> join(:inner, [u], r in "following_relationships", +          as: :relationships, +          on: r.following_id == ^user_id and r.follower_id == u.id +        ) +        |> where([relationships: r], r.state == "accept") +        |> select([u], %{count: count(u.id)}) + +      "users" +      |> where(id: ^user_id) +      |> join(:inner, [u], s in subquery(follower_count_query)) +      |> update([u, s], +        set: [follower_count: s.count] +      ) +      |> Repo.update_all([]) +    end +  end + +  def update_follower_count(_), do: :noop +end  | 
