Пример #1
0
 def test_srau_wears_a_seatbelt(self):
     def gen_test_username():
         for i in range(101):
             yield 'deadbeef'
     def reserve(cursor, username):
         raise IntegrityError
     with self.db.get_cursor() as cursor:
         with pytest.raises(FailedToReserveUsername):
             safely_reserve_a_username(cursor, gen_test_username, reserve)
Пример #2
0
 def test_srau_wears_a_seatbelt(self):
     def gen_test_username():
         for i in range(101):
             yield 'deadbeef'
     def reserve(cursor, username):
         raise IntegrityError
     with self.db.get_cursor() as cursor:
         with pytest.raises(FailedToReserveUsername):
             safely_reserve_a_username(cursor, gen_test_username, reserve)
Пример #3
0
 def test_srau_seatbelt_goes_to_100(self):
     def gen_test_username():
         for i in range(100):
             yield 'deadbeef'
     def reserve(cursor, username):
         raise IntegrityError
     with self.db.get_cursor() as cursor:
         with pytest.raises(RanOutOfUsernameAttempts):
             safely_reserve_a_username(cursor, gen_test_username, reserve)
Пример #4
0
 def test_srau_seatbelt_goes_to_100(self):
     def gen_test_username():
         for i in range(100):
             yield 'deadbeef'
     def reserve(cursor, username):
         raise IntegrityError
     with self.db.get_cursor() as cursor:
         with pytest.raises(RanOutOfUsernameAttempts):
             safely_reserve_a_username(cursor, gen_test_username, reserve)
Пример #5
0
 def test_srau_inserts_a_participant_by_default(self):
     def gen_test_username():
         yield 'deadbeef'
     with self.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor, gen_test_username)
     assert username == 'deadbeef'
     assert self.db.one('SELECT username FROM participants') == 'deadbeef'
Пример #6
0
 def test_srau_inserts_a_participant_by_default(self):
     def gen_test_username():
         yield 'deadbeef'
     with self.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor, gen_test_username)
     assert username == 'deadbeef'
     assert self.db.one('SELECT username FROM participants') == 'deadbeef'
Пример #7
0
 def test_srau_retries_work_with_db(self):
     self.make_participant('deadbeef')
     def gen_test_username():
         yield 'deadbeef'
         yield 'deafbeef'
     with self.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor, gen_test_username)
         assert username == 'deafbeef'
Пример #8
0
 def test_srau_retries_work_with_db(self):
     self.make_participant('deadbeef')
     def gen_test_username():
         yield 'deadbeef'
         yield 'deafbeef'
     with self.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor, gen_test_username)
         assert username == 'deafbeef'
Пример #9
0
    def upsert(cls, i):
        """Insert or update a user's info.
        """

        # Clean up avatar_url
        if i.avatar_url:
            scheme, netloc, path, query, fragment = urlsplit(i.avatar_url)
            fragment = ''
            if netloc.endswith('githubusercontent.com') or \
               netloc.endswith('gravatar.com'):
                query = 's=160'
            i.avatar_url = urlunsplit((scheme, netloc, path, query, fragment))

        # Serialize extra_info
        if isinstance(i.extra_info, ET.Element):
            i.extra_info = xmltodict.parse(ET.tostring(i.extra_info))
        i.extra_info = json.dumps(i.extra_info)

        cols, vals = zip(*i.__dict__.items())
        cols = ', '.join(cols)
        placeholders = ', '.join(['%s'] * len(vals))

        try:
            # Try to insert the account
            # We do this with a transaction so that if the insert fails, the
            # participant we reserved for them is rolled back as well.
            with cls.db.get_cursor() as cursor:
                username = safely_reserve_a_username(cursor)
                cursor.execute(
                    """
                    INSERT INTO elsewhere
                                (participant, {0})
                         VALUES (%s, {1})
                """.format(cols, placeholders), (username, ) + vals)
                # Propagate elsewhere.is_team to participants.number
                if i.is_team:
                    cursor.execute(
                        """
                        UPDATE participants
                           SET number = 'plural'::participant_number
                         WHERE username = %s
                    """, (username, ))
        except IntegrityError:
            # The account is already in the DB, update it instead
            username = cls.db.one(
                """
                UPDATE elsewhere
                   SET ({0}) = ({1})
                 WHERE platform=%s AND user_id=%s
             RETURNING participant
            """.format(cols, placeholders), vals + (i.platform, i.user_id))
            if not username:
                raise

        # Return account after propagating avatar_url to participant
        account = AccountElsewhere.from_user_id(i.platform, i.user_id)
        account.participant.update_avatar()
        return account
