Sync WooCommerce Memberships with Discourse groups

This Plugin allows you to Sync WooCommerce Memberships with Discourse Groups.

Pre-requisites

For this to work you have to have DiscourseConnect enabled, with either Wordpress or Discourse as the DiscourseConnect provider.

Steps

  1. Install this Wordpress plugin GitHub - paviliondev/discourse-woocommerce.

  2. Use the Wordpress theme editor to update these numbers to the WooCommerce plan_id and Discourse group id and group_name you want to sync

    $member_group_map[] = (object) array('plan_id' => 51, 'group_id' => 43, 'group_name' => 'group1');
    $member_group_map[] = (object) array('plan_id' => 62, 'group_id' => 44, 'group_name' => 'group2');
    

    You can add as many entries as you like (or remove existing ones).

Optional Step

  1. Install WP Crontrol – WordPress plugin | WordPress.org and schedule run_full_wc_membership_sync to run every 24 hours. This will do a full sync to ensure consistency.

Technical Notes

The only reliable way to capture both membership creation and status change in WooCommerce is using the wc_memberships_user_membership_saved admin hook. You can use this in combination with the wc_memberships_user_membership_status_changed hook, but it should not be necessary.

This code uses the WooCommerce logger to log info to /wp-content/uploads/wc-logs/{log_file}. If it is not working check the output there. You will see a logs that look like this, showing you the stages of the logic:

2019-05-23T07:01:57+00:00 INFO Running handle_wc_membership_saved 1, 92, 1
2019-05-23T07:01:57+00:00 INFO angus@email.com membership of VIP Membership changed to wcm-active
2019-05-23T07:01:57+00:00 INFO Sending PUT request to http://localhost:3000/groups/41/members with angus@email.com
2019-05-23T07:01:57+00:00 INFO Response from Discourse: 200 OK

Usage Notes

:point_right: The Sync will not work if you click “Delete User Membership” in the Wordpress admin panel. There is no hook in WooCommerce that fires when that is clicked. Change the status of the membership instead.

19 лайков

Thanks, @angus. Funny that it seems we were both doing this at the same time.

You managed to figure out what was in those $args. I couldn’t find the user_id in it, I think.

1 лайк

It looks correct to me. Since the site is using WordPress as the SSO provider, you could use the static add_user_to_discourse_group and remove_user_from_discourse_group functions to handle changing the group status on Discourse. That would let you do something like:

	if ( in_array( $status, ACTIVE_STATUSES ) ) {
		DiscourseUtilities::add_user_to_discourse_group( $user_id, 'your_discourse_group_name' );
	} else {
		DiscourseUtilities::remove_user_from_discourse_group( $user_id, 'your_discourse_group_name' );
	}

Both of those functions take the user’s WordPress ID as the first argument, and a comma separated list of group names (with no spaces between the names) as the second argument. Those functions will take care of all the API credentials for you, so using them would simplify the code a bit. There are more details about those functions here: Manage group membership in Discourse with WP Discourse SSO.

4 лайка

In this case Discourse is the SSO Provider :slight_smile:

2 лайка

Just an update here.

The updated version of this integration has the following structure:

  1. Sync on every WooCommerce Membership status change

  2. Batch sync every relevant WooCommerce Member every 24 hours to ensure consistency.

  3. In terms of matching wordpress users with discourse users, the code first checks if the wordpress user has a discourse account associated with it. If so, it uses the discourse user id. If not it attempts the sync using the wordpress user’s email. The sync can fail if

    3.1 the user has never associated their wordpress and discourse accounts; and
    3.2 they have used different emails on wordpress and discourse.

We’re successfully running this in production on a client’s site. So far we’ve had only 2 users who have failed to sync, i.e. where both 3.1 and 3.2 were true.

Implementation

The updated implementation is

  1. Functions.php methods (update MEMBERSHIP_PLAN_ID and DISCOURSE_GROUP_ID)
Functions.php

