[dw-free] Allow importing of your journal from another LiveJournal-based site.
[commit: http://hg.dwscoalition.org/dw-free/rev/821e7159136b]
http://bugs.dwscoalition.org/show_bug.cgi?id=114
More work on the importer. Enable importing friend groups, friends, and
entries. This is still very experimental and needs a way to bubble up
errors to the user.
Patch by
mark.
http://bugs.dwscoalition.org/show_bug.cgi?id=114
More work on the importer. Enable importing friend groups, friends, and
entries. This is still very experimental and needs a way to bubble up
errors to the user.
Patch by
![[staff profile]](https://www.dreamwidth.org/img/silk/identity/user_staff.png)
-------------------------------------------------------------------------------- diff -r 913ec83595d8 -r 821e7159136b bin/upgrading/update-db-general.pl --- a/bin/upgrading/update-db-general.pl Tue Feb 24 09:09:34 2009 +0000 +++ b/bin/upgrading/update-db-general.pl Wed Feb 25 10:25:53 2009 +0000 @@ -3008,6 +3008,21 @@ CREATE TABLE import_data ( ) EOC +# we don't store this in userprops because we need to index this +# backwards and load it easily... +register_tablecreate("import_usermap", <<'EOC'); +CREATE TABLE import_usermap ( + hostname VARCHAR(255), + username VARCHAR(255), + identity_userid INT UNSIGNED, + feed_userid INT UNSIGNED, + + PRIMARY KEY (hostname, username), + INDEX (identity_userid), + INDEX (feed_userid) +) +EOC + # NOTE: new table declarations go ABOVE here ;) diff -r 913ec83595d8 -r 821e7159136b cgi-bin/DW/Worker/ContentImporter.pm --- a/cgi-bin/DW/Worker/ContentImporter.pm Tue Feb 24 09:09:34 2009 +0000 +++ b/cgi-bin/DW/Worker/ContentImporter.pm Wed Feb 25 10:25:53 2009 +0000 @@ -83,91 +83,6 @@ sub merge_watch { } -=head2 C<< $class->post_event( $user, $hashref, $event, $item_errors ) >> - -$event is a hashref representation of a single entry, with the following format: - - { - subject => 'My Entry', - event => 'I DID STUFF!!!!!', - security => 'usemask', - allowmask => 1, - - eventtime => 'yyyy-mm-dd hh:mm:ss', - props => { - heres_a_userprop => "there's a userprop", - and_another_little => "userprop", - } - key => 'some_uniqe_key', # generally the permalink to the old entry, otherwise something unique (across *all* import sources possible) - url => 'http://permalink.tld/', # permalink to the old entry - } - -$item_errors is an arrayref of errors to be formatted nicely with a link to old and new entries. - -=cut -sub post_event { - my ( $class, $u, $opts, $evt, $item_errors ) = @_; - - return if $opts->{entry_map}->{$evt->{key}}; - - my ( $yr, $month, $day, $hr, $min, $sec ) = $evt->{eventtime} =~ m/([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})/; - my %proto = ( - lineendings => 'unix', - subject => $evt->{subject}, - event => $evt->{event}, - security => $evt->{security}, - allowmask => $evt->{allowmask}, - - year => $yr, - mon => $month, - day => $day, - hour => $hr, - min => $min, - ); - - my $props = $evt->{props}; - - # this is a list of props that actually exist on this site - # but have been shown to cause failures importing that entry. - my %bad_props = ( - current_coords => 1, - ); - foreach my $prop ( keys %$props ) { - my $p = LJ::get_prop( "log", $prop ); - - # skip over system and non-existant props - next unless $p; - next if ( $p->{ownership} eq 'system' ); - next if ( $bad_props{$prop} ); - - $proto{"prop_$prop"} = $props->{$prop}; - }; - - # Overwrite these here in case we're importing from an imported journal (hey, it could happen) - $proto{prop_opt_backdated} = '1'; - $proto{prop_import_source} = $evt->{key}; - - my %res; - LJ::do_request({ 'mode' => 'postevent', - 'user' => $u->{'user'}, - 'ver' => $LJ::PROTOCOL_VER, - %proto }, - \%res, { 'u' => $u, - 'noauth' => 1, }); - - my $errors = $opts->{errors}; - if ( $res{success} eq 'FAIL' ) { - push @$errors, "Entry from $evt->{url}: $res{errmsg}"; - } else { - my $itemid = $res{itemid}; - $u->do( "UPDATE log2 SET logtime = ? where journalid = ? and jitemid = ?", undef, $evt->{realtime}, $u->userid, $itemid ); - $opts->{entry_map}->{$evt->{key}} = $itemid; - foreach my $err ( @$item_errors ) { - push @$errors, "Entry at $res{url}: $err ( from $evt->{url} )"; - } - } -} - =head2 C<< $class->post_event( $user, $hashref, $comment ) >> $event is a hashref representation of a single comment, with the following format: @@ -228,31 +143,6 @@ sub insert_comment { my $jtalkid = LJ::Talk::Post::enter_imported_comment( $u, $parent, $item, $comment, $date, \$errref ); return undef unless $jtalkid; return $jtalkid; -} - -=head2 C<< $class->get_entry_map( $user, $hashref ) - -Returns a hashref mapping import_source keys to jitemids - -=cut -sub get_entry_map { - my ( $class, $u, $opts ) = @_; - return $opts->{entry_map} if $opts->{entry_map}; - - my $p = LJ::get_prop( "log", "import_source" ); - return {} unless $p; - - my $dbr = LJ::get_cluster_reader( $u ); - my %map; - my $sth = $dbr->prepare( "SELECT jitemid, value FROM logprop2 WHERE journalid = ? AND propid = ?" ); - - $sth->execute( $u->id, $p->{id} ); - - while ( my ( $jitemid, $value ) = $sth->fetchrow_array ) { - $map{$value} = $jitemid; - } - - return \%map; } =head2 C<< $class->get_comment_map( $user, $hashref ) @@ -389,4 +279,4 @@ sub ok { } -1; \ No newline at end of file +1; diff -r 913ec83595d8 -r 821e7159136b cgi-bin/DW/Worker/ContentImporter/LiveJournal.pm --- a/cgi-bin/DW/Worker/ContentImporter/LiveJournal.pm Tue Feb 24 09:09:34 2009 +0000 +++ b/cgi-bin/DW/Worker/ContentImporter/LiveJournal.pm Wed Feb 25 10:25:53 2009 +0000 @@ -316,25 +316,56 @@ sub get_feed_account_from_url { return undef; } +sub get_remapped_userids { + my ( $class, $data, $user ) = @_; + + return @{ $MAPS{$data->{hostname}}->{$user} } + if exists $MAPS{$data->{hostname}}->{$user}; + + my $dbh = LJ::get_db_writer() + or return; + my ( $oid, $fid ) = $dbh->selectrow_array( + 'SELECT identity_userid, feed_userid FROM import_usermap WHERE hostname = ? AND username = ?', + undef, $data->{hostname}, $user + ); + + unless ( defined $oid ) { + warn "[$$] Remapping identity userid of $data->{hostname}:$user\n"; + $oid = $class->remap_username_friend( $data, $user ); + warn " IDENTITY USERID IS STILL UNDEFINED\n" + unless defined $oid; + } + + unless ( defined $fid ) { + warn "[$$] Remapping feed userid of $data->{hostname}:$user\n"; + $fid = $class->remap_username_feed( $data, $user ); + warn " FEED USERID IS STILL UNDEFINED\n" + unless defined $fid; + } + + $dbh->do( 'REPLACE INTO import_usermap (hostname, username, identity_userid, feed_userid) VALUES (?, ?, ?, ?)', + undef, $data->{hostname}, $user, $oid, $fid ); + $MAPS{$data->{hostname}}->{$user} = [ $oid, $fid ]; + + return ( $oid, $fid ); +} + sub remap_username_feed { my ( $class, $data, $username ) = @_; # canonicalize username and try to return $username =~ s/-/_/g; - return $MAPS{feed_map}->{$username} - if defined $MAPS{feed_map}->{$username}; # don't allow identity accounts (they're not feeds by default) return undef if $username =~ m/^ext_/; # fall back to getting it from the ATOM data - my $url = "http://$data->{hostname}/~$username/data/atom"; + my $url = "http://www.$data->{hostname}/~$username/data/atom"; my $acct = $class->get_feed_account_from_url( $data, $url, $username ) or return undef; - # store it and return - return $MAPS{feed_map}->{$username} = $acct; + return $acct; } sub remap_username_friend { @@ -343,9 +374,6 @@ sub remap_username_friend { # canonicalize username, in case they gave us a URL version, convert it to # the one we know sites use $username =~ s/-/_/g; - - return $MAPS{friend_map}->{$username} - if defined $MAPS{friend_map}->{$username}; if ( $username =~ m/^ext_/ ) { my $ua = LJ::get_useragent( @@ -366,35 +394,33 @@ sub remap_username_friend { if ( $url =~ m!http://(.+)\.$LJ::DOMAIN\/$! ) { # this appears to be a local user! # Map this to the local userid in feed_map too, as this is a local user. - my $luid = LJ::User->new_from_url( $url )->id; - $MAPS{feed_map}->{$username} = $luid; - return $luid; + return LJ::User->new_from_url( $url )->id; } my $iu = LJ::User::load_identity_user( 'O', $url, undef ) or return undef; - $MAPS{friend_map}->{$username} = $iu->userid; + return $iu->id; } else { my $url_prefix = "http://$data->{hostname}/~" . $username; my ( $foaf_items ) = $class->get_foaf_from( $url_prefix ) or return undef; + # if they don't have an identity section (but foaf was successful + # or we would have returned undef above), then they are a community + # or some other account without. return 0 to signify this. my $ident = $foaf_items->{identity}->{url} - or return undef; - $MAPS{ident_map}->{$username} = $ident; + or return 0; my $iu = LJ::User::load_identity_user( 'O', $ident, undef ) or return undef; - $MAPS{friend_map}->{$username} = $iu->userid; + return $iu->id; } - - return $MAPS{friend_map}->{$username}; } sub remap_lj_user { - my ( $class, $server, $event ) = @_; - $event =~ s/(<lj.+?(user|comm|syn)=["']?(.+?)["' ]?>)/<lj site="$server" $2="$3">/gi; + my ( $class, $data, $event ) = @_; + $event =~ s/(<lj.+?(user|comm|syn)=["']?(.+?)["' ]?>)/<lj site="$data->{hostname}" $2="$3">/gi; return $event; } @@ -422,6 +448,7 @@ sub xmlrpc_call_helper { my $res; eval { $res = $xmlrpc->call($method, $req); }; if ( $res && $res->fault ) { + warn "XMLRPC fault: " . join( ', ', map { "$_:" . $res->fault->{$_} } keys %{$res->fault || {}} ) . "\n"; return { fault => 1 }; } diff -r 913ec83595d8 -r 821e7159136b cgi-bin/DW/Worker/ContentImporter/LiveJournal/Bio.pm --- a/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Bio.pm Tue Feb 24 09:09:34 2009 +0000 +++ b/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Bio.pm Wed Feb 25 10:25:53 2009 +0000 @@ -60,7 +60,7 @@ sub try_work { DW::Worker::ContentImporter::Local::Bio->merge_interests( $u, $interests ); - $items->{bio} = $class->remap_lj_user( $data->{hostname}, $items->{bio} ); + $items->{bio} = $class->remap_lj_user( $data, $items->{bio} ); DW::Worker::ContentImporter::Local::Bio->merge_bio_items( $u, $items ); return $ok->(); diff -r 913ec83595d8 -r 821e7159136b cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm --- a/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm Tue Feb 24 09:09:34 2009 +0000 +++ b/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm Wed Feb 25 10:25:53 2009 +0000 @@ -45,30 +45,24 @@ sub try_work { # setup my $u = LJ::load_userid( $data->{userid} ) or return $fail->( 'Unable to load target with id %d.', $data->{userid} ); + my $entry_map = DW::Worker::ContentImporter::Local::Entries->get_entry_map( $u ); - # temporary failure, this code hasn't been ported yet - return $fail->( 'oops, not ready yet' ); -} - -1; -__END__ - -### WORK GOES HERE - $opts->{entry_map} = DW::Worker::ContentImporter->get_entry_map($u,$opts); - my $synccount = 0; - my $lastsync = 0; - my %sync; - while (1) { - DW::Worker::ContentImporter->ratelimit_request( $opts ); - my $hash = call_xmlrpc( $opts, 'syncitems', {lastsync => $lastsync} ); + # load the syncitems list; but never try to load the same lastsync time twice, just + # in case + my ( $lastsync, %tried_syncs, %sync ); + while ( $tried_syncs{$lastsync} < 2 ) { + warn "[$$] Attempting lastsync = " . ( $lastsync || 'undef' ) . "\n"; + my $hash = $class->call_xmlrpc( $data, 'syncitems', { lastsync => $lastsync } ); foreach my $item ( @{$hash->{syncitems} || []} ) { - next unless $item->{item} =~ /L-(\d+)/; - $synccount++; + next unless $item->{item} =~ /^L-(\d+)$/; $sync{$1} = [ $item->{action}, $item->{time} ]; - $lastsync = $item->{time} if $item->{'time'} gt $lastsync; + $lastsync = $item->{time} + if !defined $lastsync || $item->{time} gt $lastsync; + $tried_syncs{$lastsync}++; } + warn " count $hash->{count} == total $hash->{total}\n"; last if $hash->{count} == $hash->{total}; } @@ -77,30 +71,44 @@ __END__ return $sync{$id}->[1] if @{$sync{$id} || []}; }; - my $lastgrab = 0; - while (1) { + # now get the actual events + while ( scalar( keys %sync ) > 0 ) { my $count = 0; - DW::Worker::ContentImporter->ratelimit_request( $opts ); - my $hash = call_xmlrpc( $opts, 'getevents', { selecttype => 'syncitems', lastsync => $lastgrab, ver => 1, lineendings => 'unix', }); + + # calculate what time to get entries for + my @keys = sort { $sync{$a}->[1] cmp $sync{$b}->[1] } keys %sync; + my $lastgrab = LJ::mysql_time( LJ::mysqldate_to_time( $sync{$keys[0]}->[1] ) - 1 ); + + warn "[$$] Fetching from lastsync = $lastgrab forward\n"; + my $hash = $class->call_xmlrpc( $data, 'getevents', + { + ver => 1, + lastsync => $lastgrab, + selecttype => 'syncitems', + lineendings => 'unix', + } + ); foreach my $evt ( @{$hash->{events} || []} ) { $count++; + $evt->{realtime} = $realtime->( $evt->{itemid} ); - $lastgrab = $evt->{realtime} if $evt->{realtime} gt $lastgrab; $evt->{key} = $evt->{url}; # skip this if we've already dealt with it before - next if $opts->{entry_map}->{$evt->{key}}; + warn " [$evt->{itemid}] $evt->{url} // $evt->{realtime} // map=$entry_map->{$evt->{key}}\n"; + my $sync = delete $sync{$evt->{itemid}}; + next if $entry_map->{$evt->{key}} || !defined $sync; + # clean up event for LJ - my @item_errors; # remap friend groups my $allowmask = $evt->{allowmask}; - my $newmask = remap_groupmask( $opts, $allowmask ); + my $newmask = $class->remap_groupmask( $data, $allowmask ); - # Bah. Assume private. This shouldn't relaly happen, but - # a good sanity check. + # if we are unable to determine a good groupmask, then fall back to making + # the entry private and mark the error. if ( $allowmask != 1 && $newmask == 1 ) { $newmask = 0; push @item_errors, "Could not determine groups."; @@ -128,19 +136,26 @@ __END__ push @item_errors, "Entry contained a template tag, please manually re-add the templated content."; } - $evt->{event} = remap_lj_user( $opts, $event ); + $evt->{event} = $class->remap_lj_user( $data, $event ); # actually post it - DW::Worker::ContentImporter->post_event( $u, $opts, $evt, \@item_errors ); + my $res = DW::Worker::ContentImporter::Local::Entries->post_event( $data, $entry_map, $u, $evt, \@item_errors ); + +# FIXME: do something with the return code and @item_errors ... other than +# printing them to STDERR of course ... + if ( $res ) { + warn " imported!\n"; + } else { + warn " failed!\n"; + } + warn " $_\n" foreach @item_errors; } - last unless $count && $lastgrab; + warn " count = $count && lastgrab = $lastgrab\n"; } - - $opts->{no_entries} = 1; return $ok->(); } -1; \ No newline at end of file +1; diff -r 913ec83595d8 -r 821e7159136b cgi-bin/DW/Worker/ContentImporter/LiveJournal/Friends.pm --- a/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Friends.pm Tue Feb 24 09:09:34 2009 +0000 +++ b/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Friends.pm Wed Feb 25 10:25:53 2009 +0000 @@ -51,23 +51,31 @@ sub try_work { my ( @friends, @feeds ); foreach my $friend (@{ $r->{friends} || [] }) { - my $local_userid = $class->remap_username_friend( $data, $friend->{username} ); + my ( $local_oid, $local_fid ) = $class->get_remapped_userids( $data, $friend->{username} ); + push @friends, { - userid => $local_userid, + userid => $local_oid, groupmask => $class->remap_groupmask( $data, $friend->{groupmask} ), - } if $local_userid; + } if $local_oid; - $local_userid = $class->remap_username_feed( $data, $friend->{username} ); push @feeds, { fgcolor => $friend->{fgcolor}, bgcolor => $friend->{bgcolor}, - userid => $local_userid, - } if $local_userid; + userid => $local_fid, + } if $local_fid; } DW::Worker::ContentImporter->merge_trust( $u, $opts, \@friends ); DW::Worker::ContentImporter->merge_watch( $u, $opts, \@feeds ); + # schedule events import + my $dbh = LJ::get_db_writer(); + $dbh->do( + q{UPDATE import_items SET status = 'ready' + WHERE userid = ? AND item = 'lj_entries' AND import_data_id = ? AND status = 'init'}, + undef, $u->id, $opts->{import_data_id} + ); + return $ok->(); } diff -r 913ec83595d8 -r 821e7159136b cgi-bin/DW/Worker/ContentImporter/Local/Entries.pm --- a/cgi-bin/DW/Worker/ContentImporter/Local/Entries.pm Tue Feb 24 09:09:34 2009 +0000 +++ b/cgi-bin/DW/Worker/ContentImporter/Local/Entries.pm Wed Feb 25 10:25:53 2009 +0000 @@ -26,7 +26,135 @@ DW::Worker::ContentImporter::Local::Entr These functions are part of the Saving API for entries. +=head2 C<< $class->get_entry_map( $user, $hashref ) + +Returns a hashref mapping import_source keys to jitemids + =cut +sub get_entry_map { + my ( $class, $u ) = @_; -1; \ No newline at end of file + my $p = LJ::get_prop( "log", "import_source" ); + return {} unless $p; + + my $dbr = LJ::get_cluster_reader( $u ); + my %map; + my $sth = $dbr->prepare( "SELECT jitemid, value FROM logprop2 WHERE journalid = ? AND propid = ?" ); + + $sth->execute( $u->id, $p->{id} ); + + while ( my ( $jitemid, $value ) = $sth->fetchrow_array ) { + $map{$value} = $jitemid; + } + + return \%map; +} + +=head2 C<< $class->post_event( $hashref, $u, $event, $item_errors ) >> + +$event is a hashref representation of a single entry, with the following format: + + { + # standard event values + subject => 'My Entry', + event => 'I DID STUFF!!!!!', + security => 'usemask', + allowmask => 1, + eventtime => 'yyyy-mm-dd hh:mm:ss', + props => { + heres_a_userprop => "there's a userprop", + and_another_little => "userprop", + } + + # the key is a uniquely opaque string that identifies this entry. this must be + # unique across all possible import sources. the permalink may work best. + key => 'some_unique_key', + + # a url to this entry's original location + url => 'http://permalink.tld/', + } + +$item_errors is an arrayref of errors to be formatted nicely with a link to old and new entries. + +Returns 1 on success, undef on error. + +=cut + +sub post_event { + my ( $class, $data, $map, $u, $evt, $errors ) = @_; + + return if $map->{$evt->{key}}; + + my ( $yr, $month, $day, $hr, $min, $sec ) = ( $1, $2, $3, $4, $5, $6 ) + if $evt->{eventtime} =~ m/(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/; + + my %proto = ( + lineendings => 'unix', + subject => $evt->{subject}, + event => $evt->{event}, + security => $evt->{security}, + allowmask => $evt->{allowmask}, + + year => $yr, + mon => $month, + day => $day, + hour => $hr, + min => $min, + ); + + my $props = $evt->{props}; + + # this is a list of props that actually exist on this site + # but have been shown to cause failures importing that entry. + my %bad_props = ( + current_coords => 1, + personifi_word_count => 1, + personifi_lang => 1, + ); + foreach my $prop ( keys %$props ) { + next if $bad_props{$prop}; + + my $p = LJ::get_prop( "log", $prop ) + or next; + next if $p->{ownership} eq 'system'; + + $proto{"prop_$prop"} = $props->{$prop}; + }; + + # Overwrite these here in case we're importing from an imported journal (hey, it could happen) + $proto{prop_opt_backdated} = '1'; + $proto{prop_import_source} = $evt->{key}; + + my %res; + LJ::do_request( + { + mode => 'postevent', + user => $u->user, + ver => $LJ::PROTOCOL_VER, + %proto, + }, + \%res, + { + u => $u, + noauth => 1, + } + ); + + if ( $res{success} eq 'FAIL' ) { + push @$errors, "Entry from $evt->{url}: $res{errmsg}"; + return undef; + + } else { + $u->do( "UPDATE log2 SET logtime = ? where journalid = ? and jitemid = ?", + undef, $evt->{realtime}, $u->userid, $res{itemid} ); + $map->{$evt->{key}} = $res{itemid}; + return 1; + + } + + # flow will never get here +} + + +1; diff -r 913ec83595d8 -r 821e7159136b htdocs/misc/import.bml --- a/htdocs/misc/import.bml Tue Feb 24 09:09:34 2009 +0000 +++ b/htdocs/misc/import.bml Wed Feb 25 10:25:53 2009 +0000 @@ -63,11 +63,15 @@ body<= ['lj_userpics', 'ready'], ['lj_bio', 'ready'], ['lj_tags', 'ready'], + ['lj_friendgroups', 'ready'], + ['lj_friends', 'init'], + ['lj_entries', 'init'], ); # schedule userpic, bio, and tag imports foreach my $item (@jobs) { $dbh->do( - "INSERT INTO import_items (userid, item, status, created, import_data_id, priority) VALUES (?, ?, ?, UNIX_TIMESTAMP(), ?, UNIX_TIMESTAMP())", + "INSERT INTO import_items (userid, item, status, created, import_data_id, priority) " . + "VALUES (?, ?, ?, UNIX_TIMESTAMP(), ?, UNIX_TIMESTAMP())", undef, $u->id, $item->[0], $item->[1], $id ); return "Database error." if $dbh->err; @@ -80,7 +84,8 @@ body<= my $dbh = LJ::get_db_writer() or return "No database."; my $imps = $dbh->selectall_arrayref( - 'SELECT import_data_id, hostname, username, password_md5 FROM import_data WHERE userid = ? ORDER BY import_data_id DESC LIMIT 10', + 'SELECT import_data_id, hostname, username, password_md5 FROM import_data WHERE userid = ? ' . + 'ORDER BY import_data_id DESC LIMIT 3', undef, $u->id ); @@ -109,7 +114,7 @@ body<= foreach my $impid ( sort { $b <=> $a } keys %s ) { my $h = $s{$impid}; $ret .= "<tr><td colspan='4'> </td></tr>"; - $ret .= "<tr><td colspan='4' style='background-color: #cccccc;'>$refresh <strong>$h->{host}</strong> | $h->{user} | $h->{pw}</td></tr>"; + $ret .= "<tr><td colspan='4' style='background-color: #cccccc;'>$refresh <strong>$h->{host}</strong> | $h->{user}</td></tr>"; foreach my $item ( sort keys %{$h->{items}} ) { my $i = $h->{items}->{$item}; $ret .= "<tr><td><em>$item</em></td>"; @@ -132,7 +137,8 @@ body<= $ret .= "<select name='hostname'><option value='livejournal.com'>LiveJournal.com</option></select><br />"; $ret .= "<br />"; $ret .= "Clicking submit below will cause import jobs to be queued to import your userpics, your "; - $ret .= "tags, and your bio fields. You can check this page for status."; + $ret .= "tags, and your bio fields. You can check this page for status.<br />"; + $ret .= "<strong>This option now imports your friend groups, friends, and entries.</strong><br />"; $ret .= "<input type='submit' value='Do it!'></form>"; return $ret; } --------------------------------------------------------------------------------