Пример #10
0
 def test_srau_retries_work_with_db(self):
     # XXX This is raising InternalError because the transaction is ended or something.
     self.make_participant('deadbeef')
     def gen_test_username():
         yield 'deadbeef'
         yield 'deafbeef'
     with self.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor, gen_test_username)
         assert username == 'deafbeef'
Пример #11
0
 def test_srau_retries_work_with_db(self):
     # XXX This is raising InternalError because the transaction is ended or something.
     self.make_participant('deadbeef')
     def gen_test_username():
         yield 'deadbeef'
         yield 'deafbeef'
     with self.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor, gen_test_username)
         assert username == 'deafbeef'
Пример #12
0
 def test_srau_safely_reserves_a_username(self):
     def gen_test_username():
         yield 'deadbeef'
     def reserve(cursor, username):
         return 'deadbeef'
     with self.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor, gen_test_username, reserve)
     assert username == 'deadbeef'
     assert self.db.one('SELECT username FROM participants') is None
Пример #13
0
 def test_srau_safely_reserves_a_username(self):
     def gen_test_username():
         yield 'deadbeef'
     def reserve(cursor, username):
         return 'deadbeef'
     with self.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor, gen_test_username, reserve)
     assert username == 'deadbeef'
     assert self.db.one('SELECT username FROM participants') is None
Пример #14
0
    def upsert(cls, i):
        """Insert or update a user's info.
        """

        # Clean up avatar_url
        if i.avatar_url:
            scheme, netloc, path, query, fragment = urlsplit(i.avatar_url)
            fragment = ''
            if netloc.endswith('githubusercontent.com') or \
               netloc.endswith('gravatar.com'):
                query = 's=160'
            i.avatar_url = urlunsplit((scheme, netloc, path, query, fragment))

        # Serialize extra_info
        if isinstance(i.extra_info, ET.Element):
            i.extra_info = xmltodict.parse(ET.tostring(i.extra_info))
        i.extra_info = json.dumps(i.extra_info)

        cols, vals = zip(*i.__dict__.items())
        cols = ', '.join(cols)
        placeholders = ', '.join(['%s']*len(vals))

        try:
            # Try to insert the account
            # We do this with a transaction so that if the insert fails, the
            # participant we reserved for them is rolled back as well.
            with cls.db.get_cursor() as cursor:
                username = safely_reserve_a_username(cursor)
                cursor.execute("""
                    INSERT INTO elsewhere
                                (participant, {0})
                         VALUES (%s, {1})
                """.format(cols, placeholders), (username,)+vals)
                # Propagate elsewhere.is_team to participants.number
                if i.is_team:
                    cursor.execute("""
                        UPDATE participants
                           SET number = 'plural'::participant_number
                         WHERE username = %s
                    """, (username,))
        except IntegrityError:
            # The account is already in the DB, update it instead
            username = cls.db.one("""
                UPDATE elsewhere
                   SET ({0}) = ({1})
                 WHERE platform=%s AND user_id=%s
             RETURNING participant
            """.format(cols, placeholders), vals+(i.platform, i.user_id))
            if not username:
                raise

        # Return account after propagating avatar_url to participant
        account = AccountElsewhere.from_user_id(i.platform, i.user_id)
        account.participant.update_avatar()
        return account