use WPDiscourse\Utilities\Utilities as DiscourseUtilities;

const MEMBERSHIP_PLAN_ID = 61128;
const DISCOURSE_GROUP_ID = 62;
const ACTIVE_STATUSES = array(‘wcm-active’);

function update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status) {
$options = DiscourseUtilities::get_options();
$base_url = $options[‘url’];
$api_key = $options[‘api-key’];
$api_username = $options[‘publish-username’];

if ( empty( $base_url ) || empty( $api_key ) || empty( $api_username ) ) {
  return new \WP_Error( 'discourse_configuration_error', 'The WP Discourse plugin has not been properly configured.' );
}

$discourse_user_id = get_user_meta($user_id, 'discourse_sso_user_id', true);
$user_info = get_userdata($user_id);
$user_email = $user_info->user_email;
$logger = wc_get_logger();

$logger->info( sprintf('%s membership of %s is %s' , $user_email, $membership_plan_name, $status ) );

if (in_array($status, ACTIVE_STATUSES)) {
	$action = 'PUT';
} else {
	$action = 'DELETE';
}

$external_url = esc_url_raw( $base_url . "/groups/". DISCOURSE_GROUP_ID ."/members" );

$args = array(
‘api_key’ => $api_key,
‘api_username’ => $api_username
);

if ($discourse_user_id) {
$args[‘user_id’] = $discourse_user_id;
} else {
$args[‘user_emails’] = $user_email;
}

$logger->info( sprintf(‘Sending %s request to %s with %s’, $action, $external_url, http_build_query($args)) );

$external_url = add_query_arg($args, $external_url);

$response = wp_remote_request($external_url,
 array(
	 'method' => $action
 )
);

$logger->info( sprintf( 'Response from Discourse: %s %s' ,
    wp_remote_retrieve_response_code($response),
    wp_remote_retrieve_response_message($response) ) );

if ( ! DiscourseUtilities::validate( $response ) ) {

	return new \WP_Error( 'discourse_response_error', 'There has been an error in retrieving the user data from Discourse.' );
}

};

function handle_wc_membership_saved($membership_plan, $args) {
$logger = wc_get_logger();

$logger->info( sprintf('Running handle_wc_membership_saved %s, %s, %s', $args['user_id'], $args['user_membership_id'], $args['is_update'] ) );

$user_id = $args['user_id'];
$membership = wc_memberships_get_user_membership($args['user_membership_id']);
$membership_plan_id = $membership->plan->id;

if ($membership && $membership_plan_id == MEMBERSHIP_PLAN_ID) {
	$membership_plan_name = $membership_plan->name;
	$status = $membership->status;
	update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);
}

};

add_action(‘wc_memberships_user_membership_saved’, ‘handle_wc_membership_saved’, 10, 2);

function full_wc_membership_sync() {
$allusers = get_users();
$logger = wc_get_logger();

$logger->info( sprintf('Running full_wc_membership_sync') );

foreach ( $allusers as $user ) {
	$user_id = $user->id;
	$membership = wc_memberships_get_user_membership($user_id, MEMBERSHIP_PLAN_ID);
	$membership_plan_id = $membership->plan->id;

	$logger->info( sprintf('Checking membership of %s', $user->user_login) );

	if ($membership && $membership_plan_id === MEMBERSHIP_PLAN_ID) {
		$membership_plan_name = $membership->plan->name;
		$status = $membership->status;
		$logger->info( sprintf('Updating group access of %s', $user->user_login) );

		update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);

		$logger->info( sprintf('Sleeping for 5 seconds') );
		sleep(5);
	}
}

}

add_action(‘run_full_wc_membership_sync’, ‘full_wc_membership_sync’);

  1. Install WP Crontrol – WordPress plugin | WordPress.org and schedule “run_full_wc_membership_sync” to run every 24 hours.

You can review the logs of the sync in WooCommerce > Status > Logs.

@DNSTARS @pfaffman

8 лайков

