Rake task for merging users

I updated the rake task so that it logs a “merge user” action instead of “delete user” in the staff logs.

6 Likes

I’m little bit confused. I don’t have users:merge task in my docker container. Does it already in v2.1.0.beta2 +16 version?

Yes, it’s part of Discourse since 2.0.0.beta4

2 Likes

Hm. Here what I have when I run rake --tasks:

rake about                                                             # List versions of all Rails frameworks and the environment
rake add_topic_to_quotes                                               # Add the topic to quotes
rake admin:create                                                      # Creates a forum administrator
rake admin:invite[email]                                               # invite an admin to this discourse instance
rake api_key:get                                                       # generate api key if missing, return existing if already there
rake app:template                                                      # Applies the template supplied by LOCATION=(/path/to/template) or URL
rake app:update                                                        # Update configs and some other initially generated files (or use just update:configs or update:bin)
rake assets:clean[keep]                                                # Remove old compiled assets
rake assets:clobber                                                    # Remove compiled assets
rake assets:environment                                                # Load asset compile environment
rake assets:precompile                                                 # Compile all the assets named in config.assets.precompile
rake assets:prestage                                                   # pre-stage assets on cdn
rake autospec                                                          # Run all specs automatically as needed
rake avatars:clean                                                     # Clean up all avatar thumbnails (use this when the thumbnail algorithm changes)
rake avatars:refresh                                                   # Refresh all avatars (download missing gravatars, refresh system)
rake build:stamp                                                       # stamp the current build with the git hash placed in version.rb
rake build_test_topic                                                  # create pushstate/replacestate test topic
rake cache_digests:dependencies                                        # Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)
rake cache_digests:nested_dependencies                                 # Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)
rake db:create[multisite]                                              # Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases
rake db:drop[multisite]                                                # Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases
rake db:environment:set[multisite]                                     # Set the environment value for the database
rake db:fixtures:load                                                  # Loads fixtures into the current environment's database
rake db:migrate[multisite]                                             # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)
rake db:migrate:status                                                 # Display status of migrations
rake db:rebuild_indexes                                                # Rebuild indexes
rake db:rollback                                                       # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rake db:schema:cache:clear                                             # Clears a db/schema_cache.yml file
rake db:schema:cache:dump                                              # Creates a db/schema_cache.yml file
rake db:schema:dump                                                    # Creates a db/schema.rb file that is portable against any DB supported by Active Record
rake db:schema:load                                                    # Loads a schema.rb file into the database
rake db:seed                                                           # Loads the seed data from db/seeds.rb
rake db:seed_fu                                                        # Loads seed data for the current environment
rake db:setup                                                          # Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)
rake db:stats                                                          # Statistics about database
rake db:structure:load                                                 # Recreates the databases from the structure.sql file
rake db:version                                                        # Retrieves the current schema version number
rake destroy:groups                                                    # Destroy all groups
rake destroy:private_messages                                          # Remove all private messages
rake destroy:stats                                                     # Destroy site stats
rake destroy:topics[category]                                          # Remove all topics in a category
rake destroy:topics_all_categories                                     # Remove all topics in all categories
rake destroy:users                                                     # Destroy all non-admin users
rake dev:cache                                                         # Toggle development mode caching on/off
rake docker:test                                                       # Run all tests (JS and code in a standalone environment)
rake emails:import                                                     # use this task to import a mailbox into Disourse
rake emails:test[email]                                                # Send email test message
rake emoji:test                                                        # test the emoji generation script
rake emoji:update                                                      # update emoji images
rake enqueue_digest_emails                                             # This task is called by the Heroku scheduler add-on
rake export:categories[category_ids]                                   # Export all the categories
rake export:category_structure[include_group_users,file_name]          # Export only the structure of all categories
rake highlightjs:update                                                # download latest version of highlight and prepare it
rake i18n:stats                                                        # show the current translation status
rake import:file[file_name]                                            # Import existing exported file
rake initializers                                                      # Print out all defined initializers in the order they are invoked by Rails
rake integration:create_fixtures                                       # Creates the integration fixtures
rake log:clear                                                         # Truncates all/specified *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)
rake middleware                                                        # Prints out your Rack middleware stack
rake multisite:generate:config                                         # generate multisite config file (if missing)
rake multisite:migrate                                                 # migrate all sites in tier
rake multisite:rollback                                                # rollback migrations for all sites in tier
rake notes                                                             # Enumerate all annotations (use notes:optimize, :fixme, :todo for focus)
rake notes:custom                                                      # Enumerate a custom annotation, specify with ANNOTATION=CUSTOM
rake plugin:install[repo]                                              # install plugin
rake plugin:install_all_official                                       # install all official plugins (use GIT_WRITE=1 to pull with write access)
rake plugin:qunit[plugin,timeout]                                      # run plugin qunit tests
rake plugin:spec[plugin]                                               # run plugin specs
rake plugin:update[plugin]                                             # update a plugin
rake plugin:update_all                                                 # update all plugins
rake poll:migrate_old_polls                                            # Migrate old polls to new syntax
rake posts:defer_all_flags                                             # Defer all flags
rake posts:delete_all_likes                                            # Delete all likes
rake posts:delete_word[find,type]                                      # Delete occurrence of a word/string
rake posts:fix_letter_avatars                                          # Rebake all posts with a quote using a letter_avatar
rake posts:normalize_code                                              # normalize all markdown so <pre><code> is not used and instead backticks
rake posts:rebake                                                      # Update each post with latest markdown
rake posts:rebake_match[pattern,type,delay]                            # Rebake all posts matching string/regex and optionally delay the loop
rake posts:refresh_emails[topic_id]                                    # Refreshes each post that was received via email
rake posts:refresh_oneboxes                                            # Update each post with latest markdown and refresh oneboxes
rake posts:remap[find,replace,type]                                    # Remap all posts matching specific string
rake posts:reorder_posts[topic_id]                                     # Reorders all posts based on their creation_date
rake qunit:test[timeout,qunit_path]                                    # Runs the qunit test suite
rake release_note:generate[from,to]                                    # generate a release note from the important commits
rake restart                                                           # Restart app by touching tmp/restart.txt
rake routes                                                            # Print out all defined routes in match order, with names
rake scheduler:run_all                                                 # run every task the scheduler knows about in that order, use only for debugging
rake secret                                                            # Generate a cryptographically secure secret key (this is typically used to generate a secret for cookie sessions)
rake site_settings:export                                              # Exports site settings
rake site_settings:import                                              # Imports site settings
rake smoke:test                                                        # run chrome headless smoke tests on current build
rake stats                                                             # Report code statistics (KLOCs, etc) from the application or engine
rake test                                                              # Runs all tests in test folder except system ones
rake test:db                                                           # Run tests quickly, but also reset db
rake test:system                                                       # Run system tests only
rake time:zones[country_or_offset]                                     # List all time zones, list by two-letter country code (`rails time:zones[US]`), or list by UTC offset (`rails time:zones[-8]`)
rake tmp:clear                                                         # Clear cache, socket and screenshot files from tmp/ (narrow w/ tmp:cache:clear, tmp:sockets:clear, tmp:screenshots:clear)
rake tmp:create                                                        # Creates tmp directories for cache, sockets, and pids
rake user_actions:rebuild                                              # rebuild the user_actions table
rake users:change_post_ownership[old_username,new_username,archetype]  # Change topic/post ownership of all the topics/posts by a specific user (without creating new revision)
rake users:update_posts[old_username,current_username]                 # Updates username in quotes and mentions
rake yarn:install                                                      # Install all JavaScript dependencies as specified via Yarn