Пример #15
0
    def archive(self, cursor):
        """Given a cursor, use it to archive ourself.

        Archiving means changing to a random username so the username they were
        using is released. We also sign them out.

        """

        self.final_check(cursor)

        def reserve(cursor, username):
            check = cursor.one("""

                UPDATE participants
                   SET username=%s
                     , username_lower=%s
                     , claimed_time=NULL
                     , session_token=NULL
                     , session_expires=now()
                     , giving = 0
                     , taking = 0
                 WHERE username=%s
             RETURNING username

            """, ( username
                 , username.lower()
                 , self.username
                  ), default=NotSane)
            return check

        archived_as = safely_reserve_a_username(cursor, reserve=reserve)
        self.app.add_event(cursor, 'participant', dict( id=self.id
                                                      , action='archive'
                                                      , values=dict( new_username=archived_as
                                                                   , old_username=self.username
                                                                    )
                                                       ))
        return archived_as
Пример #16
0
    def archive(self, cursor):
        """Given a cursor, use it to archive ourself.

        Archiving means changing to a random username so the username they were
        using is released. We also sign them out.

        """

        self.final_check(cursor)

        def reserve(cursor, username):
            check = cursor.one("""

                UPDATE participants
                   SET username=%s
                     , username_lower=%s
                     , claimed_time=NULL
                     , session_token=NULL
                     , session_expires=now()
                     , giving = 0
                     , taking = 0
                 WHERE username=%s
             RETURNING username

            """, (username, username.lower(), self.username),
                               default=NotSane)
            return check

        archived_as = safely_reserve_a_username(cursor, reserve=reserve)
        self.app.add_event(
            cursor, 'participant',
            dict(id=self.id,
                 action='archive',
                 values=dict(new_username=archived_as,
                             old_username=self.username)))
        return archived_as