What a baller, sending some magical internet money your way in thanks. :+1:

6 лайков

Much appreciated! Thank you @DNSTARS

1 лайк

Какие изменения мне нужно внести в моем случае? У меня немного другая ситуация. Вот отличия:

  1. У меня WordPress работает как SSO-клиент, пользователи входят через Discourse.
  2. Я установил плагин WP ‘WooCommerce Groups’: Groups WooCommerce Documentation - WooCommerce. Он похож на WooCommerce Memberships, но с меньшим количеством функций. Думаю, все основные функции одинаковы.
  3. У меня несколько групп в Discourse (более 3), поэтому в WP тоже 3 группы доступа.

Можете ли вы предоставить исправления для этого кода или какие-либо советы? Спасибо!
Возможно, вы сможете помочь? @pfaffman @simon

Ах, я не задал вопрос там…
Итак, мне нужно активировать эту интеграцию между WP и Discourse.

Что работает:

  1. WP работает как SSO-клиент.
  2. Членства продаются через плагин WP Groups и истекают => пользователи добавляются в необходимые группы.
  3. Пользователи удаляются из групп по истечении срока.

Мне нужно:

  • Когда пользователь добавляется в группу в WP, мне нужно добавить этого пользователя в соответствующую группу в Discourse.
  • Когда пользователь удаляется из этой группы в WP — удалить его в Discourse.
    Несколько групп в WP и Discourse.

Я заметил, что функция update_discourse_group_access добавляет учётные данные аутентификации API в URL. Это не будет работать ни в одной актуальной версии Discourse. У вас есть обновлённая версия кода, использующая аутентификацию на основе заголовков? Если нет, я могу отредактировать код, но не смогу проверить свои правки, так что это немного рискованно.

У меня есть чистые сайты Discourse и WordPress, поэтому вы можете протестировать свой код без какого-либо риска.

2 лайка

Вот код Ангуса, обновлённый для использования аутентификации на основе заголовков. Я не вносил в него никаких других изменений. Также я не тестировал этот код. Я не буду обновлять первое сообщение, пока кто-нибудь не сможет его протестировать.

use WPDiscourse\Utilities\Utilities as DiscourseUtilities;

const MEMBERSHIP_PLAN_ID = 35;
const DISCOURSE_GROUP_ID = 41;
const ACTIVE_STATUSES    = array( 'wcm-active' );

function update_discourse_group_access( $user_id, $membership_plan_id, $membership_plan_name, $status ) {
	$options      = DiscourseUtilities::get_options();
	$base_url     = $options['url'];
	$api_key      = $options['api-key'];
	$api_username = $options['publish-username'];

	if ( empty( $base_url ) || empty( $api_key ) || empty( $api_username ) ) {
		return new \WP_Error( 'discourse_configuration_error', 'Плагин WP Discourse настроен неправильно.' );
	}

	$user_info         = get_userdata( $user_id );
	$user_email        = $user_info->user_email;
	$logger            = wc_get_logger();

	$logger->info( sprintf( 'Членство %s в плане %s изменено на %s', $user_email, $membership_plan_name, $status ) );

	if ( in_array( $status, ACTIVE_STATUSES ) ) {
		$action = 'PUT';
	} else {
		$action = 'DELETE';
	}

	$external_url = esc_url_raw( $base_url . "/groups/" . DISCOURSE_GROUP_ID . "/members" );

	$logger->info( sprintf( 'Отправка запроса %s на %s с %s', $action, $external_url, $user_email ) );

	$response = wp_remote_request( $external_url,
		array(
			'method'  => $action,
			'headers' => array(
				'Api-Key'      => sanitize_key( $api_key ),
				'Api-Username' => sanitize_text_field( $api_username ),
			),
			'body'    => array( 'user_emails' => $user_email ),
		)
	);

	$logger->info( sprintf( 'Ответ от Discourse: %s %s',
		wp_remote_retrieve_response_code( $response ),
		wp_remote_retrieve_response_message( $response ) ) );

	if ( ! DiscourseUtilities::validate( $response ) ) {

		return new \WP_Error( 'discourse_response_error', 'Произошла ошибка при получении данных пользователя из Discourse.' );
	}
}

