10up / distributor

Share content between your websites.
https://distributorplugin.com
GNU General Public License v2.0
640 stars 156 forks source link

Distributing posts with correct author #68

Open robbiet480 opened 6 years ago

robbiet480 commented 6 years ago

We have a pretty big network of sites. All of them need to show proper author information. I see that as of right now Distributor will set the distributed posts author to the authenticated users ID. I'd like to change this (and am willing to do the work myself). Here's two possible solutions:

  1. The simplest method is to just pass through the user ID of the original post author and leave it up to admins to make sure their user IDs are synced across all sites. This gets a bit easier if you are using an SSO plugin of some kind.
  2. Add a hook to the post creation (or use the existing dt_item_mapping for pull or dt_push_post_args for push filter) to allow passing the "correct" author ID based on the existing post author ID and what site we are posting to. This would require end users to have a map or database of current site author ID to external site author ID. Not the hardest thing in the world for a lot of admins to build, but still a pretty high bar. If people think this is the best route, I would most likely write my own solution and document it in README.md. Eventually, a UI could possible be created in Distributor to manage this.

Anyway, looking for feedback on which path to go down or a different path entirely. I'm looking to get this done ASAP and would of course submit any changes to the plugin back.

dleeward commented 6 years ago

We wrote our own syndication application and used the author's email address to query the receiving site for the ID. This assumes authors use the same email address on all sites in the network.

robbiet480 commented 6 years ago

Thanks for the tip @dleeward, I just finished hacking together some code that can live in your theme functions.php to sync authors during a push. It will attempt to find the author by the passed email address. If an existing user can not be found, a user is created with the properties from the pushing WP. This hasn't been well tested yet and should not yet be used in production.

if ( defined( 'DT_VERSION' ) ) {
  function setup_distributor_replacements() {
    $post_types = get_post_types(array('show_in_rest' => true));
    foreach ( $post_types as $post_type ) {
      add_action( "rest_insert_{$post_type}", 'process_original_author', 10, 3 );
    }
  }

  function process_original_author( $post, $request, $update ) {
    if ( empty( $post ) || is_wp_error( $post ) ) {
      return;
    }
    if ( ! empty( $request['distributor_original_author'] ) ) {
      $author_obj = $request['distributor_original_author'];
      $new_author = get_user_by( 'email', $author_obj['email'] );
      $author_id = 0;
      if ( ! empty( $new_author ) && ! is_wp_error( $new_author ) ) {
        $author_id = $new_author->ID;
      } else {
        $create_user = wp_insert_user(array(
          'display_name'      => $author_obj['display_name'],
          'first_name'        => $author_obj['first_name'],
          'last_name'         => $author_obj['last_name'],
          'nickname'          => $author_obj['nickname'],
          'user_displayname'  => $author_obj['user_displayname'],
          'user_email'        => $author_obj['user_email'],
          'user_login'        => $author_obj['user_login'],
          'user_nicename'     => $author_obj['user_nicename'],
          'user_pass'         => NULL
        ));
        if ( is_wp_error( $create_user ) ) {
          error_log("Error creating user during Distributor push ".$create_user->get_error_message());
          return;
        }
        $author_id = $create_user;
      }
      wp_update_post( array( 'ID' => $post->ID, 'post_author' => $author_id ) );
    }
  }

  add_action('init', 'setup_distributor_replacements');

  function add_original_author($post_body, $post, $that) {
    $post_author = get_user_by( 'id', $post->post_author );
    $author_obj = array(
      'display_name'  => $post_author->display_name,
      'first_name'    => get_user_meta($post->post_author, 'first_name', true),
      'first_name'    => get_user_meta($post->post_author, 'first_name', true),
      'last_name'     => get_user_meta($post->post_author, 'last_name', true),
      'user_email'    => $post_author->user_email,
      'user_login'    => $post_author->user_login,
      'user_nicename' => $post_author->user_nicename,
    );
    $post_body['distributor_original_author'] = $author_obj;
    return $post_body;
  }

  add_filter('dt_push_post_args', 'add_original_author', 1, 3);

}
dleeward commented 6 years ago

@robbiet480 You shouldn't assume that the original author's user_login and user_nicename aren't already being used. We create an arbitrary username of the current unix time prefixed with a short text string such as ABC1519562843. This does assume you aren't creating more than one user per second. You could use microtime or add a random suffix instead .

tlovett1 commented 6 years ago

We'd discussed this one a lot.

Generally, we believe posting as the authenticated user is the safest most compatible solution for everyone. We definitely see different publishers may have different requirements. Creating a new user on the remote site opens up a ton of hard questions. What is the new users role? Should that account be accessible by the original author?

Of course this is open for discussion, and we'd love to hear other opinions, but for now the best we can do is to add additional hooks/filters to make what @robbiet480 accomplished easier.

jasonbahl commented 6 years ago

One thing to note, is that many publications have the need to support multiple authors for content, and in those cases, "Users" are often not even used for the author <-> content relationship.

When I was working on syndicating content at DFM, we used Co-Authors Plus which was a hybrid post-type/taxonomy solution for adding many authors to posts, so in our case we actually wanted the authenticated user to be connected to the post as the author of the syndicated content, so we had a trail of what user triggered the syndication, and we syndicated additional data to handle the authors<->content relationship (somewhat similar to how relationships to media is handled)