Пример #17
0
    def take_over(self, account, have_confirmation=False):
        """Given an AccountElsewhere or a tuple (platform_name, user_id),
        associate an elsewhere account.

        Returns None or raises NeedConfirmation.

        This method associates an account on another platform (GitHub, Twitter,
        etc.) with the given Gratipay participant. Every account elsewhere has an
        associated Gratipay participant account, even if its only a stub
        participant.

        In certain circumstances, we want to present the user with a
        confirmation before proceeding to transfer the account elsewhere to
        the new Gratipay account; NeedConfirmation is the signal to request
        confirmation. If it was the last account elsewhere connected to the old
        Gratipay account, then we absorb the old Gratipay account into the new one,
        effectively archiving the old account.

        Here's what absorbing means:

            - consolidated tips to and fro are set up for the new participant

                Amounts are summed, so if alice tips bob $1 and carl $1, and
                then bob absorbs carl, then alice tips bob $2(!) and carl $0.

                And if bob tips alice $1 and carl tips alice $1, and then bob
                absorbs carl, then bob tips alice $2(!) and carl tips alice $0.

                The ctime of each new consolidated tip is the older of the two
                tips that are being consolidated.

                If alice tips bob $1, and alice absorbs bob, then alice tips
                bob $0.

                If alice tips bob $1, and bob absorbs alice, then alice tips
                bob $0.

            - all tips to and from the other participant are set to zero
            - the absorbed username is released for reuse
            - the absorption is recorded in an absorptions table

        This is done in one transaction.
        """

        if isinstance(account, AccountElsewhere):
            platform, user_id = account.platform, account.user_id
        else:
            platform, user_id = account

        CREATE_TEMP_TABLE_FOR_UNIQUE_TIPS = """

        CREATE TEMP TABLE __temp_unique_tips ON COMMIT drop AS

            -- Get all the latest tips from everyone to everyone.

            SELECT ctime, tipper, tippee, amount, is_funded
              FROM current_tips
             WHERE amount > 0;

        """

        CONSOLIDATE_TIPS_RECEIVING = """

            -- Create a new set of tips, one for each current tip *to* either
            -- the dead or the live account. If a user was tipping both the
            -- dead and the live account, then we create one new combined tip
            -- to the live account (via the GROUP BY and sum()).

            INSERT INTO tips (ctime, tipper, tippee, amount, is_funded)

                 SELECT min(ctime), tipper, %(live)s AS tippee, sum(amount), bool_and(is_funded)

                   FROM __temp_unique_tips

                  WHERE (tippee = %(dead)s OR tippee = %(live)s)
                        -- Include tips *to* either the dead or live account.

                AND NOT (tipper = %(dead)s OR tipper = %(live)s)
                        -- Don't include tips *from* the dead or live account,
                        -- lest we convert cross-tipping to self-tipping.

               GROUP BY tipper

        """

        CONSOLIDATE_TIPS_GIVING = """

            -- Create a new set of tips, one for each current tip *from* either
            -- the dead or the live account. If both the dead and the live
            -- account were tipping a given user, then we create one new
            -- combined tip from the live account (via the GROUP BY and sum()).

            INSERT INTO tips (ctime, tipper, tippee, amount)

                 SELECT min(ctime), %(live)s AS tipper, tippee, sum(amount)

                   FROM __temp_unique_tips

                  WHERE (tipper = %(dead)s OR tipper = %(live)s)
                        -- Include tips *from* either the dead or live account.

                AND NOT (tippee = %(dead)s OR tippee = %(live)s)
                        -- Don't include tips *to* the dead or live account,
                        -- lest we convert cross-tipping to self-tipping.

               GROUP BY tippee

        """

        ZERO_OUT_OLD_TIPS_RECEIVING = """

            INSERT INTO tips (ctime, tipper, tippee, amount)

                SELECT ctime, tipper, tippee, 0 AS amount
                  FROM __temp_unique_tips
                 WHERE tippee=%s

        """

        ZERO_OUT_OLD_TIPS_GIVING = """

            INSERT INTO tips (ctime, tipper, tippee, amount)

                SELECT ctime, tipper, tippee, 0 AS amount
                  FROM __temp_unique_tips
                 WHERE tipper=%s

        """

        TRANSFER_BALANCE_1 = """

            UPDATE participants
               SET balance = (balance - %(balance)s)
             WHERE username=%(dead)s
         RETURNING balance;

        """

        TRANSFER_BALANCE_2 = """

            INSERT INTO transfers (tipper, tippee, amount, context)
            SELECT %(dead)s, %(live)s, %(balance)s, 'take-over'
             WHERE %(balance)s > 0;

            UPDATE participants
               SET balance = (balance + %(balance)s)
             WHERE username=%(live)s
         RETURNING balance;

        """

        MERGE_EMAIL_ADDRESSES = """

            WITH email_addresses_to_keep AS (
                     SELECT DISTINCT ON (address) id
                       FROM email_addresses
                      WHERE participant_id IN (%(dead)s, %(live)s)
                   ORDER BY address, verification_end, verification_start DESC
                 )
            DELETE FROM email_addresses
             WHERE participant_id IN (%(dead)s, %(live)s)
               AND id NOT IN (SELECT id FROM email_addresses_to_keep);

            UPDATE email_addresses
               SET participant_id = %(live)s
             WHERE participant_id = %(dead)s;

        """

        new_balance = None

        with self.db.get_cursor() as cursor:

            # Load the existing connection.
            # =============================
            # Every account elsewhere has at least a stub participant account
            # on Gratipay.

            elsewhere = cursor.one("""

                SELECT elsewhere.*::elsewhere_with_participant
                  FROM elsewhere
                  JOIN participants ON participant=participants.username
                 WHERE elsewhere.platform=%s AND elsewhere.user_id=%s

            """, (platform, user_id), default=NotSane)
            other = elsewhere.participant


            if self.username == other.username:
                # this is a no op - trying to take over itself
                return


            # Hard fail if the other participant has an identity.
            # ===================================================
            # Our identity system is very young. Maybe some day we'll do
            # something smarter here.

            if other.list_identity_metadata():
                raise WontTakeOverWithIdentities()


            # Make sure we have user confirmation if needed.
            # ==============================================
            # We need confirmation in whatever combination of the following
            # three cases:
            #
            #   - the other participant is not a stub; we are taking the
            #       account elsewhere away from another viable Gratipay
            #       participant
            #
            #   - the other participant has no other accounts elsewhere; taking
            #       away the account elsewhere will leave the other Gratipay
            #       participant without any means of logging in, and it will be
            #       archived and its tips absorbed by us
            #
            #   - we already have an account elsewhere connected from the given
            #       platform, and it will be handed off to a new stub
            #       participant

            # other_is_a_real_participant
            other_is_a_real_participant = other.is_claimed

            # this_is_others_last_login_account
            nelsewhere = len(other.get_elsewhere_logins(cursor))
            this_is_others_last_login_account = (nelsewhere <= 1)

            # we_already_have_that_kind_of_account
            nparticipants = cursor.one( "SELECT count(*) FROM elsewhere "
                                        "WHERE participant=%s AND platform=%s"
                                      , (self.username, platform)
                                       )
            assert nparticipants in (0, 1)  # sanity check
            we_already_have_that_kind_of_account = nparticipants == 1

            if elsewhere.is_team and we_already_have_that_kind_of_account:
                if len(self.get_accounts_elsewhere()) == 1:
                    raise TeamCantBeOnlyAuth

            need_confirmation = NeedConfirmation( other_is_a_real_participant
                                                , this_is_others_last_login_account
                                                , we_already_have_that_kind_of_account
                                                 )
            if need_confirmation and not have_confirmation:
                raise need_confirmation


            # We have user confirmation. Proceed.
            # ===================================
            # There is a race condition here. The last person to call this will
            # win. XXX: I'm not sure what will happen to the DB and UI for the
            # loser.


            # Move any old account out of the way.
            # ====================================

            if we_already_have_that_kind_of_account:
                new_stub_username = safely_reserve_a_username(cursor)
                cursor.run( "UPDATE elsewhere SET participant=%s "
                            "WHERE platform=%s AND participant=%s"
                          , (new_stub_username, platform, self.username)
                           )


            # Do the deal.
            # ============
            # If other_is_not_a_stub, then other will have the account
            # elsewhere taken away from them with this call.

            cursor.run( "UPDATE elsewhere SET participant=%s "
                        "WHERE platform=%s AND user_id=%s"
                      , (self.username, platform, user_id)
                       )


            # Fold the old participant into the new as appropriate.
            # =====================================================
            # We want to do this whether or not other is a stub participant.

            if this_is_others_last_login_account:

                other.clear_takes(cursor)

                # Take over tips.
                # ===============

                x, y = self.username, other.username
                cursor.run(CREATE_TEMP_TABLE_FOR_UNIQUE_TIPS)
                cursor.run(CONSOLIDATE_TIPS_RECEIVING, dict(live=x, dead=y))
                cursor.run(CONSOLIDATE_TIPS_GIVING, dict(live=x, dead=y))
                cursor.run(ZERO_OUT_OLD_TIPS_RECEIVING, (other.username,))
                cursor.run(ZERO_OUT_OLD_TIPS_GIVING, (other.username,))

                # Take over balance.
                # ==================

                other_balance = other.balance
                args = dict(live=x, dead=y, balance=other_balance)
                archive_balance = cursor.one(TRANSFER_BALANCE_1, args)
                other.set_attributes(balance=archive_balance)
                new_balance = cursor.one(TRANSFER_BALANCE_2, args)

                # Take over email addresses.
                # ==========================

                cursor.run(MERGE_EMAIL_ADDRESSES, dict(live=self.id, dead=other.id))

                # Take over payment routes.
                # =========================
                # Route ids logged in add_event call below, to keep the thread alive.

                route_ids = cursor.all( "UPDATE exchange_routes SET participant=%s "
                                        "WHERE participant=%s RETURNING id"
                                      , (self.id, other.id)
                                       )

                # Take over team ownership.
                # =========================

                cursor.run( "UPDATE teams SET owner=%s WHERE owner=%s"
                          , (self.username, other.username)
                           )

                # Disconnect any remaining elsewhere account.
                # ===========================================

                cursor.run("DELETE FROM elsewhere WHERE participant=%s", (y,))

                # Archive the old participant.
                # ============================
                # We always give them a new, random username. We sign out
                # the old participant.

                archive_username = other.archive(cursor)


                # Record the absorption.
                # ======================
                # This is for preservation of history.

                cursor.run( "INSERT INTO absorptions "
                            "(absorbed_was, absorbed_by, archived_as) "
                            "VALUES (%s, %s, %s)"
                          , ( other.username
                            , self.username
                            , archive_username
                             )
                           )

                self.app.add_event( cursor
                                  , 'participant'
                                  , dict( action='take-over'
                                        , id=self.id
                                        , values=dict( other_id=other.id
                                                     , exchange_routes=route_ids
                                                      )
                                         )
                                   )


        if new_balance is not None:
            self.set_attributes(balance=new_balance)

        self.update_avatar()

        # Note: the order ... doesn't actually matter here.
        self.update_taking()
        self.update_giving()
