// // $Id$ package com.threerings.user; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import com.google.common.collect.ImmutableList; import com.samskivert.jdbc.jora.FieldMask; import com.samskivert.servlet.user.User; import com.samskivert.util.ArrayIntSet; import static com.threerings.user.Log.log; /** * Extends the basic samskivert user record with special Three Rings business. */ public class OOOUser extends User { /** The flag set when the user's e-mail address has been validated. */ public static final int VALIDATED_FLAG = (1 << 0); /** A flag set when a user has opted-in to receive partner site spam. */ public static final int AFFILIATE_SPAM_FLAG = (1 << 1); /** A flag set when a user pays us money to buy coins. */ public static final int HAS_BOUGHT_COINS_FLAG = (1 << 2); /** A flag set when a user redeems a ubisoft cd key. */ public static final int UBISOFT_KEY_REDEEMED_FLAG = (1 << 3); /** A flag set when a user pays us money for bulk time. */ public static final int HAS_BOUGHT_TIME_FLAG = (1 << 4); /** Indicates that a user is an active player of Y!PP. */ public static final int IS_ACTIVE_YOHOHO_PLAYER = (1 << 5); /** Indicates that a user is an active player of B!H. */ public static final int IS_ACTIVE_BANG_PLAYER = (1 << 6); /** Indicates that a user is an active player of GG. */ public static final int IS_ACTIVE_GARDENS_PLAYER = (1 << 7); /** Indicates that the user has an active subscription on the family friendly servers */ public static final int FAMILY_SUBSCRIBER = (1 << 8); /** Indicates that the user has been involved in a conversion to a Steam account (either as the * source or the destination of the conversion). */ public static final int CONVERTED_TO_STEAM = (1 << 9); /** An access token indicating that this user is an admin. */ public static final byte ADMIN = 1; /** An access token indicating that this user is a maintainer. */ public static final byte MAINTAINER = 2; /** An access token indicating that this user is an insider. */ public static final byte INSIDER = 3; /** An access token indicating that this user is a tester. */ public static final byte TESTER = 4; /** An access token indicating that this user is banned from Puzzle Pirates. */ public static final byte PP_BANNED = 5; /** An access token indicating that this user is customer support personnel. */ public static final byte SUPPORT = 6; /** An access token allowing them to spend more than the default max. */ public static final byte BIG_SPENDER = 7; /** An access token indicating that the user has bounced a check or reversed a payment for * Puzzle Pirates. */ public static final byte PP_DEADBEAT = 8; /** An access token indicating that this user is banned from Bang! Howdy. */ public static final byte BANG_BANNED = 9; /** An access token indicating that the user has bounced a check or reversed a payment for * Bang! Howdy. */ public static final byte BANG_DEADBEAT = 10; /** An access token indicating that this user is banned from MetaSOY. */ public static final byte MSOY_BANNED = 11; /** An access token indicating that the user has bounced a check or reversed a payment for * MetaSOY. */ public static final byte MSOY_DEADBEAT = 12; /** An access token indicating that this user is banned from an app. */ public static final byte APPS_BANNED = 13; /** An access token indicating that the user has bounced a check or reversed a payment for an * app. */ public static final byte APPS_DEADBEAT = 14; /** An access token indicating that the user is banned from Project X. */ public static final byte PROJECTX_BANNED = 15; /** An access token indicating that the user has bounced a check or reversed a payment for * Project X. */ public static final byte PROJECTX_DEADBEAT = 16; /** An access token indicating that the user is banned from Who. */ public static final byte WHO_BANNED = 17; /** An access token indicating that the user has bounced a check or reversed a payment for * Who. */ public static final byte WHO_DEADBEAT = 18; /** An access token indicating that this user is junior customer support personnel. */ public static final byte JR_SUPPORT = 19; /** Billing status flags for a particular service. */ public static final byte TRIAL_STATE = 0; public static final byte SUBSCRIBER_STATE = 1; public static final byte BILLING_FAILURE_STATE = 2; public static final byte EX_SUBSCRIBER_STATE = 3; public static final byte BANNED_STATE = 4; /** A regular expression that defines the valid characters for affiliate site identifier * strings. */ public static final String SITE_STRING_REGEX = "[-._A-Za-z0-9]+"; /** The default site id to use in the absence of others; currently puzzlepirates.com. * Eventually we'll have to have something on a per-domain basis. */ public static final int DEFAULT_SITE_ID = 2; /** The puzzlepirates.com site identifier. */ public static final int PUZZLEPIRATES_SITE_ID = 2; /** affiliate site id used for "alt" accounts created through pp.com/register/. */ public static final int PUZZLEPIRATES_ALT_SITE_ID = 164; /** The gamegardens.com site identifier. */ public static final int GAMEGARDENS_SITE_ID = 6; /** The site id we group personal referrers under when doing reporting. This should never be * inserted into the database. */ public static final int REFERRAL_SITE_ID = 7; /** The banghowdy.com site identifier. */ public static final int BANGHOWDY_SITE_ID = 8; /** The whirled.com site identifier. */ public static final int METASOY_SITE_ID = 9; /** The puzzlepiratesfamily.com site identifier */ public static final int YPPFAMILY_SITE_ID = 10; /** The apps.threerings.net site identifier. */ public static final int APPS_SITE_ID = 11; /** The Everything app site identifier. */ public static final int EVERYTHING_SITE_ID = 12; /** The BiteMe app site identifier. */ public static final int BITEME_SITE_ID = 13; /** The Down Town app site identifier. */ public static final int DOWNTOWN_SITE_ID = 14; /** The Face Pirate app site identifier. */ public static final int FACEPIRATE_SITE_ID = 15; /** The ProjectX app site identifier - not yet used for billing, only register. */ public static final int PROJECTX_SITE_ID = 204; /** The Who site identifier - jumping to 1000 to avoid collisions with YPP and SK affiliates */ public static final int WHO_SITE_ID = 1000; /** The Ubisoft site id, which we need for various hackery. */ public static final int UBISOFT_SITE_ID = 40; /** A mapping from domain to site id for the OOO sites. */ public static List SITES = ImmutableList.of( new OOOSite(PUZZLEPIRATES_SITE_ID, "puzzlepirates", "puzzlepirates.com"), new OOOSite(GAMEGARDENS_SITE_ID, "gardens", "gamegardens.com"), new OOOSite(BANGHOWDY_SITE_ID, "bang", "banghowdy.com"), new OOOSite(METASOY_SITE_ID, "metasoy", "whirled.com"), new OOOSite(YPPFAMILY_SITE_ID, "family", "puzzlepiratesfamily.com"), new OOOSite(APPS_SITE_ID, "apps", "apps.threerings.net"), new OOOSite(EVERYTHING_SITE_ID, "everything", "notused"), new OOOSite(BITEME_SITE_ID, "biteme", "notused"), new OOOSite(DOWNTOWN_SITE_ID, "downtown", "notused"), new OOOSite(PROJECTX_SITE_ID, "projectx", "spiralknights.com"), new OOOSite(WHO_SITE_ID, "who", "doctorwhowit.com")); /** The subscriber column name for Puzzle Pirates subscribers. Used by various repository * methods. */ public static final String PUZZLEPIRATES_COLUMN = "yohoho"; /** Used to make sure someone doesn't do something stupid. */ public static final String[] IDENTS_NOT_LOADED = {}; /** The flags detailing the user's various bits of status. (VALIDATED_FLAG, etc) */ public int flags; /** The tokens detailing the user's site access permissions. (ADMIN, TESTER, etc) */ public byte[] tokens; /** The user's account status for Yohoho! Puzzle Pirates. (TRIAL_STATE, SUBSCRIBER_STATE, * etc) */ public byte yohoho; /** The spots that have been given to the user by various crews. */ public String spots; /** The amount of time remaining on the users shun, in minutes. */ public int shunLeft; /** The id of any opaque tag provided by the affiliate to tag this user for their purposes. */ public int affiliateTagId; /** The list of machine identifiers associated with this user. */ public transient String[] machIdents = IDENTS_NOT_LOADED; /** * Returns the banned token for the site or 0 if an invalid site. */ public static byte getBannedToken (int site) { switch (site) { case YPPFAMILY_SITE_ID: case PUZZLEPIRATES_SITE_ID: return PP_BANNED; case BANGHOWDY_SITE_ID: return BANG_BANNED; case METASOY_SITE_ID: return MSOY_BANNED; case EVERYTHING_SITE_ID: return APPS_BANNED; case BITEME_SITE_ID: return APPS_BANNED; case DOWNTOWN_SITE_ID: return APPS_BANNED; case FACEPIRATE_SITE_ID: return APPS_BANNED; case PROJECTX_SITE_ID: return PROJECTX_BANNED; case WHO_SITE_ID: return WHO_BANNED; default: return (byte)0; // no other sites currently support banning } } /** * Returns the deadbeat token for the site or 0 if an unsupported site. */ public static byte getDeadbeatToken (int site) { switch (site) { case YPPFAMILY_SITE_ID: case PUZZLEPIRATES_SITE_ID: return PP_DEADBEAT; case BANGHOWDY_SITE_ID: return BANG_DEADBEAT; case METASOY_SITE_ID: return MSOY_DEADBEAT; case EVERYTHING_SITE_ID: return APPS_DEADBEAT; case BITEME_SITE_ID: return APPS_DEADBEAT; case DOWNTOWN_SITE_ID: return APPS_DEADBEAT; case FACEPIRATE_SITE_ID: return APPS_DEADBEAT; case PROJECTX_SITE_ID: return PROJECTX_DEADBEAT; case WHO_SITE_ID: return WHO_DEADBEAT; default: log.warning("Requested deadbeat token for unsupported site", "site", site); return (byte)0; } } /** * Returns whether the user's e-mail address has been validated. */ public boolean isValidated () { return isFlagSet(VALIDATED_FLAG); } /** * Returns true if the user has even purchased coins from us. */ public boolean hasBoughtCoins () { return isFlagSet(HAS_BOUGHT_COINS_FLAG); } /** * Returns true if the user has even purchased time from us. */ public boolean hasBoughtTime () { return isFlagSet(HAS_BOUGHT_TIME_FLAG); } /** * @return true if the specified flag has been set. */ public boolean isFlagSet (int flag) { return ((flags & flag) != 0); } /** * Updates the user's e-mail validation status. */ public void setValidated (boolean validated) { setFlag(VALIDATED_FLAG, validated); } /** * Checks whether the user is flagged as a family ocean subscriber. */ public boolean isFamilySubscriber () { return isFlagSet(OOOUser.FAMILY_SUBSCRIBER); } /** * Set or clear the specified flag. */ public void setFlag (int flag, boolean set) { if (set) { this.flags |= flag; } else { this.flags &= ~flag; } setModified("flags"); } /** * Adds the supplied access token to this user's token ring. */ public void addToken (byte token) { // check to see if they already have it if (!holdsToken(token)) { if (tokens == null) { tokens = new byte[] { token }; } else { int tcount = tokens.length; byte[] ntokens = new byte[tcount+1]; System.arraycopy(tokens, 0, ntokens, 0, tcount); ntokens[tcount] = token; tokens = ntokens; } setModified("tokens"); } } /** * Set all the spots that this user has recieved. */ public void setSpots (ArrayIntSet blackspots) { if (blackspots == null) { throw new IllegalArgumentException("Blackspots parameter can not be null"); } String newspots = ""; Iterator itr = blackspots.iterator(); for (int ii = 0; itr.hasNext(); ii++) { Integer crewid = itr.next(); if (ii == 0) { newspots += crewid; } else { newspots += ":" + crewid; } } spots = newspots; setModified("spots"); } /** * Converts the String representation of the users black spots to an ArrayIntSet. Each spot is, * in fact, the crewid of the crew that gave it to them. */ public ArrayIntSet getSpots () { ArrayIntSet blackSpots = new ArrayIntSet(); if (spots == null) { return blackSpots; } StringTokenizer tok = new StringTokenizer(spots, ":"); while (tok.hasMoreTokens()) { String crewid = tok.nextToken(); try { int spot = Integer.parseInt(crewid); blackSpots.add(spot); } catch (NumberFormatException nfe) { log.warning("Failed parsing spots.", "user", username, "crewid", crewid, "excpetion", nfe); } } return blackSpots; } /** * Removes the supplied access token from this user's token ring. */ public void removeToken (byte token) { // make sure they actually have it if (holdsToken(token)) { // the tokens array is likely to always be very small, so we // don't go to the trouble of trying to do this with arraycopy int tcount = tokens.length; byte[] ntokens = new byte[tcount-1]; for (int ii = 0, npos = 0; ii < tcount; ii++) { if (tokens[ii] == token) { continue; } ntokens[npos++] = tokens[ii]; } tokens = ntokens; setModified("tokens"); } } /** * Returns true if this user holds the specified token. */ public boolean holdsToken (byte token) { if (tokens == null) { return false; } int tcount = tokens.length; for (int ii = 0; ii < tcount; ii++) { if (tokens[ii] == token) { return true; } } return false; } /** * Set the billing status of the user for a particular site. * * @return true if the status changed, false if not. */ public boolean setBillingStatus (int site, byte status) { switch (site) { case METASOY_SITE_ID: // we maintain a separate OOOUSER installation for msoy and thus rather than adding // another column to maintain our subscriber status for msoy, we just reuse yohoho's case PUZZLEPIRATES_SITE_ID: case YPPFAMILY_SITE_ID: if (yohoho != status) { yohoho = status; setModified("yohoho"); return true; } break; default: throw new IllegalArgumentException( "Tried to set billing status for unknown site [site=" + site + "]."); } return false; } /** * Return the billing status for the passed in site. */ public byte getBillingStatus (int site) { switch (site) { case METASOY_SITE_ID: // see setBillingStatus case PUZZLEPIRATES_SITE_ID: return yohoho; case YPPFAMILY_SITE_ID: return isFamilySubscriber() ? SUBSCRIBER_STATE : yohoho; default: return TRIAL_STATE; } } public boolean isMaintainer () { return holdsToken(MAINTAINER); } @Override public boolean isAdmin () { return holdsToken(ADMIN) || isMaintainer(); } /** * Returns true if this user is an "insider" (and as such, should be allowed in for free) */ public boolean isInsider () { return (holdsToken(INSIDER) || isAdmin()); } /** * Returns true if this user holds the support token */ public boolean isSupport () { return holdsToken(SUPPORT); } /** * Returns true if this user holds the support token (or higher) */ public boolean isSupportPlus () { return isSupport() || isAdmin(); } /** * Returns true if this user is subscriber to Puzzle Pirates. */ public boolean isSubscriber () { return isSubscriber(PUZZLEPIRATES_SITE_ID); } public boolean isSubscriber (int site) { return ((getBillingStatus(site) == SUBSCRIBER_STATE) || isInsider()); } /** * Returns true if we have allowed the user to be a big spender. */ public boolean isBigSpender () { return holdsToken(BIG_SPENDER); } /** * Returns true if this user is banned. */ public boolean isBanned (int site) { byte token = getBannedToken(site); return (token == 0 ? false : holdsToken(token)); } /** * Configures this user's banned status for the specified site. */ public boolean setBanned (int site, boolean banned) { byte token = getBannedToken(site); if (token == 0) { log.warning("Requested to update banned for invalid site", "site", site); return false; } if (banned) { addToken(token); } else { removeToken(token); } return true; } /** * Returns true if this user has bounced a check or reversed a payment. */ public boolean isDeadbeat (int site) { byte token = getDeadbeatToken(site); return (token == 0 ? false : holdsToken(token)); } /** * Configures this user's deadbeat status for the specified site. */ public void setDeadbeat (int site, boolean deadbeat) { byte token = getDeadbeatToken(site); if (token != 0) { if (deadbeat) { addToken(token); } else { removeToken(token); } } } /** * Returns our Yohoho! billing status. */ public byte getYohohoStatus () { return getBillingStatus(PUZZLEPIRATES_SITE_ID); } /** * Marks this user as a non paying user. */ public void makeTrialYohoho () { setBillingStatus(PUZZLEPIRATES_SITE_ID, TRIAL_STATE); } /** * Returns true if this user holds any of the specified tokens. */ public boolean holdsAnyToken (byte[] tokset) { if (tokens == null) { return false; } int tcount = tokens.length, scount = tokset.length; for (int ii = 0; ii < tcount; ii++) { for (int tt = 0; tt < scount; tt++) { if (tokens[ii] == tokset[tt]) { return true; } } } return false; } // A protected method can be called by another class in the same package, but if you extend the // class containing the protected method and try to call that method from a class in the same // package as the derived class, it doesn't work. However, if we simply override the method and // do nothing but call super, it's considered "legal." Yay! @Override protected void setDirtyMask (FieldMask mask) { super.setDirtyMask(mask); } }