fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2010-10-01 05:27 pm

[dw-free] New dversion for userpics to make renaming userpics faster

[commit: http://hg.dwscoalition.org/dw-free/rev/f897918203ac]

http://bugs.dwscoalition.org/show_bug.cgi?id=3064

Update userpic schema and add a migration tool to convert users from the old
to the new. We'll be rolling this out slowly and with a lot of checks to
make sure we get it right.

Patch by [personal profile] exor674.

Files modified:
  • bin/upgrading/d8d9-userpicrename.pl
  • bin/upgrading/proplists.dat
  • bin/upgrading/update-db-general.pl
  • cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm
  • cgi-bin/DW/User/DVersion/Migrate8To9.pm
  • cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm
  • cgi-bin/DW/Worker/ContentImporter/Local/Comments.pm
  • cgi-bin/LJ/Comment.pm
  • cgi-bin/LJ/Constants.pm
  • cgi-bin/LJ/Entry.pm
  • cgi-bin/LJ/S2.pm
  • cgi-bin/LJ/S2/EntryPage.pm
  • cgi-bin/LJ/S2/ReplyPage.pm
  • cgi-bin/LJ/Talk.pm
  • cgi-bin/LJ/User.pm
  • cgi-bin/LJ/Userpic.pm
  • cgi-bin/ljlib.pl
  • cgi-bin/ljprotocol.pl
  • htdocs/admin/entryprops.bml
  • htdocs/talkpost.bml
  • htdocs/talkread.bml
--------------------------------------------------------------------------------
diff -r ebea5b13e8a4 -r f897918203ac bin/upgrading/d8d9-userpicrename.pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/upgrading/d8d9-userpicrename.pl	Sat Oct 02 01:27:29 2010 +0800
@@ -0,0 +1,148 @@
+#!/usr/bin/perl
+#
+# This code was forked from the LiveJournal project owned and operated
+# by Live Journal, Inc. The code has been modified and expanded by
+# Dreamwidth Studios, LLC. These files were originally licensed under
+# the terms of the license supplied by Live Journal, Inc, which can
+# currently be found at:
+#
+# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
+#
+# In accordance with the original license, this code and all its
+# modifications are provided under the GNU General Public License.
+# A copy of that license can be found in the LICENSE file included as
+# part of this distribution.
+#
+# Goes over every user, updating their dversion to 9 and
+# moves userpicmap2 over to userpicmap3
+#
+use strict;
+use warnings;
+BEGIN {
+    use lib "$ENV{'LJHOME'}/cgi-bin/";
+    require "ljlib.pl";
+}
+use Term::ReadLine;
+use Getopt::Long;
+use DW::User::DVersion::Migrate8To9;
+
+my $BLOCK_SIZE = 10_000; # get users in blocks of 10,000
+my $VERBOSE    = 0;      # print out extra info
+my $need_help;
+my @cluster;
+my @users;
+my $endtime;
+
+my $help = <<"END";
+Usage: $0 [options]
+Options:
+    --cluster=N Specify user cluster to work on (by default, all clusters)
+    --hours=N   Work no more than N hours (by default, work until all is done)
+    --user=N    Specify users to migrate (by default, all users on the specified clusters)
+    --verbose   Be noisy
+    --help      Print this help and exit
+END
+
+GetOptions(
+    "help"      => \$need_help,
+    "cluster=i" => \@cluster,
+    "user=s"    => \@users,
+    "verbose"   => \$VERBOSE,
+    "hours=i"   => sub { $endtime = $_[1]*3600+time(); },
+) or die $help;
+
+if ( $need_help ) {
+    print $help;
+    exit(0);
+}
+
+unless ( @cluster ) {
+    no warnings 'once';
+    @cluster = ( 0, @LJ::CLUSTERS );
+}
+
+my $dbh = LJ::get_db_writer()
+    or die "Could not connect to global master";
+
+my $users = join( ', ', map { $dbh->quote($_) } @users );
+
+my $term = new Term::ReadLine 'd8-d9 migrator';
+my $line = $term->readline( "Do you want to update to dversion 9 (userpicmap3)? [N/y] " );
+unless ( $line =~ /^y/i ) {
+    print "Not upgrading to dversion 9\n\n";
+    exit;
+}
+
+print "\n--- Upgrading users to dversion (userpicmap3) ---\n\n";
+
+# get user count
+my $total = $dbh->selectrow_array( "SELECT COUNT(*) FROM user WHERE dversion = 8" );
+print "\tTotal users at dversion 8: $total\n\n";
+
+my $migrated = 0;
+my $flag_stop_work = 0;
+
+MAIN_LOOP:
+foreach my $cid ( @cluster ) {
+
+    while ( 1 ) {
+        my $sth;
+        if ( @users ) {
+            $sth = $dbh->prepare( "SELECT userid FROM user WHERE dversion=8 AND clusterid=? AND user IN ($users) LIMIT $BLOCK_SIZE" );
+        } else {
+            $sth = $dbh->prepare( "SELECT userid FROM user WHERE dversion=8 AND clusterid=? LIMIT $BLOCK_SIZE" );
+        }
+        $sth->execute( $cid );
+        die $sth->errstr if $sth->err;
+
+        my $count = $sth->rows;
+        print "\tGot $count users on cluster $cid with dversion=8\n";
+        last unless $count;
+       
+        local( $SIG{TERM}, $SIG{INT}, $SIG{HUP} );
+        $SIG{TERM} = $SIG{INT} = $SIG{HUP} = sub { $flag_stop_work = 1; };
+        while ( my ( $userid ) = $sth->fetchrow_array ) {
+            if ( $flag_stop_work ) {
+                warn "Exiting by signal...";
+                last MAIN_LOOP;
+            }
+            if ( $endtime && time()>$endtime ) {
+                warn "Exiting by time condition...";
+                last MAIN_LOOP;
+            }
+
+            my $u = LJ::load_userid( $userid )
+                or die "Invalid userid: $userid";
+             
+            if ( $u->is_expunged ) {
+                ## special case: expunged (deleted) users
+                ## just update dbversion, don't move or delete(?) data
+                $u->update_self( { 'dversion' => 9 } );
+                print "\tUpgrading version of deleted user $u->{user}\n" if $VERBOSE;
+                $migrated++;
+            } else {
+                # lock while upgrading
+                my $lock = LJ::locker()->trylock( "d8d9-$userid" );
+                unless ( $lock ) {
+                    print STDERR "Could not get a lock for user " . $u->user . ".\n";
+                    next;
+                }
+
+                my $ok = eval { $u->upgrade_to_dversion_9 };
+                die $@ if $@;
+
+                print "\tMigrated user " . $u->user . "... " . ($ok ? 'ok' : 'ERROR') . "\n"
+                    if $VERBOSE;
+
+                $migrated++ if $ok;
+            }
+        }
+
+        print "\t - Migrated $migrated users so far\n\n";
+
+        # make sure we don't end up running forever for whatever reason
+        last if $migrated > $total;
+    }
+}
+
+print "--- Done migrating $migrated of $total users to dversion 9 ---\n";
diff -r ebea5b13e8a4 -r f897918203ac bin/upgrading/proplists.dat
--- a/bin/upgrading/proplists.dat	Sat Oct 02 00:29:19 2010 +0800
+++ b/bin/upgrading/proplists.dat	Sat Oct 02 01:27:29 2010 +0800
@@ -1161,6 +1161,11 @@ talkproplist.picture_keyword:
   des: A keyword that should align to a defined picture
   prettyname: Picture Keyword
 
+talkproplist.picture_mapid:
+  datatype: num
+  des: A keyword that should align to a mapid
+  prettyname: Picture Keyword MapID
+
 talkproplist.poster_ip:
   datatype: char
   des: The poster's IP address, optionally logged.
@@ -1324,6 +1329,13 @@ logproplist.picture_keyword:
   sortorder: 30
   ownership: user
 
+logproplist.picture_mapid:
+  datatype: num
+  des: A keyword that should align to a mapid
+  prettyname: Picture Keyword MapID
+  sortorder: 30
+  ownership: system
+
 logproplist.revnum:
   datatype: num
   des: Number of times this post has been edited.
diff -r ebea5b13e8a4 -r f897918203ac bin/upgrading/update-db-general.pl
--- a/bin/upgrading/update-db-general.pl	Sat Oct 02 00:29:19 2010 +0800
+++ b/bin/upgrading/update-db-general.pl	Sat Oct 02 01:27:29 2010 +0800
@@ -554,6 +554,20 @@ CREATE TABLE userpicmap2 (
     picid int(10) unsigned NOT NULL default '0',
 
     PRIMARY KEY  (userid, kwid)
+)
+EOC
+
+register_tablecreate("userpicmap3", <<'EOC');
+CREATE TABLE userpicmap3 (
+    userid int(10) unsigned NOT NULL default '0',
+    mapid int(10) unsigned NOT NULL,
+    kwid int(10) unsigned,
+    picid int(10) unsigned,
+    redirect_mapid int(10) unsigned,
+
+    PRIMARY KEY (userid, mapid),
+    UNIQUE KEY  (userid, kwid),
+    INDEX redirect (userid, redirect_mapid)
 )
 EOC
 
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm
--- a/cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -315,9 +315,12 @@ sub entry_to_req {
     my $entryprops = $entry->props;
     $req->{props} = {};
     # only bring over these properties
-    for my $entrykey (qw ( adult_content current_coords current_location current_music opt_backdated opt_nocomments opt_noemail opt_preformatted opt_screening picture_keyword taglist used_rte pingback )) {
+    for my $entrykey (qw ( adult_content current_coords current_location current_music opt_backdated opt_nocomments opt_noemail opt_preformatted opt_screening taglist used_rte pingback )) {
         $req->{props}->{$entrykey} = $entryprops->{$entrykey} if defined $entryprops->{$entrykey};
     }
+
+    # and regenerate this one from data
+    $req->{props}->{picture_keyword} = $entry->userpic_kw;
 
     # figure out what current_moodid and current_mood to pass to the crossposted entry
     my ( $siteid, $moodid, $mood ) = ( $extacct->siteid, $entryprops->{current_moodid}, $entryprops->{current_mood} );
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/DW/User/DVersion/Migrate8To9.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/User/DVersion/Migrate8To9.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -0,0 +1,265 @@
+#!/usr/bin/perl
+#
+# DW::User::DVersion::Migrate8To9 - Handling dversion 8 to 9 migration
+#
+# Authors:
+#      Andrea Nall <anall@andreanall.com>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+
+package DW::User::DVersion::Migrate8To9;
+
+use strict;
+use warnings;
+use LJ::User;
+use Time::HiRes qw( usleep );
+
+my $readonly_bit;
+
+# find readonly cap class, complain if not found
+foreach (keys %LJ::CAP) {
+    if ($LJ::CAP{$_}->{'_name'} eq "_moveinprogress" &&
+        $LJ::CAP{$_}->{'readonly'} == 1) {
+        $readonly_bit = $_;
+        last;
+    }
+}
+unless (defined $readonly_bit) {
+    die "Won't move user without %LJ::CAP capability class named '_moveinprogress' with readonly => 1\n";
+}
+
+my $logpropid = LJ::get_prop( log => 'picture_keyword' )->{id};
+my $talkpropid = LJ::get_prop( talk => 'picture_keyword' )->{id};
+
+my $logpropid_map = LJ::get_prop( log => 'picture_mapid' )->{id};
+my $talkpropid_map = LJ::get_prop( talk => 'picture_mapid' )->{id};
+
+sub do_upgrade {
+    my $BLOCK_INSERT =   25;
+
+    my ( $u ) = @_;
+
+    return 0 if $u->readonly;
+
+    return 1 if $u->dversion >= 9;
+
+    # we really cannot have the user doing things during this process
+    $u->modify_caps( [$readonly_bit], [] );
+
+    # wait a quarter of a second, give any request the user might be doing a chance to stop
+    # as the user changing things could lead to slight data loss re. userpic selection
+    # on entries and comments
+    usleep( 250000 );
+
+    # do this in an eval so, in case something dies, we don't leave the user locked
+    my $rv = 0;
+
+    eval {
+        # Unfortunately, we need to iterate over all clusters to get a list
+        # of used keywords so we can give proper ids to everything,
+        # even removed keywords
+        my %keywords;
+        my %to_update;
+        if ( $u->is_individual ) {
+            foreach my $cluster_id ( @LJ::CLUSTERS ) {
+                my $dbcm_o = LJ::get_cluster_master( $cluster_id );
+
+                my $entries = $dbcm_o->selectall_arrayref(q{
+                    SELECT log2.journalid AS journalid, 
+                           log2.jitemid AS jitemid,
+                           logprop2.value AS value
+                    FROM logprop2 
+                    INNER JOIN log2 
+                        ON ( logprop2.journalid = log2.journalid 
+                            AND logprop2.jitemid = log2.jitemid ) 
+                    WHERE posterid = ? 
+                        AND propid=?
+                    }, undef, $u->id, $logpropid);
+                die $dbcm_o->errstr if $dbcm_o->err;
+                my $comments = $dbcm_o->selectall_arrayref( q{
+                    SELECT talkprop2.journalid AS journalid,
+                           talkprop2.jtalkid AS jtalkid,
+                           talkprop2.value AS value
+                    FROM talkprop2
+                    INNER JOIN talk2
+                        ON ( talkprop2.journalid = talk2.journalid
+                            AND talkprop2.jtalkid = talk2.jtalkid )
+                    WHERE posterid = ?
+                        AND tpropid=?
+                    }, undef, $u->id, $talkpropid);
+                die $dbcm_o->errstr if $dbcm_o->err;
+
+                $to_update{$cluster_id} = {
+                    entries => $entries,
+                    comments => $comments,
+                };
+                $keywords{$_->[2]}->{count}++ foreach ( @$entries, @$comments );
+            }
+        }
+
+        my $origmap = $u->selectall_hashref( q{
+            SELECT kwid, picid FROM userpicmap2 WHERE userid=?
+            }, "kwid", undef, $u->id);
+        die $u->errstr if $u->err;
+
+        my $picmap = $u->selectall_hashref( q{
+            SELECT picid, state FROM userpic2 WHERE userid=?
+            }, "picid", undef, $u->id);
+        die $u->errstr if $u->err;
+
+        my %outrows;
+
+        my %kwid_map;
+
+        foreach my $k ( keys %keywords ) {
+            if ( $k =~ m/^pic#(\d+)$/ ) {
+                my $picid = $1;
+                next if ! exists $picmap->{$picid} || $picmap->{$picid}->{state} eq 'X';
+                $keywords{$k}->{kwid} = undef;
+                $keywords{$k}->{picid} = $picid;
+                $outrows{$picid}->{0}++;
+            } else {
+                my $kwid = $u->get_keyword_id($k,1);
+                $kwid_map{$kwid} = $k;
+                my $picid = $origmap->{$kwid}->{picid};
+                $keywords{$k}->{kwid} = $kwid;
+                $keywords{$k}->{picid} = $picid;
+                $outrows{$picid || 0}->{$kwid}++;
+            }
+        }
+
+        foreach my $r ( values %$origmap ) {
+            $outrows{$r->{picid}}->{$r->{kwid}}++ if $r->{picid} && $r->{kwid};
+        }
+
+        {
+            my ( @bind, @vals );
+
+            # flush rows to destination table
+            my $flush = sub {
+                return unless @bind;
+
+                # insert data
+                my $bind = join( ",", @bind );
+                $u->do( "REPLACE INTO userpicmap3 (userid,mapid,kwid,picid) VALUES $bind", undef, @vals );
+                die $u->errstr if $u->err;
+
+                # reset values
+                @bind = ();
+                @vals = ();
+            };
+
+            foreach my $picid ( sort { $a <=> $b } keys %outrows ) {
+                foreach my $kwid ( sort { $a <=> $b } keys %{$outrows{$picid}} ) {
+                    next if $kwid == 0 && $picid == 0;
+                    push @bind, "(?,?,?,?)";
+                    my $mapid = LJ::alloc_user_counter( $u, 'Y' );
+                    my $keyword = $kwid == 0 ? "pic#$picid" : $kwid_map{$kwid};
+                    # if $keyword is undef, this isn't used on any entries, so we don't care about the mapid
+                    # however, if $kwid is undef, this is a pic#xxx keyword, and had to have existed on an entry
+                    $keywords{$keyword}->{mapid} = $mapid if defined $keyword;
+                    push @vals, ( $u->id, $mapid, $kwid || undef, $picid || undef );
+                    $flush->() if @bind > $BLOCK_INSERT;
+                }
+            }
+            $flush->();
+        }
+
+        if ( $u->is_individual ) {
+            foreach my $cluster_id ( @LJ::CLUSTERS ) {
+                next unless $to_update{$cluster_id};
+                my $data = $to_update{$cluster_id};
+
+                my $dbcm_o = LJ::get_cluster_master($cluster_id);
+
+                {
+                    my ( @bind, @vals );
+
+                    # flush rows to destination table
+                    my $flush = sub {
+                        return unless @bind;
+
+                        # insert data
+                        my $bind = join( ",", @bind );
+                        $dbcm_o->do( "REPLACE INTO logprop2 (journalid,jitemid,propid,value) VALUES $bind", undef, @vals );
+                        die $u->errstr if $u->err;
+
+                        # reset values
+                        @bind = ();
+                        @vals = ();
+                    };
+
+                    foreach my $entry ( @{ $data->{entries} } ) {
+                        next unless $keywords{$entry->[2]}->{mapid};
+                        push @bind, "(?,?,?,?)";
+                        push @vals, ( $entry->[0], $entry->[1], $logpropid_map, $keywords{$entry->[2]}->{mapid} );
+                        $flush->() if @bind > $BLOCK_INSERT;
+                    }
+                    $flush->();
+
+                    foreach my $entry ( @{ $data->{entries} } ) {
+                        LJ::MemCache::delete([$entry->[0],"logprop:". $entry->[0] .":". $entry->[1]]);
+                    }
+                }
+                {
+                    my ( @bind, @vals );
+
+                    # flush rows to destination table
+                    my $flush = sub {
+                        return unless @bind;
+
+                        # insert data
+                        my $bind = join( ",", @bind );
+                        $dbcm_o->do( "REPLACE INTO talkprop2 (journalid,jtalkid,tpropid,value) VALUES $bind", undef, @vals );
+                        die $u->errstr if $u->err;
+
+                        # reset values
+                        @bind = ();
+                        @vals = ();
+                    };
+
+                    foreach my $comment ( @{ $data->{comments} } ) {
+                        next unless $keywords{$comment->[2]}->{mapid};
+                        push @bind, "(?,?,?,?)";
+                        push @vals, ( $comment->[0], $comment->[1], $talkpropid_map, $keywords{$comment->[2]}->{mapid} );
+                        $flush->() if @bind > $BLOCK_INSERT;
+                    }
+                    $flush->();
+
+                    foreach my $comment ( @{ $data->{comments} } ) {
+                        LJ::MemCache::delete([$comment->[0],"talkprop:". $comment->[0] .":". $comment->[1]]);
+                    }
+                }
+            }
+        }
+
+        $rv = 1;
+    };
+
+    my $err = $@;
+
+    # okay, we're done, the user can do things again
+    $u->modify_caps( [], [$readonly_bit] );
+
+    die $err if $err;
+
+    return $rv;
+}
+
+sub upgrade_to_dversion_9 {
+    # If user has been purged, go ahead and update version
+    # Otherwise move their data
+    my $ok = $_[0]->is_expunged ? 1 : do_upgrade(@_);
+
+    $_[0]->update_self( { 'dversion' => 9 } ) if $ok;
+
+    LJ::Userpic->delete_cache( $_[0] );
+
+    return $ok;
+}
+
+*LJ::User::upgrade_to_dversion_9 = \&upgrade_to_dversion_9;
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm
--- a/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -207,7 +207,12 @@ sub try_work {
             # local picture keyword
             if ( my $jitemid = $entry_map->{$evt->{key}} ) {
                 my $entry = LJ::Entry->new( $u, jitemid => $jitemid );
-                $entry->set_prop( picture_keyword => $evt->{props}->{picture_keyword} );
+                my $kw = $evt->{props}->{picture_keyword};
+                if ( $u->userpic_have_mapid ) {
+                    $entry->set_prop( picture_mapid => $u->get_mapid_from_keyword( $kw, create => 1) );
+                } else {
+                    $entry->set_prop( picture_keyword => $kw );
+                }
             }
 
             # now try to skip it
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/DW/Worker/ContentImporter/Local/Comments.pm
--- a/cgi-bin/DW/Worker/ContentImporter/Local/Comments.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/DW/Worker/ContentImporter/Local/Comments.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -71,7 +71,12 @@ sub update_comment {
     # edits and such.  for now, I'm just trying to get the icons to update...
     my $c = LJ::Comment->instance( $u, jtalkid => $cmt->{id} )
         or return $$errref = 'Unable to instantiate LJ::Comment object.';
-    $c->set_prop( picture_keyword => $cmt->{props}->{picture_keyword} );
+    my $pu = $c->poster;
+    if ( $pu && $pu->userpic_have_mapid ) {
+        $c->set_prop( picture_mapid => $u->get_mapid_from_keyword( $cmt->{props}->{picture_keyword}, create => 1 ) );
+    } else {
+        $c->set_prop( picture_keyword => $cmt->{props}->{picture_keyword} );
+    }
 }
 
 
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Comment.pm
--- a/cgi-bin/LJ/Comment.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Comment.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -26,6 +26,14 @@ use lib "$LJ::HOME/cgi-bin";
 
 require "htmlcontrols.pl";
 use LJ::Talk;
+
+=head1 NAME
+
+LJ::Comment
+
+=head1 CLASS METHODS
+
+=cut
 
 # internal fields:
 #
@@ -218,6 +226,9 @@ sub create {
 
 }
 
+=head1 INSTANCE METHODS
+
+=cut
 
 sub absorb_row {
     my ($self, %row) = @_;
@@ -303,24 +314,6 @@ sub edit_url {
     my $url     = $entry->url;
 
     return "$url?edit=$dtalkid";
-}
-
-# return img tag of userpic that the comment poster used
-sub poster_userpic {
-    my $self = $_[0];
-    my $pic_kw = $self->prop('picture_keyword');
-    my $posteru = $self->poster;
-
-    # anonymous poster, no userpic
-    return "" unless $posteru;
-
-    # new from keyword falls back to the default userpic if
-    # there was no keyword, or if the keyword is no longer used
-    my $pic = LJ::Userpic->new_from_keyword($posteru, $pic_kw);
-    return $pic->imgtag_nosize if $pic;
-
-    # no userpic with comment
-    return "";
 }
 
 # return LJ::User of journal comment is in
@@ -1383,10 +1376,8 @@ sub _format_mail_both {
 
     if ($is_html) {
         my $pichtml;
-        my $pic_kw = $self->prop('picture_keyword');
-
         if ( $posteru ) {
-            my $pic = LJ::Userpic->new_from_keyword( $posteru, $pic_kw ) || $posteru->userpic;
+            my ( $pic, $pic_kw ) = $self->userpic;
 
             if ( $pic && $pic->load_row ) {
                 $pichtml = "<img src=\"$LJ::USERPIC_ROOT/$pic->{picid}/$pic->{userid}\" align='absmiddle' ".
@@ -1553,20 +1544,45 @@ sub is_text_spam($\$) {
     return 0; # normal text
 }
 
-# returns a LJ::Userpic object for the poster of the comment, or undef
-# it will unify interface between Entry and Comment: $foo->userpic will
-# work correctly for both Entry and Comment objects
+=head2 C<< $cmt->userpic >>
+
+Returns a LJ::Userpic object for the poster of the comment, or undef.
+
+If called in a list context, returns ( LJ::Userpic object, keyword )
+
+=cut
 sub userpic {
     my $self = $_[0];
 
     my $up = $self->poster;
     return unless $up;
 
-    my $key = $self->prop('picture_keyword');
-
     # return the picture from keyword, if defined
     # else return poster's default userpic
-    return LJ::Userpic->new_from_keyword( $up, $key ) || $up->userpic;
+    my $kw = $_[0]->userpic_kw;
+    my $pic = LJ::Userpic->new_from_keyword( $up, $kw ) || $up->userpic;
+
+    return wantarray ? ( $pic, $kw ) : $pic;
+}
+
+=head2 C<< $cmt->userpic_kw >>
+
+Returns the userpic keyword used on this comment, or undef.
+
+=cut
+sub userpic_kw {
+    my $self = $_[0];
+
+    my $up = $self->poster;
+    return unless $up;
+
+    if ( $up->userpic_have_mapid ) {
+        my $mapid = $self->prop('picture_mapid');
+
+        return $up->get_keyword_from_mapid( $mapid ) if $mapid;
+    } else {
+        return $self->prop('picture_keyword');
+    }
 }
 
 sub poster_ip {
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Constants.pm
--- a/cgi-bin/LJ/Constants.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Constants.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -68,11 +68,12 @@ use constant CMAX_UPIC_DESCRIPTION => 12
 #    6: clustered memories, friend groups, and keywords (for memories)
 #    7: clustered userpics, keyword limiting, and comment support
 #    8: clustered polls
+#    9: userpicmap3, with mapid
 #
 # Dreamwidth installations should ALL be dversion >= 8.  We do not support anything
 # else and are ripping out code to support all previous dversions.
 #
-use constant MAX_DVERSION => 8;
+use constant MAX_DVERSION => 9;
 $LJ::MAX_DVERSION = MAX_DVERSION;
 
 1;
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Entry.pm
--- a/cgi-bin/LJ/Entry.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Entry.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -21,6 +21,14 @@ use strict;
 use strict;
 use vars qw/ $AUTOLOAD /;
 use Carp qw/ croak confess /;
+
+=head1 NAME
+
+LJ::Entry
+
+=head1 CLASS METHODS
+
+=cut
 
 # internal fields:
 #
@@ -186,6 +194,10 @@ sub new_from_row {
 
     return $self;
 }
+
+=head1 INSTANCE METHODS
+
+=cut
 
 # returns true if entry currently exists.  (it's possible for a given
 # $u, to make a fake jitemid and that'd be a valid skeleton LJ::Entry
@@ -854,35 +866,54 @@ sub tag_map {
     return $tags->{$self->jitemid} || {};
 }
 
-# returns a LJ::Userpic object for this post, or undef
-# currently this is for the permalink view, not for the friends view
-# context.  TODO: add a context option for friends page, and perhaps
+=head2 C<< $entry->userpic >>
+
+Returns a LJ::Userpic object for this post, or undef.
+
+If called in a list context, returns ( LJ::Userpic object, keyword )
+
+See userpic_kw.
+
+=cut
+# FIXME: add a context option for friends page, and perhaps
 # respect $remote's userpic viewing preferences (community shows poster
 # vs community's picture)
 sub userpic {
-    my $self = shift;
+    my $up = $_[0]->poster;
+    my $kw = $_[0]->userpic_kw;
+    my $pic = LJ::Userpic->new_from_keyword( $up, $kw ) || $up->userpic;
+
+    return wantarray ? ( $pic, $kw ) : $pic;
+}
+
+=head2 C<< $entry->userpic_kw >>
+
+Returns the keyword to use for the entry.
+
+If a keyword is specified, it uses that, otherwise
+it tries the custom mood text, followed by the standard mood.
+
+=cut
+sub userpic_kw {
+    my $self = $_[0];
 
     my $up = $self->poster;
+    
+    my $key;
+    # try their entry-defined userpic keyword
+    if ( $up->userpic_have_mapid ) {
+        my $mapid = $self->prop('picture_mapid');
 
-    # try their entry-defined userpic keyword, then their custom
+        $key = $up->get_keyword_from_mapid( $mapid ) if $mapid;
+    } else {
+        $key = $self->prop('picture_keyword');
+    }
+
+    # ... but if that fails, then their custom
     # mood, then their standard mood
-    my $key = $self->prop('picture_keyword') ||
-        $self->prop('current_mood') ||
+    return $key || $self->prop('current_mood') ||
         DW::Mood->mood_name( $self->prop('current_moodid') );
-
-    # return the picture from keyword, if defined
-    # else return poster's default userpic
-    return LJ::Userpic->new_from_keyword( $up, $key ) || $up->userpic;
 }
-
-sub userpic_kw_from_props {
-    my ($class, $props) = @_;
-
-    return $props->{'picture_keyword'} ||
-        $props->{'current_mood'} ||
-        DW::Mood->mood_name( $props->{'current_moodid'} );
-}
-
 
 # returns true if the user is allowed to share an entry via Tell a Friend
 # $u is the logged-in user
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/S2.pm
--- a/cgi-bin/LJ/S2.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/S2.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -1960,11 +1960,11 @@ sub Entry_from_entryobj
 
     # loading S2 Userpic
     my $userpic;
-    my $kw = $entry_obj->userpic_kw_from_props( $entry_obj->props );
+    my ( $pic, $kw ) = $entry_obj->userpic;
 
     # if the post was made in a community, use either the userpic it was posted with or the community pic depending on the style setting
     if ( $posterid == $journalid || !S2::get_property_value($opts->{ctx}, 'use_shared_pic') ) {
-        $userpic = Image_userpic( $poster, $entry_obj->userpic->picid, $kw ) if $entry_obj->userpic;
+        $userpic = Image_userpic( $poster, $pic->picid, $kw ) if $pic;
     } else {
         $userpic = Image_userpic( $journal, $journal->userpic->picid ) if $journal->userpic;
     }
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/S2/EntryPage.pm
--- a/cgi-bin/LJ/S2/EntryPage.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/S2/EntryPage.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -181,7 +181,7 @@ sub EntryPage
                     $height = $height / 2;
                 }
 
-                $comment_userpic = Image_userpic( $com->{upost}, $com->{picid}, $com->{props}->{picture_keyword}, 
+                $comment_userpic = Image_userpic( $com->{upost}, $com->{picid}, $com->{pickw}, 
                                                   $width, $height );
             }
 
@@ -226,7 +226,7 @@ sub EntryPage
                 '_type' => 'Comment',
                 'journal' => $userlite_journal,
                 'metadata' => {
-                    'picture_keyword' => $com->{'props'}->{'picture_keyword'},
+                    'picture_keyword' => $com->{pickw},
                 },
                 'permalink_url' => "$permalink?thread=$dtalkid" . LJ::Talk::comment_anchor( $dtalkid ),
                 'reply_url' => $reply_url,
@@ -496,8 +496,8 @@ sub EntryPage_entry
     
     # load the userpic; include the keyword selected by the user
     # as a backup for the alttext
-    my $pickw = LJ::Entry->userpic_kw_from_props($entry->props);
-    my $userpic = Image_userpic($pu, $entry->userpic ? $entry->userpic->picid : 0, $pickw);
+    my ( $pic, $pickw ) = $entry->userpic;
+    my $userpic = Image_userpic($pu, $pic ? $pic->picid : 0, $pickw);
 
     my $comments = CommentInfo( $entry->comment_info(
         u => $u, remote => $remote, style_args => $style_args, viewall => $viewall
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/S2/ReplyPage.pm
--- a/cgi-bin/LJ/S2/ReplyPage.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/S2/ReplyPage.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -118,8 +118,8 @@ sub ReplyPage
         $comment_values{subject} = $comment->subject_orig;
         $comment_values{body} = $comment->body_orig;
         $comment_values{subjecticon} = $comment->prop('subjecticon');
-        $comment_values{prop_picture_keyword} = $comment->prop('picture_keyword');
         $comment_values{prop_opt_preformatted} = $comment->prop('opt_preformatted');
+        $comment_values{prop_picture_keyword} = $comment->userpic_kw;
     }
 
     if ($replytoid) {
@@ -130,6 +130,8 @@ sub ReplyPage
             return;
         }
 
+        # FIXME: Why are we loading the comment manually when we do LJ::Comment->new below
+        # and could do everything through there.
         my $sql = "SELECT jtalkid, posterid, state, datepost FROM talk2 ".
             "WHERE journalid=$u->{'userid'} AND jtalkid=$re_talkid ".
             "AND nodetype='L' AND nodeid=" . $entry->jitemid;
@@ -177,16 +179,17 @@ sub ReplyPage
         }
 
         my $datetime = DateTime_unix(LJ::mysqldate_to_time($parpost->{'datepost'}));
-
-        my ($s2poster, $pu);
+        
         my $comment_userpic;
-        if ($parpost->{'posterid'}) {
-            $pu = LJ::load_userid($parpost->{'posterid'});
+        my $s2poster;
+        
+        my $pu = $parentcomment->poster;
+        if ( $pu ) {
             return $opts->{handler_return} = 403 if $pu->is_suspended; # do not show comments by suspended users
             $s2poster = UserLite($pu);
 
-            my $pickw = LJ::Entry->userpic_kw_from_props($parpost->{'props'});
-            $comment_userpic = Image_userpic($pu, 0, $pickw);
+            my ( $pic, $pickw ) = $parentcomment->userpic;
+            $comment_userpic = Image_userpic($pu, $pic ? $pic->picid : 0, $pickw);
         }
 
         LJ::CleanHTML::clean_comment(\$parpost->{'body'},
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Talk.pm
--- a/cgi-bin/LJ/Talk.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Talk.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -1147,28 +1147,38 @@ sub load_comments
     }
 
     # optionally give them back user refs
-    if (ref($opts->{'userref'}) eq "HASH") {
+    if (ref($opts->{userref}) eq "HASH") {
         my %userpics = ();
         # copy into their ref the users we've already loaded above.
         while (my ($k, $v) = each %up) {
-            $opts->{'userref'}->{$k} = $v;
+            $opts->{userref}->{$k} = $v;
         }
 
         # optionally load userpics
-        if (ref($opts->{'userpicref'}) eq "HASH") {
+        if (ref($opts->{userpicref}) eq "HASH") {
             my @load_pic;
             foreach my $talkid (@posts_to_load) {
                 my $post = $posts->{$talkid};
-                my $kw;
-                if ($post->{'props'} && $post->{'props'}->{'picture_keyword'}) {
-                    $kw = $post->{'props'}->{'picture_keyword'};
+                my $pu = $opts->{userref}->{$post->{posterid}};
+                my ( $id, $kw );
+                if ( $pu && $pu->userpic_have_mapid ) {
+                    my $mapid;
+                    if ($post->{props} && $post->{props}->{picture_mapid}) {
+                        $mapid = $post->{props}->{picture_mapid};
+                    }
+                    $kw = $pu ? $pu->get_keyword_from_mapid( $mapid ) : undef;
+                    $id = $pu ? $pu->get_picid_from_mapid( $mapid ) : undef;
+                } else {
+                    if ($post->{props} && $post->{props}->{picture_keyword}) {
+                        $kw = $post->{props}->{picture_keyword};
+                    }
+                    $id = $pu ? $pu->get_picid_from_keyword( $kw ) : undef;
                 }
-                my $pu = $opts->{'userref'}->{$post->{'posterid'}};
-                my $id = $pu ? $pu->get_picid_from_keyword( $kw ) : undef;
-                $post->{'picid'} = $id;
+                $post->{picid} = $id;
+                $post->{pickw} = $kw;
                 push @load_pic, [ $pu, $id ];
             }
-            load_userpics( $opts->{'userpicref'}, \@load_pic );
+            load_userpics( $opts->{userpicref}, \@load_pic );
         }
     }
     return map { $posts->{$_} } @top_replies;
@@ -2855,7 +2865,12 @@ sub enter_comment {
     $talkprop{'unknown8bit'} = 1 if $comment->{unknown8bit};
     $talkprop{'subjecticon'} = $comment->{subjecticon};
 
-    $talkprop{'picture_keyword'} = $comment->{picture_keyword};
+    my $pu = $comment->{u};
+    if ( $pu && $pu->userpic_have_mapid ) {
+        $talkprop{picture_mapid} = $pu->get_mapid_from_keyword( $comment->{picture_keyword} );
+    } else {
+        $talkprop{picture_keyword} = $comment->{picture_keyword};
+    }
 
     $talkprop{'opt_preformatted'} = $comment->{preformat} ? 1 : 0;
     if ($journalu->opt_logcommentips eq "A" ||
@@ -3015,7 +3030,14 @@ sub enter_imported_comment {
 
     $talkprop{'unknown8bit'}      = 1 if $comment->{unknown8bit};
     $talkprop{'subjecticon'}      = $comment->{subjecticon};
-    $talkprop{'picture_keyword'}  = $comment->{picture_keyword};
+
+    my $pu = $comment->{u};
+    if ( $pu && $pu->userpic_have_mapid ) {
+        $talkprop{picture_mapid} = $pu->get_mapid_from_keyword( $comment->{picture_keyword}, create => 1 );
+    } else {
+        $talkprop{picture_keyword} = $comment->{picture_keyword};
+    }
+
     $talkprop{'opt_preformatted'} = $comment->{preformat} ? 1 : 0;
 
     # remove blank/0 values (defaults)
@@ -3140,7 +3162,7 @@ sub init {
         $form->{'userpost'} = $remote->{'user'};
         $form->{'usertype'} = "user";
     }
-    # XXXevan hack:  remove me when we fix preview.
+    # FIXME: XXXevan hack:  remove me when we fix preview.
     $init->{cookie_auth} = $cookie_auth;
 
     # test accounts may only comment on other test accounts.
@@ -3665,10 +3687,16 @@ sub edit_comment {
 
     my %props = (
         subjecticon => $comment->{subjecticon},
-        picture_keyword => $comment->{picture_keyword},
         opt_preformatted => $comment->{preformat} ? 1 : 0,
         edit_reason => $comment->{editreason},
     );
+
+    my $pu = $comment_obj->poster;
+    if ( $pu && $pu->userpic_have_mapid ) {
+        $props{picture_mapid} = $pu->get_mapid_from_keyword( $comment->{picture_keyword} );
+    } else {
+        $props{picture_keyword} = $comment->{picture_keyword};
+    }
 
     # set most of the props together
     $comment_obj->set_props(%props);
@@ -3689,7 +3717,7 @@ sub edit_comment {
     $comment->{talkid} = $comment_obj->jtalkid;
 
     # cluster tracking
-    LJ::mark_user_active($comment_obj->poster, 'comment');
+    LJ::mark_user_active($pu, 'comment');
 
     # fire events
     if ( LJ::is_enabled('esn') ) {
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/User.pm
--- a/cgi-bin/LJ/User.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/User.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -6082,6 +6082,7 @@ sub activate_userpics {
     return 1 if $u->is_expunged;
 
     my $userid = $u->userid;
+    my $have_mapid = $u->userpic_have_mapid;
 
     # active / inactive lists
     my @active = ();
@@ -6117,33 +6118,45 @@ sub activate_userpics {
 
         # query all pickws in logprop2 with jitemid > that value
         my %count_kw = ();
-        my $propid = LJ::get_prop("log", "picture_keyword")->{'id'};
+        my $propid;
+        if ( $have_mapid ) {
+            $propid = LJ::get_prop("log", "picture_mapid")->{id};
+        } else {
+            $propid = LJ::get_prop("log", "picture_keyword")->{id};
+        }
         my $sth = $dbcr->prepare("SELECT value, COUNT(*) FROM logprop2 " .
                                  "WHERE journalid=? AND jitemid > ? AND propid=?" .
                                  "GROUP BY value");
-        $sth->execute($userid, $jitemid, $propid);
+        $sth->execute($userid, $jitemid || 0, $propid);
         while (my ($value, $ct) = $sth->fetchrow_array) {
             # keyword => count
             $count_kw{$value} = $ct;
         }
 
-        my $keywords_in = join(",", map { $dbh->quote($_) } keys %count_kw);
+        my $values_in = join(",", map { $dbh->quote($_) } keys %count_kw);
 
         # map pickws to picids for freq hash below
         my %count_picid = ();
-        if ($keywords_in) {
-            my $sth = $dbcr->prepare( "SELECT k.keyword, m.picid FROM userkeywords k, userpicmap2 m ".
-                                      "WHERE k.keyword IN ($keywords_in) AND k.kwid=m.kwid AND k.userid=m.userid " .
-                                      "AND k.userid=?" );
-            $sth->execute($userid);
-            while (my ($keyword, $picid) = $sth->fetchrow_array) {
-                # keyword => picid
-                $count_picid{$picid} += $count_kw{$keyword};
+        if ( $values_in ) {
+            if ( $have_mapid ) {
+                foreach my $mapid ( keys %count_kw ) {
+                    my $picid = $u->get_picid_from_mapid($mapid);
+                    $count_picid{$picid} += $count_kw{$mapid} if $picid;
+                }
+            } else {
+                my $sth = $dbcr->prepare( "SELECT k.keyword, m.picid FROM userkeywords k, userpicmap2 m ".
+                                        "WHERE k.keyword IN ($values_in) AND k.kwid=m.kwid AND k.userid=m.userid " .
+                                        "AND k.userid=?" );
+                $sth->execute($userid);
+                while (my ($keyword, $picid) = $sth->fetchrow_array) {
+                    # keyword => picid
+                    $count_picid{$picid} += $count_kw{$keyword};
+                }
             }
         }
 
         # we're only going to ban the least used, excluding the user's default
-        my @ban = (grep { $_ != $u->{'defaultpicid'} }
+        my @ban = (grep { $_ != $u->{defaultpicid} }
                    sort { $count_picid{$a} <=> $count_picid{$b} } @active);
 
         @ban = splice(@ban, 0, $to_ban) if @ban > $to_ban;
@@ -6171,6 +6184,7 @@ sub activate_userpics {
 
     # delete userpic info object from memcache
     LJ::Userpic->delete_cache($u);
+    $u->clear_userpic_kw_map;
 
     return 1;
 }
@@ -6221,7 +6235,13 @@ sub expunge_userpic {
     # else now mark it
     $u->do( "UPDATE userpic2 SET state='X' WHERE userid = ? AND picid = ?", undef, $u->userid, $picid );
     return LJ::error( $dbcm ) if $dbcm->err;
+    
+    # Since we don't clean userpicmap2 when we migrate to dversion 9, clean it here on expunge no matter the dversion.
     $u->do( "DELETE FROM userpicmap2 WHERE userid = ? AND picid = ?", undef, $u->userid, $picid );
+    if ( $u->userpic_have_mapid ) {
+        $u->do( "DELETE FROM userpicmap3 WHERE userid = ? AND picid = ? AND kwid=NULL", undef, $u->userid, $picid );
+        $u->do( "UPDATE userpicmap3 SET picid = NULL WHERE userid = ? AND picid = ?", undef, $u->userid, $picid );
+    }
 
     # now clear the user's memcache picture info
     LJ::Userpic->delete_cache( $u );
@@ -6231,6 +6251,111 @@ sub expunge_userpic {
     return ( $u->userid, map {$_->[0]} grep {$_ && @$_ && $_->[0]} @rval );
 }
 
+=head3 C<< $u->get_keyword_from_mapid( $mapid, %opts ) >>
+
+Returns the keyword for the given mapid or undef if the mapid doesn't exist.
+
+Arguments:
+
+=over 4
+
+=item mapid
+
+=back
+
+Additional options:
+
+=over 4
+
+=item redir_callback
+
+Called if the mapping is redirected to another mapping with the following arguments
+
+( $u, $old_mapid, $new_mapid )
+
+=back
+
+=cut
+sub get_keyword_from_mapid {
+    my ( $u, $mapid, %opts ) = @_;
+    my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
+    return undef unless $info;
+    return undef unless $u->userpic_have_mapid;
+
+    $mapid = $u->resolve_mapid_redirects($mapid,%opts);
+    my $kw = $info->{mapkw}->{ $mapid };
+    return $kw;
+}
+
+=head3 C<< $u->get_mapid_from_keyword( $kw, %opts ) >>
+
+Returns the mapid for a given keyword.
+
+Arguments:
+
+=over 4
+
+=item kw
+
+The keyword.
+
+=back
+
+Additional options:
+
+=over 4
+
+=item create
+
+Should a mapid be created if one does not exist.
+
+Default: 0
+
+=back
+
+=cut
+sub get_mapid_from_keyword {
+    my ( $u, $kw, %opts ) = @_;
+    return 0 unless $u->userpic_have_mapid;
+
+    my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
+    return 0 unless $info;
+
+    my $mapid = $info->{kwmap}->{$kw};
+    return $mapid if $mapid;
+
+    # the silly "pic#2343" thing when they didn't assign a keyword, if we get here
+    # we need to create it.
+    if ( $kw =~ /^pic\#(\d+)$/ ) {
+        my $picid = $1;
+        return 0 unless $info->{pic}{$picid};           # don't create rows for invalid pics
+        return 0 unless $info->{pic}{$picid}{state} eq 'N'; # or inactive
+
+        return $u->_create_mapid( undef, $picid )
+    }
+
+    return 0 unless $opts{create};
+
+    return $u->_create_mapid( $u->get_keyword_id( $kw ), undef );
+}
+
+=head3 C<< $u->get_picid_from_keyword( $kw, $default ) >>
+
+Returns the picid for a given keyword.
+
+=over 4
+
+=item kw
+
+Keyword to look up.
+
+=item default (optional)
+
+Default: the users default userpic.
+
+=back
+
+=cut
 sub get_picid_from_keyword {
     my ( $u, $kw, $default ) = @_;
     $default ||= ref $u ? $u->{defaultpicid} : 0;
@@ -6239,15 +6364,61 @@ sub get_picid_from_keyword {
     my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
     return $default unless $info;
 
-    my $pr = $info->{'kw'}{$kw};
+    my $pr = $info->{kw}{$kw};
     # normal keyword
     return $pr->{picid} if $pr->{picid};
 
     # the silly "pic#2343" thing when they didn't assign a keyword
     if ( $kw =~ /^pic\#(\d+)$/ ) {
         my $picid = $1;
-        return $picid if $info->{'pic'}{$picid};
-    }
+        return $picid if $info->{pic}{$picid};
+    }
+
+    return $default;
+}
+
+=head3 C<< $u->get_picid_from_mapid( $mapid, %opts ) >>
+
+Returns the picid for a given mapid.
+
+Arguments:
+
+=over 4
+
+=item mapid
+
+=back
+
+Additional options:
+
+=over 4
+
+=item default
+
+Default: the users default userpic.
+
+=item redir_callback
+
+Called if the mapping is redirected to another mapping with the following arguments
+
+( $u, $old_mapid, $new_mapid )
+
+=back
+
+=cut
+sub get_picid_from_mapid {
+    my ( $u, $mapid, %opts ) = @_;
+    my $default = $opts{default} || ref $u ? $u->{defaultpicid} : 0;
+    return $default unless $mapid;
+    return $default unless $u->userpic_have_mapid;
+
+    my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
+    return $default unless $info;
+
+    $mapid = $u->resolve_mapid_redirects($mapid,%opts);
+    my $pr = $info->{mapid}{$mapid};
+
+    return $pr->{picid} if $pr->{picid};
 
     return $default;
 }
@@ -6340,10 +6511,13 @@ Maps a picid to a pic hashref.
 #       userid,
 #       "packed string", which expands to an array of {width=>..., ...}
 #       "packed string", which expands to { 'kw1' => id, 'kw2' => id, ...}
+#       series of 3 4-byte numbers, which expands to { mapid1 => id, mapid2 => id, ...}, as well as { mapid1 => mapid2 }
+#       "packed string", which expands to { 'kw1' => mapid, 'kw2' => mapid, ...}
 #       ]
 sub get_userpic_info {
     my ( $u, $opts ) = @_;
     return undef unless LJ::isu( $u ) && $u->clusterid;
+    my $mapped_icons = $u->userpic_have_mapid;
 
     # in the cache, cool, well unless it doesn't have comments or urls or descriptions
     # and we need them
@@ -6356,7 +6530,7 @@ sub get_userpic_info {
         return $cachedata if $good;
     }
 
-    my $VERSION_PICINFO = 3;
+    my $VERSION_PICINFO = 4;
 
     my $memkey = [$u->userid,"upicinf:$u->{'userid'}"];
     my ($info, $minfo);
@@ -6370,13 +6544,13 @@ sub get_userpic_info {
             # old data in the cache.  delete.
             LJ::MemCache::delete($memkey);
         } else {
-            my (undef, $picstr, $kwstr) = @$minfo;
+            my (undef, $picstr, $kwstr, $picmapstr, $kwmapstr) = @$minfo;
             $info = {
-                'pic' => {},
-                'kw' => {},
+                pic => {},
+                kw => {}
             };
             while (length $picstr >= 7) {
-                my $pic = { userid => $u->{'userid'} };
+                my $pic = { userid => $u->userid };
                 ($pic->{picid},
                  $pic->{width}, $pic->{height},
                  $pic->{state}) = unpack "NCCA", substr($picstr, 0, 7, '');
@@ -6389,12 +6563,37 @@ sub get_userpic_info {
                 my $kw = substr($kwstr, $pos, $nulpos-$pos);
                 my $id = unpack("N", substr($kwstr, $nulpos+1, 4));
                 $pos = $nulpos + 5; # skip NUL + 4 bytes.
-                $info->{kw}->{$kw} = $info->{pic}->{$id} if $info;
-            }
-        }
+                $info->{kw}->{$kw} = $info->{pic}->{$id};
+            }
+
+            if ( $mapped_icons ) {
+                if ( defined $picmapstr && defined $kwmapstr ) {
+                    $pos =  0;
+                    while ($pos < length($picmapstr)) {
+                        my ($mapid, $id, $redir) = unpack("NNN", substr($picmapstr, $pos, 12));
+                        $pos += 12; # 3 * 4 bytes.
+                        $info->{mapid}->{$mapid} = $info->{pic}{$id} if $id;
+                        $info->{map_redir}->{$mapid} = $redir if $redir;
+                    }
+                    
+                    $pos = $nulpos = 0;
+                    while (($nulpos = index($kwmapstr, "\0", $pos)) > 0) {
+                        my $kw = substr($kwmapstr, $pos, $nulpos-$pos);
+                        my $id = unpack("N", substr($kwmapstr, $nulpos+1, 4));
+                        $pos = $nulpos + 5; # skip NUL + 4 bytes.
+                        $info->{kwmap}->{$kw} = $id;
+                        $info->{mapkw}->{$id} = $kw || "pic#" . $info->{mapid}->{$id}->{picid};
+                    }
+                } else { # This user is on dversion 9, but the data isn't in memcache
+                         # so force a db load
+                    undef $info;
+                }
+            }
+        }
+
 
         # Load picture comments
-        if ( $opts->{load_comments} ) {
+        if ( $opts->{load_comments} && $info ) {
             my $commemkey = [$u->userid, "upiccom:" . $u->userid];
             my $comminfo = LJ::MemCache::get( $commemkey );
 
@@ -6464,10 +6663,10 @@ sub get_userpic_info {
     my %minfodesc;
     unless ($info) {
         $info = {
-            'pic' => {},
-            'kw' => {},
+            pic => {},
+            kw => {}
         };
-        my ($picstr, $kwstr);
+        my ($picstr, $kwstr, $predirstr, $kwmapstr);
         my $sth;
         my $dbcr = LJ::get_cluster_def_reader($u);
         my $db = @LJ::MEMCACHE_SERVERS ? LJ::get_db_writer() : LJ::get_db_reader();
@@ -6480,7 +6679,7 @@ sub get_userpic_info {
         while (my $pic = $sth->fetchrow_hashref) {
             next if $pic->{state} eq 'X'; # no expunged pics in list
             push @pics, $pic;
-            $info->{'pic'}->{$pic->{'picid'}} = $pic;
+            $info->{pic}->{$pic->{picid}} = $pic;
             $minfocom{int($pic->{picid})} = $pic->{comment}
                 if $opts->{load_comments} && $pic->{comment};
             $minfourl{int($pic->{picid})} = $pic->{url}
@@ -6493,20 +6692,47 @@ sub get_userpic_info {
         $picstr = join('', map { pack("NCCA", $_->{picid},
                                  $_->{width}, $_->{height}, $_->{state}) } @pics);
 
-        $sth = $dbcr->prepare( "SELECT k.keyword, m.picid FROM userpicmap2 m, userkeywords k ".
-                               "WHERE k.userid=? AND m.kwid=k.kwid AND m.userid=k.userid" );
+        if ( $mapped_icons ) {
+            $sth = $dbcr->prepare( "SELECT k.keyword, m.picid, m.mapid, m.redirect_mapid FROM userpicmap3 m LEFT JOIN userkeywords k ON ".
+                                "( m.userid=k.userid AND m.kwid=k.kwid ) WHERE m.userid=?" );
+        } else {
+            $sth = $dbcr->prepare( "SELECT k.keyword, m.picid FROM userpicmap2 m, userkeywords k ".
+                                "WHERE k.userid=? AND m.kwid=k.kwid AND m.userid=k.userid" );
+        }
         $sth->execute($u->{'userid'});
         my %minfokw;
-        while (my ($kw, $id) = $sth->fetchrow_array) {
-            next unless $info->{'pic'}->{$id};
+        my %picmap;
+        my %kwmap;
+        while (my ($kw, $id, $mapid, $redir) = $sth->fetchrow_array) {
+            my $skip_kw = 0;
+            if ( $mapped_icons ) {
+                $picmap{$mapid} = [ int($id), int($redir) ];
+                if ( $redir ) {
+                    $info->{map_redir}->{$mapid} = $redir;
+                } else {
+                    unless ( defined $kw ) {
+                        $skip_kw = 1;
+                        $kw = "pic#$id";
+                    }
+                    $info->{kwmap}->{$kw} = $kwmap{$kw} = $mapid;
+                    $info->{mapkw}->{$mapid} = $kw;
+                }
+            }
+            next if $skip_kw;
+            next unless $info->{pic}->{$id};
             next if $kw =~ /[\n\r\0]/;  # used to be a bug that allowed these to get in.
-            $info->{'kw'}->{$kw} = $info->{'pic'}->{$id};
+            $info->{kw}->{$kw} = $info->{pic}->{$id};
+            $info->{mapid}->{$mapid} = $info->{pic}->{$id} if $mapped_icons && $id;
             $minfokw{$kw} = int($id);
         }
         $kwstr = join('', map { pack("Z*N", $_, $minfokw{$_}) } keys %minfokw);
+        if ( $mapped_icons ) {
+            $predirstr = join('', map { pack("NNN", $_, @{ $picmap{$_} } ) } keys %picmap);
+            $kwmapstr = join('', map { pack("Z*N", $_, $kwmap{$_}) } keys %kwmap);
+        }
 
         $memkey = [$u->{'userid'},"upicinf:$u->{'userid'}"];
-        $minfo = [ $VERSION_PICINFO, $picstr, $kwstr ];
+        $minfo = [ $VERSION_PICINFO, $picstr, $kwstr, $predirstr, $kwmapstr ];
         LJ::MemCache::set($memkey, $minfo);
 
         if ( $opts->{load_comments} ) {
@@ -6558,7 +6784,7 @@ sub get_userpic_kw_map {
     foreach my $keyword ( keys %{$picinfo->{kw}} ) {
         my $picid = $picinfo->{kw}->{$keyword}->{picid};
         $keywords->{$picid} = [] unless $keywords->{$picid};
-        push @{$keywords->{$picid}}, $keyword if ( $keyword && $picid );
+        push @{$keywords->{$picid}}, $keyword if ( $keyword && $picid && $keyword !~ m/^pic\#(\d+)$/ );
     }
 
     return $u->{picid_kw_map} = $keywords;
@@ -6587,6 +6813,59 @@ sub mogfs_userpic_key {
     return "up:" . $self->userid . ":$picid";
 }
 
+=head3 C<< $u->resolve_mapid_redirects( $mapid, %opts ) >>
+
+Resolve any mapid redirect, guarding against any redirect loops.
+
+Returns: new map id, or 0 if the mapping cannot be resolved.
+
+Arguments:
+
+=over 4
+
+=item mapid
+
+=back
+
+Additional options:
+
+=over 4
+
+=item redir_callback
+
+Called if the mapping is redirected to another mapping with the following arguments
+
+( $u, $old_mapid, $new_mapid )
+
+=back
+
+=cut
+sub resolve_mapid_redirects {
+    my ( $u, $mapid, %opts ) = @_;
+
+    my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
+    return 0 unless $info;
+
+    my %seen = ( $mapid => 1 );
+    my $orig_id = $mapid;
+
+    while ( $info->{map_redir}->{ $mapid } ) {
+        $orig_id = $mapid;
+        $mapid = $info->{map_redir}->{ $mapid };
+
+        # To implement lazy updating or the like
+        $opts{redir_callback}->($u, $orig_id, $mapid) if $opts{redir_callback};
+
+        # This should never happen, but am checking it here mainly in case
+        # never *does* happen, so we don't hang the web process with an endless loop.
+        if ( $seen{$mapid}++ ) {
+            warn("userpicmap3 redirectloop for " . $u->id . " on mapid " . $mapid);
+            return 0;
+        }
+    }
+
+    return $mapid;
+}
 
 =head3 C<< $u->userpic >>
 
@@ -6599,6 +6878,15 @@ sub userpic {
     return LJ::Userpic->new($u, $u->{defaultpicid});
 }
 
+=head3 C<< $u->userpic_have_mapid >>
+
+Returns true if the userpicmap keyword mappings have a mapid column ( dversion 9 or higher )
+
+=cut
+# FIXME: This probably should be userpics_use_mapid
+sub userpic_have_mapid {
+    return $_[0]->dversion >= 9;
+}
 
 
 =head3 C<< $u->userpic_quota >>
@@ -6613,7 +6901,23 @@ sub userpic_quota {
     return $quota;
 }
 
-
+# Intentionally no POD here.
+# This is an internal helper method
+# takes a $kwid and $picid ( either can be undef )
+# and creates a mapid row for it
+sub _create_mapid {
+    my ( $u, $kwid, $picid ) = @_;
+    return 0 unless $u->userpic_have_mapid;
+
+    my $mapid = LJ::alloc_user_counter($u,'Y');
+    $u->do( "INSERT INTO userpicmap3 (userid, mapid, kwid, picid) VALUES (?,?,?,?)", undef, $u->id, $mapid, $kwid, $picid);
+    return 0 if $u->err;
+
+    LJ::Userpic->delete_cache($u);
+    $u->clear_userpic_kw_map;
+
+    return $mapid;
+}
 
 ########################################################################
 ###  99. Miscellaneous Legacy Items
@@ -7390,7 +7694,7 @@ sub unset_remote
 #       'Q' == Notification Inbox,
 #       'D' == 'moDule embed contents', 'I' == Import data block
 #       'Z' == import status item, 'X' == eXternal account
-#       'F' == filter id
+#       'F' == filter id, 'Y' = pic/keYword mapping id
 #
 sub alloc_user_counter
 {
@@ -7399,7 +7703,7 @@ sub alloc_user_counter
 
     ##################################################################
     # IF YOU UPDATE THIS MAKE SURE YOU ADD INITIALIZATION CODE BELOW #
-    return undef unless $dom =~ /^[LTMPSRKCOVEQGDIZXF]$/;            #
+    return undef unless $dom =~ /^[LTMPSRKCOVEQGDIZXFY]$/;           #
     ##################################################################
 
     my $dbh = LJ::get_db_writer();
@@ -7520,6 +7824,9 @@ sub alloc_user_counter
     } elsif ($dom eq "F") {
         $newmax = $u->selectrow_array("SELECT MAX(filterid) FROM watch_filters WHERE userid=?",
                                       undef, $uid);
+    } elsif ($dom eq "Y") {
+        $newmax = $u->selectrow_array("SELECT MAX(mapid) FROM userpicmap3 WHERE userid=?",
+                                         undef, $uid);
     } else {
         die "No user counter initializer defined for area '$dom'.\n";
     }
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Userpic.pm
--- a/cgi-bin/LJ/Userpic.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Userpic.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -60,6 +60,14 @@ sub reset_singletons {
 sub reset_singletons {
     %singletons = ();
 }
+
+=head1 NAME
+
+LJ::Userpic
+
+=head1 Class Methods
+
+=cut
 
 # LJ::Userpic constructor. Returns a LJ::Userpic object.
 # Return existing with userid and picid populated, or make new.
@@ -148,8 +156,12 @@ sub new_from_row {
     return $self;
 }
 
-sub new_from_keyword
-{
+=head2 C<< $class->new_from_keyword( $u, $kw ) >>
+
+Returns the LJ::Userpic for the given keyword
+
+=cut
+sub new_from_keyword {
     my ( $class, $u, $kw ) = @_;
     return undef unless LJ::isu( $u );
 
@@ -158,7 +170,24 @@ sub new_from_keyword
     return $picid ? $class->new( $u, $picid ) : undef;
 }
 
-# instance methods
+
+=head2 C<< $class->new_from_mapid( $u, $mapid ) >>
+
+Returns the LJ::Userpic for the given mapid
+
+=cut
+sub new_from_mapid {
+    my ( $class, $u, $mapid ) = @_;
+    return undef unless LJ::isu( $u );
+
+    my $picid = $u->get_picid_from_mapid( $mapid );
+
+    return $picid ? $class->new( $u, $picid ) : undef;
+}
+
+=head1 Instance Methods
+
+=cut
 
 sub valid {
     return defined $_[0]->state;
@@ -934,11 +963,14 @@ sub delete {
 
     # userpic keywords
     eval {
-        $u->do( "DELETE FROM userpicmap2 WHERE userid=? " .
-                "AND picid=?", undef, $u->userid, $picid ) or die;
-        $u->do( "DELETE FROM userpic2 WHERE picid=? AND userid=?",
-                undef, $picid, $u->userid ) or die;
-        };
+        if ( $u->userpic_have_mapid ) {
+            $u->do( "DELETE FROM userpicmap3 WHERE userid = ? AND picid = ? AND kwid=NULL", undef, $u->userid, $picid ) or die;
+            $u->do( "UPDATE userpicmap3 SET picid=NULL WHERE userid=? AND picid=?", undef, $u->userid, $picid ) or die;
+        } else {
+            $u->do( "DELETE FROM userpicmap2 WHERE userid=? AND picid=?", undef, $u->userid, $picid ) or die;
+        }
+        $u->do( "DELETE FROM userpic2 WHERE picid=? AND userid=?", undef, $picid, $u->userid ) or die;
+    };
     $fail->() if $@;
 
     $u->log_event('delete_userpic', { picid => $picid });
@@ -1005,15 +1037,31 @@ sub set_keywords {
     @keywords = grep { !/^pic\#\d+$/ } grep { s/^\s+//; s/\s+$//; $_; } @keywords;
 
     my $u = $self->owner;
+    my $have_mapid = $u->userpic_have_mapid;
+
     my $sth;
     my $dbh;
 
-    $sth = $u->prepare( "SELECT kwid FROM userpicmap2 WHERE userid=? AND picid=?" );
+    if ( $have_mapid ) {
+        $sth = $u->prepare( "SELECT kwid FROM userpicmap3 WHERE userid=? AND picid=?" );
+    } else {
+        $sth = $u->prepare( "SELECT kwid FROM userpicmap2 WHERE userid=? AND picid=?" );
+    }
     $sth->execute( $u->userid, $self->id );
 
     my %exist_kwids;
     while (my ($kwid) = $sth->fetchrow_array) {
         $exist_kwids{$kwid} = 1;
+    }
+
+    my %kwid_to_mapid;
+    if ( $have_mapid ) {
+        $sth = $u->prepare( "SELECT mapid, kwid FROM userpicmap3 WHERE userid=?" );
+        $sth->execute( $u->userid );
+
+        while (my ($mapid, $kwid) = $sth->fetchrow_array) {
+            $kwid_to_mapid{$kwid} = $mapid;
+        }
     }
 
     my (@bind, @data, @kw_errors);
@@ -1030,23 +1078,39 @@ sub set_keywords {
         }
 
         unless (delete $exist_kwids{$kwid}) {
-            push @bind, '(?, ?, ?)';
-            push @data, $u->{'userid'}, $kwid, $picid;
+            if ( $have_mapid ) {
+                $kwid_to_mapid{$kwid} ||= LJ::alloc_user_counter( $u, 'Y' );
+
+                push @bind, '(?, ?, ?, ?)';
+                push @data, $u->userid, $kwid_to_mapid{$kwid}, $kwid, $picid;
+            } else {
+                push @bind, '(?, ?, ?)';
+                push @data, $u->userid, $kwid, $picid;
+            }
         }
     }
 
     LJ::Userpic->delete_cache($u);
 
     foreach my $kwid (keys %exist_kwids) {
-        $u->do("DELETE FROM userpicmap2 WHERE userid=? AND picid=? AND kwid=?", undef, $u->{userid}, $self->id, $kwid);
+        if ( $have_mapid ) {
+            $u->do("UPDATE userpicmap3 SET picid=NULL WHERE userid=? AND picid=? AND kwid=?", undef, $u->id, $self->id, $kwid);
+        } else {
+            $u->do("DELETE FROM userpicmap2 WHERE userid=? AND picid=? AND kwid=?", undef, $u->id, $self->id, $kwid);
+        }
     }
 
     # save data if any
     if (scalar @data) {
         my $bind = join(',', @bind);
 
-        $u->do( "REPLACE INTO userpicmap2 (userid, kwid, picid) VALUES $bind",
-                undef, @data );
+        if ( $have_mapid ) {
+            $u->do( "REPLACE INTO userpicmap3 (userid, mapid, kwid, picid) VALUES $bind",
+                    undef, @data );
+        } else {            
+            $u->do( "REPLACE INTO userpicmap2 (userid, kwid, picid) VALUES $bind",
+                    undef, @data );
+        }
     }
 
     # clear the userpic-keyword map.
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/ljlib.pl
--- a/cgi-bin/ljlib.pl	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/ljlib.pl	Sat Oct 02 01:27:29 2010 +0800
@@ -126,7 +126,7 @@ sub END { LJ::end_request(); }
                     "embedcontent", "usermsg", "usermsgtext", "usermsgprop",
                     "notifyarchive", "notifybookmarks", "pollprop2", "embedcontent_preview",
                     "logprop_history", "import_status", "externalaccount",
-                    "content_filters", "content_filter_data",
+                    "content_filters", "content_filter_data", "userpicmap3",
                     );
 
 # keep track of what db locks we have out
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/ljprotocol.pl
--- a/cgi-bin/ljprotocol.pl	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/ljprotocol.pl	Sat Oct 02 01:27:29 2010 +0800
@@ -1132,6 +1132,7 @@ sub postevent
         $flags->{noauth} = 1;
         $flags->{usejournal_okay} = 1;
         $flags->{no_xpost} = 1;
+        $flags->{create_unknown_picture_mapid} = 1;
     }
 
     return undef unless LJ::Hooks::run_hook('post_noauth', $req) || authenticate($req, $err, $flags);
@@ -1221,6 +1222,12 @@ sub postevent
 
     return undef
         unless common_event_validation($req, $err, $flags);
+
+    # now we can move over to picture_mapid instead of picture_keyword if appropriate
+    if ( $req->{props} && $req->{props}->{picture_keyword} && $u->userpic_have_mapid ) {
+        $req->{props}->{picture_mapid} = $u->get_mapid_from_keyword( $req->{props}->{picture_keyword}, create => $flags->{create_unknown_picture_mapid} || 0 );
+        delete $req->{props}->{picture_keyword};
+    }
 
     # confirm we can add tags, at least
     return fail($err, 312)
@@ -1886,6 +1893,12 @@ sub editevent
     return undef
         unless common_event_validation($req, $err, $flags);
 
+    # now we can move over to picture_mapid instead of picture_keyword if appropriate
+    if ( $req->{props} && $req->{props}->{picture_keyword} && $u->userpic_have_mapid ) {
+        $req->{props}->{picture_mapid} = $u->get_mapid_from_keyword( $req->{props}->{picture_keyword}, create => $flags->{create_unknown_picture_mapid} || 0 );
+        delete $req->{props}->{picture_keyword};
+    }
+
     ## handle meta-data (properties)
     my %props_byname = ();
     foreach my $key (keys %{$req->{'props'}}) {
@@ -2409,10 +2422,15 @@ sub getevents
 
         # if they want subjects to be events, replace event
         # with subject when requested.
-        if ($req->{'prefersubject'} && length($t->[0])) {
+        if ($req->{prefersubject} && length($t->[0])) {
             $t->[1] = $t->[0];  # event = subject
             $t->[0] = undef;    # subject = undef
         }
+
+        # re-generate the picture_keyword prop for the returned data, as a mapid will mean nothing
+        my $pu = $uowner;
+        $pu = LJ::load_user( $evt->{poster} ) if $evt->{poster};
+        $evt->{props}->{picture_keyword} = $pu->get_keyword_from_mapid( $evt->{props}->{picture_mapid} ) if $pu->userpic_have_mapid;
 
         # now that we have the subject, the event and the props,
         # auto-translate them to UTF-8 if they're not in UTF-8.
@@ -3068,17 +3086,18 @@ sub list_pickws
     my %seen;  # mashifiedptr -> 1
 
     # FIXME: should be a utf-8 sort
-    foreach my $kw (sort keys %{$pi->{'kw'}}) {
-        my $pic = $pi->{'kw'}{$kw};
+    foreach my $kw ( sort keys %{$pi->{kw}} ) {
+        my $pic = $pi->{kw}{$kw};
         $seen{$pic} = 1;
-        next if $pic->{'state'} eq "I";
-        push @res, [ $kw, $pic->{'picid'} ];
+        next if $pic->{state} eq "I";
+        push @res, [ $kw, $pic->{picid} ];
     }
 
     # now add all the pictures that don't have a keyword
-    foreach my $picid (keys %{$pi->{'pic'}}) {
-        my $pic = $pi->{'pic'}{$picid};
+    foreach my $picid ( keys %{$pi->{pic}} ) {
+        my $pic = $pi->{pic}{$picid};
         next if $seen{$pic};
+        next if $pic->{state} eq "I";
         push @res, [ "pic#$picid", $picid ];
     }
 
@@ -3852,6 +3871,7 @@ sub editevent
     $res->{'itemid'} = $rs->{'itemid'};
     $res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
     $res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
+
     return 1;
 }
 
diff -r ebea5b13e8a4 -r f897918203ac htdocs/admin/entryprops.bml
--- a/htdocs/admin/entryprops.bml	Sat Oct 02 00:29:19 2010 +0800
+++ b/htdocs/admin/entryprops.bml	Sat Oct 02 01:27:29 2010 +0800
@@ -59,8 +59,10 @@ body<=
         }
     }
 
+    my $pu = $entry->poster;
+
     $ret .= "<strong>Subject</strong>: <a href=" . $entry->url . ">" . $subject . "</a><br />";
-    $ret .= "<strong>Poster</strong>: " . $entry->poster->ljuser_display . "<br />";
+    $ret .= "<strong>Poster</strong>: " . $pu->ljuser_display . "<br />";
     $ret .= "<strong>Journal</strong>: " . $entry->journal->ljuser_display . "<br />";
     $ret .= "<strong>Security</strong>: " . $security . " ";
     $ret .= "(journal wide minsecurity: " . ($entry->journal->prop("newpost_minsecurity") || "public") . ")<br />";
@@ -82,7 +84,7 @@ body<=
             
             # render xpost prop into human readable form
             if ( $prop eq "xpost" || $prop eq "xpostdetail" ) {
-                my %external_accounts_map = map { $_->acctid => $_->servername } DW::External::Account->get_external_accounts( $entry->poster );
+                my %external_accounts_map = map { $_->acctid => $_->servername } DW::External::Account->get_external_accounts( $pu );
 
                 # FIXME: temporary; trying to figure out when this is undef
                 my $xpost_prop = $props{$prop};
@@ -100,6 +102,20 @@ body<=
                 # FIXME: temporary
                 $props{$prop} .= "raw information about $prop - <input type='text' value='$xpost_prop' />" 
                     unless $xpost_hash;
+            } elsif ( $prop eq 'picture_mapid' && $pu->userpic_have_mapid ) {
+                my $result = "$props{$prop} -> ";
+                my $kw = $pu->get_keyword_from_mapid( $props{$prop},
+                            redir_callback => sub {
+                                $result .= "$_[2] -> ";
+                            });
+                $result .= $kw;
+                my $picid = $pu->get_picid_from_keyword($kw,-1);
+                if ( $picid == -1 ) {
+                    $result .= " ( not assigned to an icon )";
+                } else {
+                    $result .= " ( assigned to an icon )";
+                }
+                $props{$prop} = $result;
             }
 
             $extra = "<br /><small>$p->{des}</small>";
diff -r ebea5b13e8a4 -r f897918203ac htdocs/talkpost.bml
--- a/htdocs/talkpost.bml	Sat Oct 02 00:29:19 2010 +0800
+++ b/htdocs/talkpost.bml	Sat Oct 02 01:27:29 2010 +0800
@@ -53,8 +53,9 @@ body<=
         $FORM{subject} = $comment->subject_orig;
         $FORM{body} = $comment->body_orig;
         $FORM{subjecticon} = $comment->prop('subjecticon');
-        $FORM{prop_picture_keyword} = $comment->prop('picture_keyword');
         $FORM{prop_opt_preformatted} = $comment->prop('opt_preformatted');
+
+        $FORM{prop_picture_keyword} = $comment->userpic_kw;
     }
 
     if ($uri =~ m!/(\d+)\.html$!) {
diff -r ebea5b13e8a4 -r f897918203ac htdocs/talkread.bml
--- a/htdocs/talkread.bml	Sat Oct 02 00:29:19 2010 +0800
+++ b/htdocs/talkread.bml	Sat Oct 02 01:27:29 2010 +0800
@@ -239,7 +239,7 @@ body<=
     $ret .= "<p>";
     $ret .= "<table id='poster'><tr>";
 
-    my $userpic = $entry->userpic;
+    my ( $userpic, $kw ) = $entry->userpic;
     LJ::Hooks::run_hook('notify_event_displayed', $entry);
 
     # Build the userpic image tag for the entry's userpic
@@ -249,7 +249,7 @@ body<=
         my $apost = "</a></td>";
 
         # for each image, get the html imgtag
-        $ret .= $apre . $userpic->imgtag( keyword => $props->{picture_keyword}, user => $up ) . $apost;
+        $ret .= $apre . $userpic->imgtag( keyword => $kw, user => $up ) . $apost;
     }
 
     $ret .= "<td class='attrib' valign='bottom'>";
@@ -498,7 +498,7 @@ body<=
                    # get the picture keyword from the comment properties
                    # get the comment poster as well
                    my %kwopts = (
-                      keyword => $post->{props}->{picture_keyword},
+                      keyword => $post->{pickw},
                       user => $upost,
                    );
 
--------------------------------------------------------------------------------