function handle_wc_membership_saved( $membership_plan, $args ) {
	$logger = wc_get_logger();

	$logger->info( sprintf( 'Выполняется handle_wc_membership_saved %s, %s, %s', $args['user_id'], $args['user_membership_id'], $args['is_update'] ) );

	$user_id            = $args['user_id'];
	$membership         = wc_memberships_get_user_membership( $args['user_membership_id'] );
	$membership_plan_id = $membership->plan->id;

	if ( $membership && $membership_plan_id == MEMBERSHIP_PLAN_ID ) {
		$membership_plan_name = $membership_plan->name;
		$status               = $membership->status;
		update_discourse_group_access( $user_id, $membership_plan_id, $membership_plan_name, $status );
	}
}
add_action( 'wc_memberships_user_membership_saved', 'handle_wc_membership_saved', 10, 2 );

В коде устанавливается переменная $discourse_user_id, но она нигде не используется. Её, вероятно, можно удалить из кода.

Я не уверен в этом. Код подключается к хуку действия wc_memberships_user_membership_saved. Вероятно, этот хук добавляется плагином WooCommerce Memberships. В плагине WooCommerce Groups, скорее всего, есть аналогичный хук, но маловероятно, что он будет иметь такое же имя и параметры.

3 лайка

Плагин «Groups for Woocommerce» использует плагин «WP Group» для работы функций групп. Вот его API: Actions | Itthinx Documentation

Что касается Groups WooCommerce:

Действия

Расширение в настоящее время не вызывает никаких конкретных действий.

Чтобы упростить задачу, я перешёл на WooCommerce Memberships и использовал исходный код с изменениями от @simon.

Я заменил идентификаторы на свои данные:

const MEMBERSHIP_PLAN_ID = 347;
const DISCOURSE_GROUP_ID = 42;

На данный момент синхронизация не работает, но в логе появляется следующая информация:

> 2020-05-14T13:12:34+00:00 INFO test@gmail.com членство в PAID1 изменено на wcm-active
> 2020-05-14T13:12:34+00:00 INFO Отправка PUT-запроса на https://site/groups/42/members с test@gmail.com
> 2020-05-14T13:12:34+00:00 INFO Ответ от Discourse: 400 Bad Request

Идентификатор группы Discourse я нашёл здесь:
site/groups/PAID1.json

> {"group":{"id":42,"automatic":false,"name":"PAID1","user_count":0,"mentionable_level":0,"messageable_level":0,"visibility_level":0,"automatic_membership_email_domains":"","primary_group":false,"title":"","grant_trust_level":null,"incoming_email":null,"has_messages":false,"flair_url":"","flair_bg_color":"","flair_color":"","bio_raw":"","bio_cooked":null,"bio_excerpt":null,"public_admission":false,"public_exit":false,"allow_membership_requests":false,"full_name":"France-PAID1","default_notification_level":3,"membership_request_template":"","is_group_user":false,"is_group_owner":true,"members_visibility_level":0,"can_see_members":true,"publish_read_state":false,"is_group_owner_display":false,"mentionable":true,"messageable":false},"extras":{"visible_group_names":["admins","moderators","PAID1","PAID2","PAID3","staff","trust_level_0","trust_level_1","trust_level_2","trust_level_3","trust_level_4"]}}
2 лайка

Итак, остаются два открытых вопроса:

  1. Как синхронизировать без ошибок?
  2. Что делать, если у меня в WP более 3 типов участников, а в Discourse — более 3 групп? Этот код предназначен только для одного типа.

Похоже, это связано с ошибкой в моём обновлении кода. Я разберусь с этим сегодня позже.

2 лайка