Пример #18
0
 def with_random_username(cls):
     """Return a new participant with a random username.
     """
     with cls.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor)
     return cls.from_username(username)
Пример #19
0
    def take_over(self, account, have_confirmation=False):
        """Given an AccountElsewhere or a tuple (platform_name, user_id),
        associate an elsewhere account.

        Returns None or raises NeedConfirmation.

        This method associates an account on another platform (GitHub, Twitter,
        etc.) with the given Gratipay participant. Every account elsewhere has an
        associated Gratipay participant account, even if its only a stub
        participant.

        In certain circumstances, we want to present the user with a
        confirmation before proceeding to transfer the account elsewhere to
        the new Gratipay account; NeedConfirmation is the signal to request
        confirmation. If it was the last account elsewhere connected to the old
        Gratipay account, then we absorb the old Gratipay account into the new one,
        effectively archiving the old account.

        Here's what absorbing means:

            - consolidated tips to and fro are set up for the new participant

                Amounts are summed, so if alice tips bob $1 and carl $1, and
                then bob absorbs carl, then alice tips bob $2(!) and carl $0.

                And if bob tips alice $1 and carl tips alice $1, and then bob
                absorbs carl, then bob tips alice $2(!) and carl tips alice $0.

                The ctime of each new consolidated tip is the older of the two
                tips that are being consolidated.

                If alice tips bob $1, and alice absorbs bob, then alice tips
                bob $0.

                If alice tips bob $1, and bob absorbs alice, then alice tips
                bob $0.

            - all tips to and from the other participant are set to zero
            - the absorbed username is released for reuse
            - the absorption is recorded in an absorptions table

        This is done in one transaction.
        """

        if isinstance(account, AccountElsewhere):
            platform, user_id = account.platform, account.user_id
        else:
            platform, user_id = account

        CREATE_TEMP_TABLE_FOR_UNIQUE_TIPS = """

        CREATE TEMP TABLE __temp_unique_tips ON COMMIT drop AS

            -- Get all the latest tips from everyone to everyone.

            SELECT ctime, tipper, tippee, amount, is_funded
              FROM current_tips
             WHERE amount > 0;

        """

        CONSOLIDATE_TIPS_RECEIVING = """

            -- Create a new set of tips, one for each current tip *to* either
            -- the dead or the live account. If a user was tipping both the
            -- dead and the live account, then we create one new combined tip
            -- to the live account (via the GROUP BY and sum()).

            INSERT INTO tips (ctime, tipper, tippee, amount, is_funded)

                 SELECT min(ctime), tipper, %(live)s AS tippee, sum(amount), bool_and(is_funded)

                   FROM __temp_unique_tips

                  WHERE (tippee = %(dead)s OR tippee = %(live)s)
                        -- Include tips *to* either the dead or live account.

                AND NOT (tipper = %(dead)s OR tipper = %(live)s)
                        -- Don't include tips *from* the dead or live account,
                        -- lest we convert cross-tipping to self-tipping.

               GROUP BY tipper

        """

        CONSOLIDATE_TIPS_GIVING = """

            -- Create a new set of tips, one for each current tip *from* either
            -- the dead or the live account. If both the dead and the live
            -- account were tipping a given user, then we create one new
            -- combined tip from the live account (via the GROUP BY and sum()).

            INSERT INTO tips (ctime, tipper, tippee, amount)

                 SELECT min(ctime), %(live)s AS tipper, tippee, sum(amount)

                   FROM __temp_unique_tips

                  WHERE (tipper = %(dead)s OR tipper = %(live)s)
                        -- Include tips *from* either the dead or live account.

                AND NOT (tippee = %(dead)s OR tippee = %(live)s)
                        -- Don't include tips *to* the dead or live account,
                        -- lest we convert cross-tipping to self-tipping.

               GROUP BY tippee

        """

        ZERO_OUT_OLD_TIPS_RECEIVING = """

            INSERT INTO tips (ctime, tipper, tippee, amount)

                SELECT ctime, tipper, tippee, 0 AS amount
                  FROM __temp_unique_tips
                 WHERE tippee=%s

        """

        ZERO_OUT_OLD_TIPS_GIVING = """

            INSERT INTO tips (ctime, tipper, tippee, amount)

                SELECT ctime, tipper, tippee, 0 AS amount
                  FROM __temp_unique_tips
                 WHERE tipper=%s

        """

        TRANSFER_BALANCE_1 = """

            UPDATE participants
               SET balance = (balance - %(balance)s)
             WHERE username=%(dead)s
         RETURNING balance;

        """

        TRANSFER_BALANCE_2 = """

            INSERT INTO transfers (tipper, tippee, amount, context)
            SELECT %(dead)s, %(live)s, %(balance)s, 'take-over'
             WHERE %(balance)s > 0;

            UPDATE participants
               SET balance = (balance + %(balance)s)
             WHERE username=%(live)s
         RETURNING balance;

        """

        MERGE_EMAIL_ADDRESSES = """

            WITH emails_to_keep AS (
                     SELECT DISTINCT ON (address) id
                       FROM emails
                      WHERE participant_id IN (%(dead)s, %(live)s)
                   ORDER BY address, verification_end, verification_start DESC
                 )
            DELETE FROM emails
             WHERE participant_id IN (%(dead)s, %(live)s)
               AND id NOT IN (SELECT id FROM emails_to_keep);

            UPDATE emails
               SET participant_id = %(live)s
             WHERE participant_id = %(dead)s;

        """

        new_balance = None

        with self.db.get_cursor() as cursor:

            # Load the existing connection.
            # =============================
            # Every account elsewhere has at least a stub participant account
            # on Gratipay.

            elsewhere = cursor.one("""

                SELECT elsewhere.*::elsewhere_with_participant
                  FROM elsewhere
                  JOIN participants ON participant=participants.username
                 WHERE elsewhere.platform=%s AND elsewhere.user_id=%s

            """, (platform, user_id),
                                   default=NotSane)
            other = elsewhere.participant

            if self.username == other.username:
                # this is a no op - trying to take over itself
                return

            # Hard fail if the other participant has an identity.
            # ===================================================
            # Our identity system is very young. Maybe some day we'll do
            # something smarter here.

            if other.list_identity_metadata():
                raise WontTakeOverWithIdentities()

            # Make sure we have user confirmation if needed.
            # ==============================================
            # We need confirmation in whatever combination of the following
            # three cases:
            #
            #   - the other participant is not a stub; we are taking the
            #       account elsewhere away from another viable Gratipay
            #       participant
            #
            #   - the other participant has no other accounts elsewhere; taking
            #       away the account elsewhere will leave the other Gratipay
            #       participant without any means of logging in, and it will be
            #       archived and its tips absorbed by us
            #
            #   - we already have an account elsewhere connected from the given
            #       platform, and it will be handed off to a new stub
            #       participant

            # other_is_a_real_participant
            other_is_a_real_participant = other.is_claimed

            # this_is_others_last_login_account
            nelsewhere = len(other.get_elsewhere_logins(cursor))
            this_is_others_last_login_account = (nelsewhere <= 1)

            # we_already_have_that_kind_of_account
            nparticipants = cursor.one(
                "SELECT count(*) FROM elsewhere "
                "WHERE participant=%s AND platform=%s",
                (self.username, platform))
            assert nparticipants in (0, 1)  # sanity check
            we_already_have_that_kind_of_account = nparticipants == 1

            if elsewhere.is_team and we_already_have_that_kind_of_account:
                if len(self.get_accounts_elsewhere()) == 1:
                    raise TeamCantBeOnlyAuth

            need_confirmation = NeedConfirmation(
                other_is_a_real_participant, this_is_others_last_login_account,
                we_already_have_that_kind_of_account)
            if need_confirmation and not have_confirmation:
                raise need_confirmation

            # We have user confirmation. Proceed.
            # ===================================
            # There is a race condition here. The last person to call this will
            # win. XXX: I'm not sure what will happen to the DB and UI for the
            # loser.

            # Move any old account out of the way.
            # ====================================

            if we_already_have_that_kind_of_account:
                new_stub_username = safely_reserve_a_username(cursor)
                cursor.run(
                    "UPDATE elsewhere SET participant=%s "
                    "WHERE platform=%s AND participant=%s",
                    (new_stub_username, platform, self.username))

            # Do the deal.
            # ============
            # If other_is_not_a_stub, then other will have the account
            # elsewhere taken away from them with this call.

            cursor.run(
                "UPDATE elsewhere SET participant=%s "
                "WHERE platform=%s AND user_id=%s",
                (self.username, platform, user_id))

            # Fold the old participant into the new as appropriate.
            # =====================================================
            # We want to do this whether or not other is a stub participant.

            if this_is_others_last_login_account:

                other.clear_takes(cursor)

                # Take over tips.
                # ===============

                x, y = self.username, other.username
                cursor.run(CREATE_TEMP_TABLE_FOR_UNIQUE_TIPS)
                cursor.run(CONSOLIDATE_TIPS_RECEIVING, dict(live=x, dead=y))
                cursor.run(CONSOLIDATE_TIPS_GIVING, dict(live=x, dead=y))
                cursor.run(ZERO_OUT_OLD_TIPS_RECEIVING, (other.username, ))
                cursor.run(ZERO_OUT_OLD_TIPS_GIVING, (other.username, ))

                # Take over balance.
                # ==================

                other_balance = other.balance
                args = dict(live=x, dead=y, balance=other_balance)
                archive_balance = cursor.one(TRANSFER_BALANCE_1, args)
                other.set_attributes(balance=archive_balance)
                new_balance = cursor.one(TRANSFER_BALANCE_2, args)

                # Take over email addresses.
                # ==========================

                cursor.run(MERGE_EMAIL_ADDRESSES,
                           dict(live=self.id, dead=other.id))

                # Take over payment routes.
                # =========================
                # Route ids logged in add_event call below, to keep the thread alive.

                route_ids = cursor.all(
                    "UPDATE exchange_routes SET participant=%s "
                    "WHERE participant=%s RETURNING id", (self.id, other.id))

                # Take over team ownership.
                # =========================

                cursor.run("UPDATE teams SET owner=%s WHERE owner=%s",
                           (self.username, other.username))

                # Disconnect any remaining elsewhere account.
                # ===========================================

                cursor.run("DELETE FROM elsewhere WHERE participant=%s", (y, ))

                # Archive the old participant.
                # ============================
                # We always give them a new, random username. We sign out
                # the old participant.

                archive_username = other.archive(cursor)

                # Record the absorption.
                # ======================
                # This is for preservation of history.

                cursor.run(
                    "INSERT INTO absorptions "
                    "(absorbed_was, absorbed_by, archived_as) "
                    "VALUES (%s, %s, %s)",
                    (other.username, self.username, archive_username))

                self.app.add_event(
                    cursor, 'participant',
                    dict(action='take-over',
                         id=self.id,
                         values=dict(other_id=other.id,
                                     exchange_routes=route_ids)))

        if new_balance is not None:
            self.set_attributes(balance=new_balance)

        self.update_avatar()

        # Note: the order ... doesn't actually matter here.
        self.update_taking()
        self.update_giving()
Пример #20
0
 def with_random_username(cls):
     """Return a new participant with a random username.
     """
     with cls.db.get_cursor() as cursor:
         username = safely_reserve_a_username(cursor)
     return cls.from_username(username)