And I rebuild discourse container in a last week.

1 Like

Oh, I see. The task doesn’t show up in that list because it doesn’t have a description. I’m going to add one.

But you should be able to execute the task as shown in the first post:

4 Likes

Here is what I have:

root@host:/var/www/discourse# rake users:merge['source','target']
rake aborted!
PG::UndefinedFunction: ОШИБКА:  функция jsonb_build_object(unknown, text, unknown, text, unknown, text, unknown, text) не существует
LINE 4:                 jsonb_build_object(
                        ^
HINT:  Функция с данными именем и типами аргументов не найдена. Возможно, вам следует добавить явные приведения типов.
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-mini-profiler-1.0.0/lib/patches/db/pg.rb:92:in `async_exec'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/rack-mini-profiler-1.0.0/lib/patches/db/pg.rb:92:in `async_exec'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/mini_sql-0.1.9/lib/mini_sql/connection.rb:104:in `run'
/var/www/discourse/vendor/bundle/ruby/2.5.0/gems/mini_sql-0.1.9/lib/mini_sql/connection.rb:76:in `exec'
/var/www/discourse/app/jobs/regular/update_username.rb:63:in `update_notifications'
/var/www/discourse/app/jobs/regular/update_username.rb:22:in `execute'
/var/www/discourse/app/services/username_changer.rb:46:in `update_username'
/var/www/discourse/app/services/user_merger.rb:30:in `update_username'
/var/www/discourse/app/services/user_merger.rb:10:in `merge!'
/var/www/discourse/lib/tasks/users.rake:47:in `block in <top (required)>'
/usr/local/bin/bundle:23:in `load'
/usr/local/bin/bundle:23:in `<main>'
Tasks: TOP => users:merge
(See full trace by running task with --trace)

It wrote: ERROR: function jsonb_build_object(unknown, text, unknown, text, unknown, text, unknown, text) doesn't exist

You need to upgrade PostgreSQL to version 9.5 or greater. The current docker container uses Postgres 10 by default.

4 Likes

Ou, I see now. Thank you very much!

@gerhard Any chance that merging users on a site with the Discourse Voting plugin will prevent one of those users from logging in with the following error and log message?

The software powering this discussion forum encountered an unexpected problem. We apologize for the inconvenience.

Disabling the plugin solves the problem, but I like the plugin, so ideas on how I could fix it are very welcome!

The line in question is at https://github.com/discourse/discourse-voting/blob/master/plugin.rb#L112 and I think I’ve fixed it at https://github.com/discourse/discourse-voting/pull/33.

5 Likes

Maybe. Plugins can subscribe to the :merging_users event. It’s possible that the Voting plugin needs to do that.

5 Likes

Just found out about this and immediately tried it out. Works like a charm. Thank you for this nice feature!

3 Likes

Is there any way to empty the second emails field? I merged two users and then later realized I should not have done so. Now I can’t create the user again because the email address is in the second emails field.

Obviously some sort of UI fix would be lovely long term for emptying/changing these addresses, but I know that’s likely a ways off. In the meantime I’d be grateful for command line steps. Thanks in advance! :seedling:

Sure, this can be done in the rails console within the container.

rails c

# restrict to secondary emails, just in case you try to delete the wrong email
UserEmail.where(email: "foo@example.com", primary: false).destroy_all

# probably not needed, but better safe than sorry
EmailToken.where(email: "foo@example.com").destroy_all
8 Likes

This is helpful. This seems like something that people will need with some frequency; it’d probably be good if this were a rake task.

2 Likes

I have spent some time using this super handy user merge rake command combined with @gerhard’s rails commands to remove secondary emails. Thank you both.

These commands have become an important and handy part of my workflow to bring back defunct users who have become unreachable, or to invite people who attend or apply for our events to join our community. Often there is a need to change the email address of the existing account, while retaining the correspondence with a new, staged user.

I do share @pfaffman’s challenge.

The answer for now seems to be to merge the user, delete the secondary email address of the remaining user using @gerhard’s rails commands, then change the email address via the web interface. (in my case it’s via wordpress SSO). Presto, done. But it’s three commands involving some copy pasting and is a bit time consuming.

Some suggestions follow. Curious to hear reactions.

How about a users:swapemail command to run before the merge?

rake users:swapemail[user1,user2]
rake users:merge[user1,user2] 

Add ability to edit secondary email addresses via the admin UI?

I have SSO on my site so can’t use this, but for those who don’t it would be useful to be able to edit the primary and secondary email addresses directly via the user admin.

Add ability to update secondary email via SSO?

This might already be possible via API, but support for a secondary email field would need to be added to the SSO provider, in my case wordpress.

Move primary email to secondary when primary email is changed?

Perhaps a simpler, more sustainable long-term solution that doesn’t require alot of other changes would be to simply change it so when a user or admin (via web or SSO) changes the primary email, it moves the old email to the secondary email field first. This could be enabled with an admin site setting.

8 Likes

Is there any risk to merging someone’s shadow/anonymous account into their account? I just did it and it seemed to work fine.

This seems incredibly helpful!

I’m trying to do the following:

root@snap-forum-app:/var/www/discourse# rake users:merge ['blob', 'tjvr'] --trace
** Invoke users:merge (first_time)
** Invoke environment (first_time)
** Execute environment
** Execute users:merge
ERROR: Expecting rake users:merge[source_username,target_username]

Any ideas on how to proceed? I’ve got SSO enabled, and am trying to merge the non-sso user into the SSO user. I have a few accounts which existed prior to enabling SSO on the forum.

Try removing the space after the comma. You also shouldn’t need quotes around the username.

6 Likes

Ah, thanks! Quotes break it… I originally tried no spaces, and no quotes, but somehow missed no quotes and no spaces… :frowning:

So, now when I run this I encounter the following exception, PostsExistError. This seems a bit odd to me, since isn’t goal to move all posts? I also ran the change ownership task for good measure and I got the same result. The admin page says this user has “-6” posts created, and none listed on the user replies view.

root@snap-forum-app:/var/www/discourse# rake users:merge[blob,tjvr] --trace
** Invoke users:merge (first_time)
** Invoke environment (first_time)
** Execute environment
** Execute users:merge
rake aborted!
UserDestroyer::PostsExistError: UserDestroyer::PostsExistError
/var/www/discourse/app/services/user_destroyer.rb:18:in `destroy'
/var/www/discourse/app/services/user_merger.rb:360:in `delete_source_user'
/var/www/discourse/app/services/user_merger.rb:22:in `merge!'
/var/www/discourse/lib/tasks/users.rake:48:in `block in <top (required)>'

Edit: Well, I went into the console and just updated the owner of 5 posts that seems to be assigned to blob’s account. Not sure what happened, but after that the merge was successful. :slight_smile:

I’m running into trouble with this command and my SSO setup, which appears to fail silently if a user changes their email address to an address that exists already in the secondary email field. The email address changes in wordpress and all appears to be well, but it does not change in discourse.

When I run @gerhard’s rails commands to remove secondary emails first, then change the email address in wordpress, it changes successfully in discourse.

The primary email address is not an issue, because if someone tries to change their email address to an address that already exists in wordpress, it doesn’t let them.

Does anyone have any suggestions? Is it possible to disable the secondary email address field, or to have the API ignore it or use it better? E.g. if the email address exists as secondary, just move what was primary to secondary and move what was secondary to primary?

1 Like