Я обновил пример кода здесь: Sync WooCommerce Memberships with Discourse groups - #11 by simon. В коде, который я опубликовал вчера, была ошибка кодировки. Я проверил, что API-вызов в коде работает корректно, но не тестировал сам код с плагином Woocommerce Memberships.

Это должно быть возможно. Кто-то может помочь вам здесь, но у вас, возможно, будет больше шансов, если вы наймете кого-то для выполнения этой работы. Вы можете создать тему в нашей категории Marketplace, чтобы найти разработчика для выполнения работы.

2 лайка

@simon, получилось! Спасибо, команда! :vulcan_salute:

1 лайк

@simon, в вашем коде я не нашёл части, отвечающей за пакетную синхронизацию, которую использовал @angus.

/*run_full_wc_membership_sync*/
function full_wc_membership_sync() {
$allusers = get_users();
$logger = wc_get_logger();

$logger->info( sprintf('Выполняется run_full_wc_membership_sync') );

foreach ( $allusers as $user ) {
	$user_id = $user->id;
	$membership = wc_memberships_get_user_membership($user_id, MEMBERSHIP_PLAN_ID);
	$membership_plan_id = $membership->plan->id;

	$logger->info( sprintf('Проверка членства для %s', $user->user_login) );

	if ($membership && $membership_plan_id === MEMBERSHIP_PLAN_ID) {
		$membership_plan_name = $membership->plan->name;
		$status = $membership->status;
		$logger->info( sprintf('Обновление доступа к группе для %s', $user->user_login) );

		update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);

		$logger->info( sprintf('Пауза на 5 секунд') );
		sleep(5);
	}
}

}

add_action('run_full_wc_membership_sync', 'full_wc_membership_sync');  

Должен ли я добавить этот код в конец вашего, если хочу активировать ежедневную синхронизацию?

Когда я пытаюсь добавить это событие планировщика в WP через плагин WordPress Crontrol с именем хука:

run_full_wc_membership_sync

получаю ошибку:

Что может быть её причиной?

Решено. Может помочь кому-то. Мне нужно было выбрать «В» в поле «Следующий запуск» и указать там дату, например, 2020-05-10 10:00:00.

Протестировал синхронизацию — теперь всё работает.

Вот рабочее решение, если у вас есть несколько планов членства WooCommerce и групп в Discourse. Вам нужно заменить идентификаторы ваших планов членства и идентификаторы групп Discourse на свои.

  1. Идентификаторы планов членства WooCommerce можно найти в URL при редактировании плана в WordPress: https://site/wp-admin/post.php?post=**441**&action=edit
  2. Идентификаторы групп Discourse можно найти здесь: https://site/groups/group_name.json

Рабочий код для включения в functions.php:

Сводка

//wp+discourse
use WPDiscourse\Utilities\Utilities as DiscourseUtilities;

const MEMBERSHIP_PLAN_DISCOURSE_GROUP = [
“347” => “42”,
“357” => “43”,
“441” => “44”
];

//const ACTIVE_STATUSES = array( ‘wcm-active’ );
const ACTIVE_STATUSES = array( ‘wcm-active’, ‘wcm-free_trial’ );