tlovett1 commented 6 years ago

Appreciate the feedback, Jason!

robbiet480 commented 6 years ago

So i've taken this a step further now and actually added support for Co-Authors Plus to Distributor via hooks. Here's my code for anyone else interested in the integration. I've inquired in #131 about inclusion in Distributor.

<?php

function handle_dt_process_distributor_attributes( $post, $request, $update ) {
  if ( empty( $post ) || is_wp_error( $post ) ) {
    return;
  }

  if ( is_plugin_active( 'co-authors-plus/co-authors-plus.php' ) && ! empty( $request['distributor_coauthors'] ) ) {
    global $coauthors_plus;

    $author_logins = array();

    foreach ( $request['distributor_coauthors'] as $coauthor ) {

      $existing_guest_author = $coauthors_plus->guest_authors->get_guest_author_by( 'user_login', $coauthor['user_login'] );

      if ( ! $existing_guest_author ) {
        $guest_author_id = $coauthors_plus->guest_authors->create( array(
          'display_name' => $coauthor['display_name'],
          'first_name' => $coauthor['first_name'],
          'last_name' => $coauthor['last_name'],
          'user_login' => $coauthor['user_login'],
          'user_nicename' => $coauthor['user_nicename']
        ) );

        if ( is_wp_error( $guest_author_id ) ) {
          error_log("Error creating co-author during Distributor push ".$guest_author_id->get_error_message());
          return;
        }

        if ( !$guest_author_id ) {
          error_log( '-- Failed to create guest author.' );
        }

        if ( ! empty($coauthor['avatar_url']) ) {
          require_once(ABSPATH . 'wp-admin/includes/media.php');
          require_once(ABSPATH . 'wp-admin/includes/file.php');
          require_once(ABSPATH . 'wp-admin/includes/image.php');

          $thumbnail_id = media_sideload_image($coauthor['avatar_url'], $guest_author_id, null, 'id');
          set_post_thumbnail($guest_author_id, $thumbnail_id);
        }

        $guest_author = $coauthors_plus->guest_authors->get_guest_author_by( 'ID', $guest_author_id );
        array_push($author_logins, $guest_author->user_login);
      } else {
        array_push($author_logins, $existing_guest_author->user_login);
      }
    }
    clean_post_cache($post->ID);
    $coauthors_plus->guest_authors->delete_guest_author_cache($author_logins[0]);
  }
}

add_action( 'dt_process_distributor_attributes', 'handle_dt_process_distributor_attributes', 10, 3 );

if ( is_plugin_active( 'co-authors-plus/co-authors-plus.php' ) ) {
  function distributor_coauthors_get_avatar_url( $coauthor ) {
    global $coauthors_plus;
    if ( ! is_object( $coauthor ) ) {
      return '';
    }

    if ( isset( $coauthor->type ) && 'guest-author' == $coauthor->type ) {
      if ( ! has_post_thumbnail( $coauthor->ID ) ) {
        return '';
      }

      $large_image_url = wp_get_attachment_image_src( get_post_thumbnail_id( $coauthor->ID ), 'large' );

      return $large_image_url[0];
    }

    // Make sure we're dealing with an object for which we can retrieve an email
    if ( isset( $coauthor->user_email ) ) {
      return get_avatar( $coauthor->user_email, $size, $default, $alt );
    }

    // Nothing matched, an invalid object was passed.
    return '';
  }
}

function add_dt_push_post_args($post_body, $post, $that) {
  if ( is_plugin_active( 'co-authors-plus/co-authors-plus.php' ) ) {
    $post_body['distributor_coauthors'] = array();

    foreach ( $coauthors as $coauthor ) {
      if( array_key_exists("data", $coauthor) ) {
        // Don't include real WP_Users
        continue;
      }
      $coauthor_arr = (array) $coauthor;
      $coauthor_arr['avatar_url'] = distributor_coauthors_get_avatar_url($coauthor);
      array_push($post_body['distributor_coauthors'], $coauthor_arr);
    }
  }

  return $post_body;
}

add_filter('dt_push_post_args', 'add_dt_push_post_args', 1, 3);

add_filter('dt_push_post_timeout', function($default_timeout, $post) {
  return 30;
}, 1, 2);
robbiet480 commented 6 years ago

Re: the original query about distributing with authors, I'd say that CAP could be a good stop gap solution to allow for Distributor to ensure the proper author is credited while not having to worry about things that @tlovett1 mentioned like what role should new users have and such. Furthermore, I think it would make a lot of sense to add more template tags to expose information like original author to the frontend just like how we are now able to expose original site name.

jeffpaul commented 5 years ago

I'd like to block this issue by the decision-to-be-made in #131. If that decision turns out to be that Distributor will support external plugins more than via hooks/filters, then we can continue investigating this specific issue with Co-Authors Plus and how it can best be gracefully included with/alongside Distributor. If Distributor takes the hooks/filters-only approach, then its likely we'll close this issue (and hopefully link out to an example repo for the Co-Authors Plus extension code).