function update_discourse_group_access( $user_id, $membership_plan_id, $membership_plan_name, $status ) {
$options = DiscourseUtilities::get_options();
$base_url = $options[‘url’];
$api_key = $options[‘api-key’];
$api_username = $options[‘publish-username’];

if ( empty( $base_url ) || empty( $api_key ) || empty( $api_username ) ) {
	return new \WP_Error( 'discourse_configuration_error', 'Плагин WP Discourse настроен неправильно.' );
}

$user_info         = get_userdata( $user_id );
$user_email        = $user_info->user_email;
$logger            = wc_get_logger();

$logger->info( sprintf( 'Членство %s в плане %s изменено на %s', $user_email, $membership_plan_name, $status ) );

if ( in_array( $status, ACTIVE_STATUSES ) ) {
	$action = 'PUT';
} else {
	$action = 'DELETE';
}

$external_url = esc_url_raw( $base_url . "/groups/" . MEMBERSHIP_PLAN_DISCOURSE_GROUP[$membership_plan_id] . "/members" );

$logger->info( sprintf( 'Отправка запроса %s на %s с %s', $action, $external_url, $user_email ) );

$response = wp_remote_request( $external_url,
	array(
		'method'  => $action,
		'headers' => array(
			'Api-Key'      => sanitize_key( $api_key ),
			'Api-Username' => sanitize_text_field( $api_username ),
		),
		'body'    => array( 'user_emails' => $user_email ),
	)
);

$logger->info( sprintf( 'Ответ от Discourse: %s %s',
	wp_remote_retrieve_response_code( $response ),
	wp_remote_retrieve_response_message( $response ) ) );

if ( ! DiscourseUtilities::validate( $response ) ) {

	return new \WP_Error( 'discourse_response_error', 'Произошла ошибка при получении данных пользователя из Discourse.' );
}

}

function handle_wc_membership_saved( $membership_plan, $args ) {
$logger = wc_get_logger();

$logger->info( sprintf( 'Выполнение handle_wc_membership_saved %s, %s, %s', $args['user_id'], $args['user_membership_id'], $args['is_update'] ) );

$user_id            = $args['user_id'];

$membership         = wc_memberships_get_user_membership( $args['user_membership_id'] );

$membership_plan_id = $membership->plan->id;

if ( $membership && isset(MEMBERSHIP_PLAN_DISCOURSE_GROUP[$membership_plan_id])) {
	$membership_plan_name = $membership_plan->name;
	$status               = $membership->status;
	update_discourse_group_access( $user_id, $membership_plan_id, $membership_plan_name, $status );
}

}
add_action( ‘wc_memberships_user_membership_saved’, ‘handle_wc_membership_saved’, 10, 2 );

/run_full_wc_membership_sync/
function full_wc_membership_sync() {
$allusers = get_users();
$logger = wc_get_logger();

$logger->info( sprintf('Выполнение full_wc_membership_sync') );

foreach ( $allusers as $user ) {

   $user_id = $user->id;

   $membership = wc_memberships_get_user_membership($user_id);

   $membership_plan_id = $membership->plan->id;

   $logger->info( sprintf('Проверка членства %s', $user->user_login) );

   if ($membership  && isset(MEMBERSHIP_PLAN_DISCOURSE_GROUP[$membership_plan_id])) {
       
	  $membership_plan_name = $membership->plan->name;
      
	  $status = $membership->status;
      
	  $logger->info( sprintf('Обновление доступа к группе для %s', $user->user_login) );

	  update_discourse_group_access($user_id, $membership_plan_id, $membership_plan_name, $status);

	  $logger->info( sprintf('Пауза на 5 секунд') );
      
	  sleep(5);
   }
}

}

add_action(‘run_full_wc_membership_sync’, ‘full_wc_membership_sync’);

Вам нужно изменить в первой колонке — идентификаторы планов членства, во второй колонке — идентификаторы групп Discourse.

Сводка
                                    "347"  => "42",
                                    "357"  => "43",
                                    "441"  => "44"
7 лайков

Привет, @Ed_Bobkov! Надеюсь, ты сможешь мне помочь. Вот несколько фактов о текущей ситуации:

  1. У меня WordPress настроен как SSO-клиент: пользователи могут входить через Discourse или WordPress.
  2. В данный момент я не использую плагин «подписки» на WordPress.
  3. В Discourse будут десятки групп для каждого типа участников.
  4. Я планировал вручную создавать новую группу подписки в WordPress и соответствующую группу в Discourse каждый раз, когда появляется новый тип подписки для пользователей.
  5. Как ты и сказал: «Мне нужно активировать эту интеграцию между WordPress и Discourse».
  6. Я предполагаю, что мне понадобится плагин «WooCommerce Memberships»?

Какие мои следующие шаги?