Extracted ooo-user bits into separate library.

This commit is contained in:
Michael Bayne
2011-10-05 09:37:10 -07:00
commit 1d78bbe27b
84 changed files with 12903 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" standalone="yes"?>
<project name="ooo-user" default="compile" basedir=".">
<property name="src.dir" location="src/main/java"/>
<property name="deploy.dir" location="target"/>
<property name="classes.dir" location="${deploy.dir}/classes"/>
<property name="jar.file" location="${deploy.dir}/${ant.project.name}.jar"/>
<!-- bring in our standard build support -->
<property name="ooo-build.vers" value="2.7"/>
<ant antfile="etc/bootstrap.xml"/>
<import file="${user.home}/.m2/ooo-build/${ooo-build.vers}/ooo-build.xml"/>
<target name="prepare" depends="-init-ooo">
<mkdir dir="${classes.dir}"/>
<mavendep pom="pom.xml"/>
</target>
<target name="clean" description="Cleans out compilation results.">
<delete dir="${deploy.dir}"/>
</target>
<target name="compile" depends="prepare" description="Builds the Java code.">
<ooojavac srcdir="${src.dir}" destdir="${classes.dir}" version="1.5"
classpathref="pom.xml.path"/>
</target>
<target name="package" depends="compile" description="Builds and jars the code.">
<jar destfile="${jar.file}" basedir="${classes.dir}"/>
</target>
<target name="maven-deploy" depends="package"
description="Deploys our build artifacts to a Maven repository.">
<mavendeploy file="${jar.file}" pom="pom.xml" srcdir="${src.dir}"/>
</target>
</project>
+36
View File
@@ -0,0 +1,36 @@
<project name="bootstrap" default="-extract-ooo-build">
<!--
Pulls in ooo-build from our maven repository to make our base build system available.
From http://ooo-build.googlecode.com/hg/etc/bootstrap.xml
To use, copy this file into your project's directory and add
<property name="ooo-build.vers" value="2.1"/>
<ant antfile="bootstrap.xml"/>
<import file="${user.home}/.m2/ooo-build/${ooo-build.vers}/ooo-build.xml"/>
to the top level of your build.xml. Then you can depend on -init-ooo in your lowest level
target to expose ooo-build's maven macros, javac macros, and antcontrib.
-->
<property name="maven.dir" value="${user.home}/.m2"/>
<property name="ooo-build.path"
value="com/threerings/ooo-build/${ooo-build.vers}"/>
<property name="ooo-build.jar"
value="ooo-build-${ooo-build.vers}.jar"/>
<property name="ooo-build.local.dir" value="${maven.dir}/repository/${ooo-build.path}"/>
<property name="ooo-build.local.file" value="${ooo-build.local.dir}/${ooo-build.jar}"/>
<condition property="ooo-build.exists"><available file="${ooo-build.local.file}"/></condition>
<target name="-download-ooo-build" unless="ooo-build.exists">
<mkdir dir="${ooo-build.local.dir}"/>
<get src="http://ooo-maven.googlecode.com/hg/repository/${ooo-build.path}/${ooo-build.jar}"
dest="${ooo-build.local.file}" usetimestamp="true"/>
</target>
<property name="ooo-build.dir" value="${maven.dir}/ooo-build/${ooo-build.vers}"/>
<condition property="extracted.exists"><available file="${ooo-build.dir}/ooo-build.xml"/></condition>
<target name="-extract-ooo-build" depends="-download-ooo-build" unless="extracted.exists">
<mkdir dir="${ooo-build.dir}"/>
<unjar src="${ooo-build.local.file}" dest="${ooo-build.dir}"/>
</target>
</project>
+60
View File
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.threerings</groupId>
<artifactId>ooo-parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>ooo-user</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>ooo-user</name>
<dependencies>
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>depot</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>samskivert</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>10.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<links>
<link>http://samskivert.googlecode.com/svn/apidocs/</link>
<link>http://docs.guava-libraries.googlecode.com/git/javadoc/</link>
</links>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,283 @@
//
// $Id$
package com.threerings.servlet;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.servlet.Site;
import com.samskivert.servlet.SiteIdentifier;
import com.samskivert.util.ArrayUtil;
import com.threerings.servlet.persist.DomainRecord;
import com.threerings.servlet.persist.SiteIdentifierRepository;
import com.threerings.servlet.persist.SiteRecord;
/**
* Accomplishes the process of site identification based on a mapping from domains (e.g.
* samskivert.com) to site identifiers that is maintained in a depot database table.
*
* <p> There are two tables, one that maps domains to site identifiers and another that maps site
* identifiers to site strings. These are both loaded at construct time and refreshed periodically
* in the course of normal operation.
*
* <p> Note that any of the calls to identify, lookup or enumerate site information can result in
* the sites table being refreshed from the database which will take relatively much longer than
* the simple hashtable lookup that the operations normally require. However, this happens only
* once every 15 minutes and the circumstances in which the site identifier are normally used can
* generally accomodate the extra 100 milliseconds or so that it is likely to take to reload the
* (tiny) sites and domains tables from the database.
*/
public class DepotSiteIdentifier
implements SiteIdentifier
{
/**
* Constructs a debot site identifier.
*/
public DepotSiteIdentifier (PersistenceContext pctx)
{
this(pctx, DEFAULT_SITE_ID);
}
/**
* Creates an identifier that will load data from the supplied connection provider and which
* will use the supplied default site id instead of {@link #DEFAULT_SITE_ID}.
*/
public DepotSiteIdentifier (PersistenceContext pctx, int defaultSiteId)
{
this(pctx, defaultSiteId, DEFAULT_SITE_STRING);
}
/**
* Creates an identifier that will load data from the supplied connection provider and which
* will use the supplied default site id instead of {@link #DEFAULT_SITE_ID} and the supplied
* default site string instead of {@link #DEFAULT_SITE_STRING}.
*/
public DepotSiteIdentifier (
PersistenceContext pctx, int defaultSiteId, String defaultSiteString)
{
_repo = new SiteIdentifierRepository(pctx);
refreshSiteData();
_defaultSiteId = defaultSiteId;
_defaultSiteString = defaultSiteString;
}
// documentation inherited
public int identifySite (HttpServletRequest req)
{
checkReloadSites();
String serverName = req.getServerName();
// scan for the mapping that matches the specified domain
int msize = _mappings.size();
for (int i = 0; i < msize; i++) {
SiteMapping mapping = _mappings.get(i);
if (serverName.endsWith(mapping.domain)) {
return mapping.siteId;
}
}
// if we matched nothing, return the default id
return _defaultSiteId;
}
// documentation inherited
public String getSiteString (int siteId)
{
checkReloadSites();
Site site = _sitesById.get(siteId);
if (site == null) {
site = _sitesById.get(_defaultSiteId);
}
return (site == null) ? _defaultSiteString : site.siteString;
}
// documentation inherited
public int getSiteId (String siteString)
{
checkReloadSites();
Site site = _sitesByString.get(siteString);
return (site == null) ? _defaultSiteId : site.siteId;
}
// documentation inherited from interface
public Iterator<Site> enumerateSites ()
{
checkReloadSites();
return _sitesById.values().iterator();
}
/**
* Insert a new site into the site table and into this mapping.
*/
public Site insertNewSite (String siteString)
{
return insertNewSite(siteString, 0);
}
/**
* Insert a new site into the site table and into this mapping.
*/
public Site insertNewSite (String siteString, int siteId)
{
if (_sitesByString.containsKey(siteString) ||
(siteId > 0 && _sitesById.containsKey(siteId))) {
return null;
}
// add it to the db
Site site = _repo.insertNewSite(siteString, siteId);
// add it to our two mapping tables, taking care to avoid causing enumerateSites() to choke
Map<String, Site> newStrings = Maps.newHashMap(_sitesByString);
Map<Integer, Site> newIds = Maps.newHashMap(_sitesById);
newIds.put(site.siteId, site);
newStrings.put(site.siteString, site);
_sitesByString = newStrings;
_sitesById = newIds;
return site;
}
/**
* Insert a new domain into the domain table and into this mapping.
*/
public void insertNewDomain (String domain, int siteId)
{
// scan for the mapping that matches the specified domain
int msize = _mappings.size();
for (int i = 0; i < msize; i++) {
SiteMapping mapping = _mappings.get(i);
if (mapping.domain.equals(domain)) {
return;
}
}
// add it to the db
_repo.insertNewDomain(domain, siteId);
// add it to our two mapping table, taking care to avoid causing enumerateSites() to choke
List<SiteMapping> mappings = Lists.newArrayList();
mappings.addAll(_mappings);
mappings.add(new SiteMapping(siteId, domain));
Collections.sort(mappings, SiteMapping.BY_SPECIFICITY);
_mappings = mappings;
}
/**
* Checks to see if we should reload our sites information from the sites table.
*/
protected void checkReloadSites ()
{
long now = System.currentTimeMillis();
boolean reload = false;
synchronized (this) {
reload = (now - _lastReload > RELOAD_INTERVAL);
if (reload) {
_lastReload = now;
}
}
if (reload) {
refreshSiteData();
}
}
/**
* Refreshes the cached site information.
*/
public void refreshSiteData ()
{
List<SiteRecord> siteRecords = _repo.loadSites();
Map<Integer,Site> sites = Maps.newHashMap();
Map<String,Site> strings = Maps.newHashMap();
for (SiteRecord record : siteRecords) {
Site site = record.toSite();
sites.put(record.siteId, site);
strings.put(record.siteString, site);
}
_sitesById = sites;
_sitesByString = strings;
List<DomainRecord> domainRecords = _repo.loadDomains();
List<SiteMapping> mappings = Lists.newArrayList();
for (DomainRecord record : domainRecords) {
mappings.add(new SiteMapping(record.siteId, record.domain));
}
Collections.sort(mappings, SiteMapping.BY_SPECIFICITY);
_mappings = mappings;
}
/**
* Used to track domain to site identifier mappings.
*/
protected static class SiteMapping
{
/**
* Sorts site mappings from most specific (www.yahoo.com) to least specific (yahoo.com).
*/
public static final Comparator<SiteMapping> BY_SPECIFICITY = new Comparator<SiteMapping>() {
public int compare (SiteMapping one, SiteMapping two) {
return one._rdomain.compareTo(two._rdomain);
}
};
/** The domain to match. */
public String domain;
/** The site identifier for the associated domain. */
public int siteId;
public SiteMapping (int siteId, String domain) {
this.siteId = siteId;
this.domain = domain;
byte[] bytes = domain.getBytes();
ArrayUtil.reverse(bytes);
_rdomain = new String(bytes);
}
@Override // from Object
public String toString () {
return "[" + domain + " => " + siteId + "]";
}
protected String _rdomain;
}
/** The repository through which we load up site identifier information. */
protected SiteIdentifierRepository _repo;
/** The site id to return if we cannot identify the site from our table data. */
protected int _defaultSiteId;
/** The site string to return if we cannot identify the site from our table data. */
protected String _defaultSiteString;
/** The list of domain to site identifier mappings ordered from most specific domain to least
* specific. */
protected volatile List<SiteMapping> _mappings = Lists.newArrayList();
/** The mapping from integer site identifiers to string site identifiers. */
protected volatile Map<Integer, Site> _sitesById = Maps.newHashMap();
/** The mapping from string site identifiers to integer site identifiers. */
protected volatile Map<String, Site> _sitesByString = Maps.newHashMap();
/** Used to periodically reload our site data. */
protected long _lastReload;
/** Reload our site data every 15 minutes. */
protected static final long RELOAD_INTERVAL = 15 * 60 * 1000L;
}
@@ -0,0 +1,48 @@
//
// $Id$
package com.threerings.servlet.persist;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.servlet.SiteIdentifier;
/**
* Represents a domain mapping known to a {@link SiteIdentifier}.
*/
@Entity(name="domains")
public class DomainRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<DomainRecord> _R = DomainRecord.class;
public static final ColumnExp<String> DOMAIN = colexp(_R, "domain");
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The domain. */
@Id @Column(length=128)
public String domain;
/** The site identifier. */
public int siteId;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link DomainRecord}
* with the supplied key values.
*/
public static Key<DomainRecord> getKey (String domain)
{
return newKey(_R, domain);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(DOMAIN); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,84 @@
//
// $Id$
package com.threerings.servlet.persist;
import java.util.List;
import java.util.Set;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.servlet.Site;
/**
* Depot implements of a site identifier repository.
*/
@Singleton
public class SiteIdentifierRepository extends DepotRepository
{
/**
* Creates the repository.
*/
@Inject public SiteIdentifierRepository (PersistenceContext ctx)
{
super(ctx);
}
/**
* Loads the sites.
*/
public List<SiteRecord> loadSites ()
{
return findAll(SiteRecord.class);
}
/**
* Loads the domains.
*/
public List<DomainRecord> loadDomains ()
{
return findAll(DomainRecord.class);
}
/**
* Adds a new site.
*/
public Site insertNewSite (String siteString)
{
return insertNewSite(siteString, 0);
}
/**
* Adds a new site.
*/
public Site insertNewSite (String siteString, int siteId)
{
SiteRecord record = new SiteRecord();
record.siteString = siteString;
record.siteId = siteId;
insert(record);
return record.toSite();
}
/**
* Adds a new domain.
*/
public void insertNewDomain (String domain, int siteId)
{
DomainRecord record = new DomainRecord();
record.domain = domain;
record.siteId = siteId;
insert(record);
}
@Override // documentation inherited
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
{
classes.add(DomainRecord.class);
classes.add(SiteRecord.class);
}
}
@@ -0,0 +1,64 @@
//
// $Id$
package com.threerings.servlet.persist;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.GeneratedValue;
import com.samskivert.depot.annotation.GenerationType;
import com.samskivert.depot.annotation.Id;
import com.samskivert.servlet.Site;
import com.samskivert.servlet.SiteIdentifier;
/**
* Represents a site mapping known to a {@link SiteIdentifier}.
*/
@Entity(name="sites")
public class SiteRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<SiteRecord> _R = SiteRecord.class;
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
public static final ColumnExp<String> SITE_STRING = colexp(_R, "siteString");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The site's unique identifier. */
@Id @GeneratedValue(strategy=GenerationType.AUTO)
public int siteId;
/** The site's human readable identifier (i.e., "monkeybutter"). */
@Column(length=24)
public String siteString;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link SiteRecord}
* with the supplied key values.
*/
public static Key<SiteRecord> getKey (int siteId)
{
return newKey(_R, siteId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(SITE_ID); }
// AUTO-GENERATED: METHODS END
/**
* Creates a Site from this record.
*/
public Site toSite ()
{
Site site = new Site();
site.siteId = siteId;
site.siteString = siteString;
return site;
}
}
@@ -0,0 +1,118 @@
//
// $Id$
package com.threerings.user;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.PreparedStatement;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.SimpleRepository;
import com.samskivert.util.IntTuple;
/**
* Manages database access for figuring out if we're doing an AB test of the website, and if so,
* keeping track of how many have gone each way.
*/
public class ABTestRepository extends SimpleRepository
{
/**
* The database identifier used when establishing a database
* connection. This value being <code>abtestdb</code>.
*/
public static final String ABTEST_DB_IDENT = "abtestdb";
/**
* Creates the repository and prepares it for operation.
*
* @param provider the database connection provider.
*/
public ABTestRepository (ConnectionProvider provider)
throws PersistenceException
{
super(provider, ABTEST_DB_IDENT);
}
/**
* Looks up whether or not we're doing an AB test, and what the remaining desired number
* of people for each bucket is.
*
* Returns null if there's no test running.
*/
public IntTuple getABTestCounts ()
throws PersistenceException
{
return execute(new Operation<IntTuple>() {
public IntTuple invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Statement stmt = conn.createStatement();
try {
ResultSet rs =
stmt.executeQuery("select REMAINING_A, REMAINING_B from AB_TEST");
while (rs.next()) {
int a = rs.getInt(1);
int b = rs.getInt(2);
if (a != 0 && b != 0) {
return new IntTuple(a,b);
}
}
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
public void decrementABTest (final boolean groupA)
throws PersistenceException
{
final IntTuple counts = getABTestCounts();
if (counts == null) {
// Not running a test? Ignore it
return;
}
if (counts.left == -1) {
// Can set to -1/-1 to show we want it to go on indefinitely
return;
}
executeUpdate(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
PreparedStatement stmt = conn.prepareStatement(
"update AB_TEST set REMAINING_" + (groupA ? "A" : "B") +
"=" + (Math.max(0, (groupA ? counts.left : counts.right) - 1)));
try {
JDBCUtil.checkedUpdate(stmt, 1);
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
@Override
protected void migrateSchema (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
String[] abTestTable = {
"REMAINING_A INTEGER",
"REMAINING_B INTEGER",
};
JDBCUtil.createTableIfMissing(conn, "AB_TEST", abTestTable, "");
}
}
@@ -0,0 +1,78 @@
//
// $Id$
package com.threerings.user;
import java.sql.Timestamp;
import com.samskivert.util.StringUtil;
/**
* Representation of a AccountAction record from the database.
*/
public class AccountAction
{
/** An action constant indicating the first time a user becomes a
* subscriber, and have also never before bought coins or time. */
public static final int INITIAL_SUBSCRIPTION = 0;
/** An action constant indicating that some server has modified the
* coins count for a particular user. */
public static final int COINS_UPDATED = 1;
/** An action constant indicating that the user, whom at some point in
* the past used to be a subscriber, has become a subscriber again. */
public static final int REPEAT_SUBSCRIPTION = 2;
/** An action constant indicating that the user purchased coins for the
* first time ever, and have never before subscribed or bought time. */
public static final int INITIAL_COIN_PURCHASE = 3;
/** An action constant indicating that the specified account has
* expired and any player resources should be expired. The 'data'
* field is currently filled in with the yohoho accountId for the
* expired account. */
public static final int ACCOUNT_EXPIRED = 4;
/** An action constant indicating that the specified account has been
* deleted in the external account system. The 'data' field is
* optionally filled in with the "disabled" username which the server
* can use to simply disable the account rather than deleting it. */
public static final int ACCOUNT_DELETED = 5;
/** An action unconnected with any single account, but rather a system-wide
* signal to all servers that they should do whatever "daily" actions
* they would like to do at the present time. */
public static final int DO_DAILY_ACTIONS = 6;
/** An action constant indicating that the user purchased time for the
* first time ever, and has never before subscribed or bought coins. */
public static final int INITIAL_TIME_PURCHASE = 7;
/** Indicates that a reward has been activated for the account. */
public static final int REWARD_ACTIVATED = 8;
/** Placeholder for application-defined action constants. */
public static final int FIRST_APPLICATION_ACTION = 100;
/** Unique action identifier. */
public int actionId;
/** The unique identifier for the account. */
public String accountName;
/** The action that took place. */
public int action;
/** Data that is interpreted depending on the action type. */
public String data;
/** When the action took place. */
public Timestamp entered;
@Override
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
@@ -0,0 +1,401 @@
//
// $Id$
package com.threerings.user;
import static com.threerings.user.Log.log;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.JORARepository;
import com.samskivert.jdbc.jora.Table;
import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.StringUtil;
/**
* Provides access to the account actions table.
*
* @deprecated Use com.threerings.user.depot.AccountActionRepository instead.
*/
@Deprecated
public class AccountActionRepository extends JORARepository
{
/**
* The database identifier used when establishing a database connection.
* This value being <code>actiondb</code>.
*/
public static final String ACTION_DB_IDENT = "actiondb";
/**
* Creates the repository and prepares it for operation.
*
* @param provider the database connection provider.
*/
public AccountActionRepository (ConnectionProvider provider)
throws PersistenceException
{
super(provider, ACTION_DB_IDENT);
// figure out whether or not we should be disabled
execute(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
_active = JDBCUtil.tableExists(conn, "ACCOUNT_ACTIONS");
if (!_active) {
log.info("No actions table. Disabling.");
}
return null;
}
});
}
/**
* Return the list of actions that have not yet been processed by the specified server.
* Returns all actions if no server name is specified.
*
* <em>Note:</em> this method has two side effects: the first time it is called, it will
* register this server as a action participant (assuming server is non-null) and
* subsequently, it will call {@link #pruneActions} if it has not done so within the last
* hour.
*/
public List<AccountAction> getActions (String server)
throws PersistenceException
{
return getActions(server, Integer.MAX_VALUE);
}
/**
* Return the list of actions that have not yet been processed by the specified server.
* Returns all actions if no server name is specified.
*
* @param maxActions the maximum number of actions to return.
*
* <em>Note:</em> this method has two side effects: the first time it is called, it will
* register this server as a action participant (assuming server is non-null) and
* subsequently, it will call {@link #pruneActions} if it has not done so within the last
* hour.
*/
public List<AccountAction> getActions (String server, int maxActions)
throws PersistenceException
{
if (!_active) {
return new ArrayList<AccountAction>();
}
// look ma, subselects
String where = "where ACTION_ID NOT IN " +
"(select ACTION_ID from PROCESSED_ACTIONS " +
"where SERVER = '" + server + "') limit " + maxActions;
List<AccountAction> list = loadAll(_atable, where);
// unjigger the account names
for (AccountAction action : list) {
action.accountName = JDBCUtil.unjigger(action.accountName);
}
// no fooling around if we are acting in general
if (StringUtil.isBlank(server)) {
return list;
}
// if this is the first time this method is called, register ourselves
// with the action system
long now = System.currentTimeMillis();
if (_lastActionPrune == 0L) {
_lastActionPrune = now;
try {
registerActionServer(server);
} catch (PersistenceException pe) {
log.warning("Failure registering server", "server", server, pe);
}
} else if (now - _lastActionPrune > ACTION_PRUNE_INTERVAL) {
_lastActionPrune = now;
try {
pruneActions();
} catch (PersistenceException pe) {
log.warning("Failure auto-pruning actions", "server", server, pe);
}
}
return list;
}
/**
* Adds a new action to the repository.
*/
public void addAction (String accountName, int action)
throws PersistenceException
{
addAction(accountName, null, action, "");
}
/**
* Adds a new action to the repository.
*/
public void addAction (String accountName, String data, int action)
throws PersistenceException
{
addAction(accountName, data, action, "");
}
/**
* Adds a new action to the repository.
*/
public void addAction (String accountName, int action, String server)
throws PersistenceException
{
addAction(accountName, null, action, server);
}
/**
* Adds a new action to the repository, with the specified server being marked as already
* having processed the action.
*/
public void addAction (String accountName, String data, int action, final String server)
throws PersistenceException
{
if (!_active) {
log.info("Dropping account action",
"name", accountName, "data", data, "action", action, "server", server);
return;
}
// create and insert the account action
final AccountAction aact = new AccountAction();
aact.accountName = JDBCUtil.jigger(accountName);
aact.data = data;
aact.action = action;
aact.entered = new Timestamp(System.currentTimeMillis());
aact.actionId = insert(_atable, aact);
// note that it was processed by this server if appropriate
if (!StringUtil.isBlank(server)) {
noteProcessed(aact.actionId, server);
}
}
/**
* Updates the single specified action to reflect that the specified server has processed it.
*/
public void updateAction (AccountAction action, String server)
throws PersistenceException
{
updateActions(Collections.singletonList(action), server);
}
/**
* Batch update a list of actions.
*/
public void updateActions (List<AccountAction> actions, String server)
throws PersistenceException
{
for (int ii = 0, ll = actions.size(); ii < ll; ii++) {
noteProcessed(actions.get(ii).actionId, server);
}
}
/**
* Deletes an action from the repository.
*/
public void deleteAction (final AccountAction action)
throws PersistenceException
{
// delete the action
delete(_atable, action);
// and any records indicating it has been processed
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Statement stmt = conn.createStatement();
try {
stmt.executeUpdate("delete from PROCESSED_ACTIONS " +
"where ACTION_ID = " + action.actionId);
} finally {
stmt.close();
}
return null;
}
});
}
/**
* Notes that the specified server has processed the sepecified action.
*/
public void noteProcessed (final int actionId, final String server)
throws PersistenceException
{
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Statement stmt = conn.createStatement();
try {
String query = "insert into PROCESSED_ACTIONS " +
"values(" + actionId + ", '" + server + "')";
stmt.executeUpdate(query);
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
/**
* Prunes actions that have been processed by all registered servers to keep the actions table
* small. This method need not be called by hand as it is called automatically by
* {@link #getActions(String,int)}, but no more frequently than once an hour.
*/
public void pruneActions ()
throws PersistenceException
{
final Set<String> servers = loadActionServers();
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Map<Integer,Set<String>> procmap = Maps.newHashMap();
ArrayIntSet actids = new ArrayIntSet();
Statement stmt = conn.createStatement();
try {
// determine which servers have processed which actions
ResultSet rs = stmt.executeQuery(
"select ACTION_ID, SERVER from PROCESSED_ACTIONS");
while (rs.next()) {
Integer actionId = (Integer)rs.getObject(1);
Set<String> procset = procmap.get(actionId);
if (procset == null) {
procmap.put(actionId, procset = Sets.newHashSet());
}
procset.add(rs.getString(2));
}
// determine which of those have been fully processed
for (Map.Entry<Integer,Set<String>> entry : procmap.entrySet()) {
Integer actionId = entry.getKey();
Set<String> procset = entry.getValue();
if (procset.containsAll(servers)) {
actids.add(actionId.intValue());
}
}
// now wipe out the actions (and processed entries) for
// all actions that have been fully processed
if (actids.size() > 0) {
String where = "where ACTION_ID in " +
"(" + Joiner.on(",").join(actids) + ")";
stmt.executeUpdate("delete from ACCOUNT_ACTIONS " + where);
stmt.executeUpdate("delete from PROCESSED_ACTIONS " + where);
}
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
/**
* Registers a server with the action system. This method is called automatically the first
* time {@link #getActions(String)} is called.
*/
protected void registerActionServer (final String server)
throws PersistenceException
{
// see if we're already registered
Set<String> set = loadActionServers();
if (set.contains(server)) {
return;
}
// if not, insert ourselves into the action table
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
String query = "insert into ACTION_SERVERS (SERVER) values (?)";
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(query);
stmt.setString(1, server);
stmt.executeUpdate();
return null;
} finally {
JDBCUtil.close(stmt);
}
}
});
log.info("Registered action server '" + server + "'.");
}
/**
* Loads up the set of action servers.
*/
protected Set<String> loadActionServers ()
throws PersistenceException
{
return execute(new Operation<Set<String>>() {
public Set<String> invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
Set<String> set = Sets.newHashSet();
String query = "select * from ACTION_SERVERS";
Statement stmt = null;
try {
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
set.add(rs.getString(1));
}
} finally {
JDBCUtil.close(stmt);
}
return set;
}
});
}
@Override
protected void createTables ()
{
_atable = new Table<AccountAction>(AccountAction.class, TABLE, "ACTION_ID", true);
}
/** The table used to note actions that have taken place. */
protected Table<AccountAction> _atable;
/** Whether or not we're actually being used. */
protected boolean _active;
/** Used by {@link #getActions(String)}. */
protected long _lastActionPrune;
/** We automatically prune the actions table once an hour. */
protected static final long ACTION_PRUNE_INTERVAL = 60 * 60 * 1000L;
/** The name of the account actions table. */
protected static final String TABLE = "ACCOUNT_ACTIONS";
}
@@ -0,0 +1,647 @@
//
// $Id$
package com.threerings.user;
import static com.threerings.user.Log.log;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.samskivert.io.PersistenceException;
import com.samskivert.servlet.SiteIdentifier;
import com.samskivert.servlet.util.CookieUtil;
import com.samskivert.servlet.util.ParameterUtil;
import com.samskivert.util.StringUtil;
public abstract class AffiliateInfo
{
/** The name of the session attribute used to hold the affiliate suffix */
public static final String AFFILIATE_SUFFIX_ATTRIBUTE =
AffiliateInfo.class.getName() + ".AffiliateSuffix";
/**
* Class determining the behavior if the affiliate is 'new referral' i.e. the URL
* has a 'from' and optionally, a 'tag' parameter.
*/
protected static class NewReferral extends AffiliateInfo
{
protected final String fromParameter;
protected final int siteId;
protected final int tagId;
public NewReferral (String fromParameter, int siteId, int tagId)
{
this.fromParameter = fromParameter;
this.siteId = siteId;
this.tagId = tagId;
}
@Override
public void addToContext (HttpServletRequest req)
{
if (isPersonalSuffix(fromParameter)) {
setAffiliateSuffix(req, fromParameter, tagId);
} else {
setAffiliateSuffix(req, "" + siteId, tagId);
}
}
@Override
public String getAffiliateName ()
{
return fromParameter;
}
@Override
public boolean hasName ()
{
return !isPersonalSuffix(fromParameter);
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "" + tagId);
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
if (isPersonalSuffix(fromParameter)) {
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, fromParameter);
} else {
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + siteId);
}
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// TODO probably need nothing
giveCookie(req, rsp, SITE_REFER_COOKIE, fromParameter);
}
@Override
public int getTagId ()
{
return tagId;
}
}
/**
* Class determining the behavior if the wasn't referred to us in this particular
* request, but they were referred to us before, and we've recorded that in cookies.
*/
protected static class CurrentCookies extends AffiliateInfo
{
protected final String siteIdCookieValue;
protected final int tagIdCookieValue;
public CurrentCookies (String siteIdCookieValue, int tagIdCookieValue)
{
this.siteIdCookieValue = siteIdCookieValue;
this.tagIdCookieValue = tagIdCookieValue;
}
@Override
public void addToContext (HttpServletRequest req)
{
setAffiliateSuffix(req, siteIdCookieValue, tagIdCookieValue);
}
@Override
public String getAffiliateName ()
{
return null;
}
@Override
public boolean hasName ()
{
return false;
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "" + tagIdCookieValue);
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, siteIdCookieValue);
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, SITE_REFER_COOKIE, "");
}
@Override
public int getTagId ()
{
return tagIdCookieValue;
}
}
/**
* Class determining the behavior if the user has an 'old-style' cookie indicating
* that they came to us as a referral.
*/
protected static class LegacyCookies extends AffiliateInfo
{
protected final String siteRefer;
protected final int siteId;
public LegacyCookies (int siteId, String siteReferCookie)
{
this.siteRefer = siteReferCookie;
this.siteId = siteId;
}
@Override
public void addToContext (HttpServletRequest req)
{
if (isPersonalSuffix(siteRefer)) {
setAffiliateSuffix(req, siteRefer, AffiliateTag.NO_TAG);
} else {
setAffiliateSuffix(req, "" + siteId, AffiliateTag.NO_TAG);
}
}
@Override
public String getAffiliateName ()
{
if (!isPersonalSuffix(siteRefer)) {
return siteRefer;
} else {
return "";
}
}
@Override
public boolean hasName ()
{
return !isPersonalSuffix(siteRefer);
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// TODO: should we actually be removing the cookie here
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "");
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
if (isPersonalSuffix(siteRefer)) {
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, siteRefer);
} else {
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + siteId);
}
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// TODO: should be be removing this cookie?
giveCookie(req, rsp, SITE_REFER_COOKIE, siteRefer);
}
@Override
public int getTagId ()
{
return AffiliateTag.NO_TAG;
}
}
/**
* Define the behavior if we don't have any affiliate information but we want to track the user
* in a separate category from the 'default site'.
*/
protected static class AlternateDefault extends AffiliateInfo
{
protected final int site;
protected AlternateDefault (int site)
{
this.site = site;
}
@Override
public boolean isDefaultAffiliate ()
{
return true;
}
@Override
public void addToContext (HttpServletRequest req)
{
setAffiliateSuffix(req, "" + site, AffiliateTag.NO_TAG);
}
@Override
public String getAffiliateName ()
{
return "";
}
@Override
public int getTagId ()
{
return AffiliateTag.NO_TAG;
}
@Override
public boolean hasName ()
{
return false;
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// Don't issue a cookie;
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + site);
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// Don't issue a cookie;
}
}
/**
* Determines the behavior if a tag cookie exists, but not an affiliate (site id) cookie.
* "0" will be supplied as the affiliate to the 'affsuf' context attribute.
*/
protected static class TagOnly extends AffiliateInfo
{
protected final int tagId;
public TagOnly (int tagId)
{
this.tagId = tagId;
}
@Override
public void addToContext (HttpServletRequest req)
{
setAffiliateSuffix(req, "0", tagId);
}
@Override
public String getAffiliateName ()
{
return "";
}
@Override
public boolean hasName ()
{
return false;
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, String.valueOf(tagId));
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// do not issue a cookie
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// do not issue a cookie
}
@Override
public int getTagId ()
{
return tagId;
}
}
/**
* Find out whether we know the name of the affiliate.
*/
public abstract boolean hasName ();
/**
* Get the name of the affiliate.
*
* @return - The name as a string.
*/
public abstract String getAffiliateName ();
/**
* Get the id for the affiliate tag.
*/
public abstract int getTagId ();
/**
* Compute the appropriate affiliate 'affsuf' and add it to the session along with the
* 'affid' used by some of the web templates. Now with less Velocity.
*/
public abstract void addToContext (HttpServletRequest req);
/**
* Issue the site_id cookie to the browser (non-velocity).
*/
public abstract void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp);
/**
* Issue the site_refer cookie to the browser.
*/
public abstract void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp);
/**
* Issue the affiliate_tag cookie to the browser (non-velocity).
*/
public abstract void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp);
/**
* Normally returns false, but can be overridden to indicate that we've been given
* a cookie with a default affiliate passed in to us. "Default" isn't quite the right word
* since this is really used for letting pages hardwire their default affiliate to be something
* other than the default, but everything else uses this word, and I don't have a better idea
* offhand.
*/
public boolean isDefaultAffiliate ()
{
return false;
}
/**
* Make an affiliate info object from the cookies and parameters in the request.
*/
public static AffiliateInfo getAffiliateInfo (
OOOUserManager usermgr, ReferralRepository repository, SiteIdentifier identifier,
HttpServletRequest req, boolean parseFromParameter) throws PersistenceException
{
int siteId = identifier.identifySite(req);
return getAffiliateInfo(usermgr, repository, identifier, req, siteId, parseFromParameter);
}
/**
* Make an affiliate info object from the cookies and parameters in the request.
*
* @param alternateDefault - override default affiliate (hard-coded into affiliate pages)
* @param parseFromParameter - Should we look at the request parameter "from"?
* @return - The affiliate info or null if there wasn't enough information in the request to
* construct it.
* @throws PersistenceException - Thrown if accessing the ooouser database failed.
*/
public static AffiliateInfo getAffiliateInfo (
OOOUserManager usermgr, ReferralRepository repository, SiteIdentifier identifier,
HttpServletRequest req, int alternateDefault,
boolean parseFromParameter) throws PersistenceException
{
// if they're already a user with us, bail
if (null != usermgr.loadUser(req)) {
return null;
}
String fromParameter = parseFromParameter ? parseFromParameter(req) : "";
// is this a new referral - i.e. this request was referred from a referral link?
if (!StringUtil.isBlank(fromParameter)) {
String tagParameter = readAffiliateTag(req);
int siteId = parseReferrer(repository, identifier, fromParameter);
if (siteId == 0) {
log.warning("User referred by bogus referrer [referrer=" + fromParameter + "].");
return null;
}
if (!StringUtil.isBlank(tagParameter) && !isPersonalSuffix(fromParameter)) {
int tagId = usermgr.getAffiliateTagId(tagParameter);
return new NewReferral(fromParameter, siteId, tagId);
} else {
return new NewReferral(fromParameter, siteId, AffiliateTag.NO_TAG);
}
}
// do we have modern cookies for this referral?
String siteIdCookieValue = readSiteIdCookie(req);
if (!StringUtil.isBlank(siteIdCookieValue)) {
int tagIdCookieValue = readTagId(req);
return new CurrentCookies(siteIdCookieValue, tagIdCookieValue);
}
// do we have old fashioned cookies for this referral?
String siteReferCookie = CookieUtil.getCookieValue(req, SITE_REFER_COOKIE);
if (!StringUtil.isBlank(siteReferCookie)) {
int siteId = parseReferrer(repository, identifier, siteReferCookie);
return new LegacyCookies(siteId, siteReferCookie);
}
// we have no useful affiliate information. If we have been provided with an alternative
// default site, then use that as the site-id.
if (alternateDefault != identifier.identifySite(req)) {
return new AlternateDefault(alternateDefault);
}
// we have a tag cookie but no affiliate cookie
int tagIdCookieValue = readTagId(req);
if (tagIdCookieValue != AffiliateTag.NO_TAG) {
return new TagOnly(tagIdCookieValue);
}
// we couldn't figure out anything useful to do with whatever affiliate related info
// we had, so we return null and the caller can do nothing.
return null;
}
/**
* @param repository For verifying referrer ids against the database
* @param identifier Velocity site repository
* @param referrer Referral request string, may be a personal referrer of
* the format r[0-9]+ or user-[0-9]+, or a site referrer of format [0-9]+
* @return the referrer siteId, which may be a positive or negative number,
* or 0 if no match.
*/
public static int parseReferrer (ReferralRepository repository, SiteIdentifier identifier,
String referrer)
{
// blank referrer string
if (StringUtil.isBlank(referrer)) {
return 0;
}
// handle new-style personal referrals
if (referrer.matches("r[0-9]+")) {
int refId = 0;
try {
refId = Integer.parseInt(referrer.substring(1));
} catch (NumberFormatException nfe) {
log.warning("Bogus user referral: " + referrer);
return 0;
}
// look up the record in the referrer repository
ReferralRecord rec = null;
try {
rec = repository.lookupReferral(refId);
return (rec == null) ? OOOUser.DEFAULT_SITE_ID : (-1 * rec.referrerId);
} catch (PersistenceException pe) {
log.warning("Error looking up referral " + "[ref=" + referrer + ", error=" + pe
+ "].");
return 0;
}
}
// handle old-style personal referrals
if (referrer.startsWith("user-")) {
try {
return -1 * Integer.parseInt(referrer.substring(5));
} catch (NumberFormatException nfe) {
log.warning("Bogus user referral: " + referrer);
return 0;
}
}
// check referrer string against site repository
int site = identifier.getSiteId(referrer);
if (site != -1) {
return site;
}
log.warning("Bogus site specified in referrer " + "[value=" + referrer + "].");
return 0;
}
/**
* Parse the contents of the 'from' parameter.
*/
public static String parseFromParameter (HttpServletRequest req)
{
String value = ParameterUtil.getParameter(req, "from", true);
if (value != null) {
// strip the crap some affiliates append to their ID
Matcher am = _affregex.matcher(value);
if (am.find()) {
value = am.group();
}
}
return value;
}
/**
* Find out whether the affiliate name is actually a personal referral as
* opposed to a site referral.
*
* @return - True if the affiliate name is a personal referral. False if
* it's a site or unknown.
*/
protected static boolean isPersonalSuffix (String name)
{
return name != null && name.matches("r[0-9]+");
}
/**
* Get the value stored in the site id cookie.
*/
protected static String readSiteIdCookie (HttpServletRequest req)
{
return CookieUtil.getCookieValue(req, AffiliateUtils.AFFILIATE_ID_COOKIE);
}
/**
* Calculate and add the affiliate id and affiliate suffix to the session. No longer
* added to the Velocity context; this is velocity-independant.
* @param req
* @param referrer
* @param tagId
*/
protected void setAffiliateSuffix (HttpServletRequest req, String referrer, int tagId)
{
if (tagId != AffiliateTag.NO_TAG) {
req.getSession().setAttribute(AFFILIATE_SUFFIX_ATTRIBUTE, "-" + referrer + "-" + tagId);
} else {
req.getSession().setAttribute(AFFILIATE_SUFFIX_ATTRIBUTE, "-" + referrer);
}
req.getSession().setAttribute(AffiliateUtils.AFFILIATE_ID_ATTRIBUTE, referrer);
}
/**
* Add a cookie to the response (velocity independant).
* @param req servlet request object
* @param rsp sevlet response object
* @param name cookie identifier
* @param value value for the cookie
*/
protected static void giveCookie (HttpServletRequest req, HttpServletResponse rsp,
String name, String value)
{
Cookie cookie = new Cookie(name, value);
// cookie expires in one month
cookie.setMaxAge(30 * 24 * 60 * 60);
// set the path and widened domain (eg www.domain.com -> .domain.com) of the cookie
cookie.setPath("/");
CookieUtil.widenDomain(req, cookie);
rsp.addCookie(cookie);
}
/**
* Get the affiliate tag from the tag parameter in the request.
*
* @param req - The request.
* @return - The tag string.
*/
protected static String readAffiliateTag (HttpServletRequest req)
{
return req.getParameter("tag");
}
protected static int readTagId (HttpServletRequest req)
{
// if they have an affiliate tag cookie, get it.
String cookie = CookieUtil.getCookieValue(req, AffiliateUtils.AFFILIATE_TAG_COOKIE);
if (cookie != null) {
try {
return Integer.parseInt(cookie);
} catch (NumberFormatException e) {
return AffiliateTag.NO_TAG;
}
} else {
return AffiliateTag.NO_TAG;
}
}
/** A regexp that matches just the proper parts of an affiliate id. */
protected static Pattern _affregex = Pattern.compile(OOOUser.SITE_STRING_REGEX);
/**
* The name of the legacy cookie used to store the affiliate name passed by
* them to us with their original request.
*/
protected static final String SITE_REFER_COOKIE = "site_refer";
}
@@ -0,0 +1,20 @@
//
// $Id$
package com.threerings.user;
/**
* Maintains a mapping from an arbitrary text tag to an integer identifier.
*/
public class AffiliateTag
{
/** Value indicating that the affiliate did not provide a tag **/
public static final int NO_TAG = 0;
/** The automatically generated tag id. */
public int tagId;
/** The arbitrary text tag. */
public String tag;
}
@@ -0,0 +1,40 @@
//
// $Id$
/*
* Copyright (C) 2007-2010 Three Rings Design, Inc.
* Copyright 1999,2004 The Apache Software Foundation.
*/
package com.threerings.user;
/**
* Utilities for dealing with affiliate tagging.
*/
public class AffiliateUtils {
/** Request parameter containing integer affiliate site id. */
public static final String AFFILIATE_ID_PARAMETER = "affiliate";
/** before the days of affiliate, there was from,
* which also supports referrals from in-game users */
public static final String FROM_PARAMETER = "from";
/** Request parameter containing sub-affiliate tag; an arbitrary integer usable
* by the partner to distinguish different placements on their site. */
public static final String AFFILIATE_TAG_PARAMETER = "tag";
/** Affiliate cookie name.
* this cookie is also used for legacy personal referrals */
public static final String AFFILIATE_ID_COOKIE = "site_id";
/** Affiliate arbitrary tag cookie name. */
public static final String AFFILIATE_TAG_COOKIE = "affiliate_tag";
/** The name of the session attribute used to hold the affiliate id */
public static final String AFFILIATE_ID_ATTRIBUTE = AffiliateUtils.class.getName() + ".AffiliateId";
/** The name of the session attribute used to hold the affiliate tag */
public static final String AFFILIATE_TAG_ATTRIBUTE = AffiliateUtils.class.getName() + ".AffiliateTag";
}
@@ -0,0 +1,22 @@
//
// $Id$
package com.threerings.user;
/**
* Represents a row in the BANNED_IDENTS table.
*/
public class BannedIdent
{
/** A 'unique' id for a specific machine we have seen. */
public String machIdent;
/** The site id which this machine is banned from. */
public int siteId;
@Override
public String toString ()
{
return "(" + siteId + ") " + machIdent;
}
}
@@ -0,0 +1,122 @@
//
// $Id$
package com.threerings.user;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.StringTokenizer;
import com.samskivert.net.HttpPostUtil;
import com.samskivert.util.Logger;
import com.samskivert.util.ServiceWaiter;
import com.samskivert.util.StringUtil;
/**
* Utility methods to verify that a user is actually a Shockwave Gameblast
* member.
*
* See GameBlastRemoteAPI.htm in the docs.
*/
public class BlastVerify
{
/** Return status code indicating that the account verified. */
public static final byte VERIFIED = 0;
/** Return status code indicating invalid account. */
public static final byte INVALID_ACCOUNT = 1;
/** Return status code indicating an expired account. */
public static final byte EXPIRED = 2;
/** Return status code indicating that there were technical difficulties
* and the blastiness of the user cannot be determined at this time. */
public static final byte RETRY = 3;
/** Returns status code indicating invalid account. */
public static final byte INVALID_PASSWORD = 4;
/**
* Verify the username/password to ensure that it's an active shockwave
* blast user.
*
* @return true if the account is in good standing, false if it doesn
*/
public static byte verifyBlastUser (String username, String password)
{
String request = "member_name=" + StringUtil.encode(username) +
"&password=" + StringUtil.encode(password);
String response;
try {
response = HttpPostUtil.httpPost(LOGIN_URL, request, TIMEOUT);
} catch (ServiceWaiter.TimeoutException te) {
return RETRY;
} catch (IOException ioe) {
log.warning("Error communicating with blast", ioe);
return RETRY;
}
// we just do a very simplistic parsing of the response
StringTokenizer st = new StringTokenizer(response);
while (st.hasMoreTokens()) {
String s = st.nextToken();
if (s.startsWith("Error_Code")) {
StringTokenizer st2 = new StringTokenizer(s, " =\"");
if (st2.countTokens() == 2) {
st2.nextToken();
String code = st2.nextToken();
try {
int result = Integer.parseInt(code);
switch (result) {
case 1: return VERIFIED;
case 2: return INVALID_PASSWORD;
case 3: return EXPIRED;
case 4: return INVALID_ACCOUNT;
default: return RETRY;
}
} catch (NumberFormatException nfe) {
// handled below
break;
}
} else {
break;
}
}
}
log.warning("Unable to parse response from blast [resp=\"" + response + "\"].");
return RETRY;
}
/** How long we wait while trying to verify. */
protected static final int TIMEOUT = 30;
/** The URL for username/password verification. */
protected static URL LOGIN_URL;
/** Used for logging. */
protected static final Logger log = Logger.getLogger(BlastVerify.class);
static {
try {
LOGIN_URL = new URL(
// "http://gameblastbeta.shockwave.com" + // testing URL
"https://transactor.shockwave.com" + // real URL
"/servlet/LoginRemote");
} catch (MalformedURLException mue) {
log.warning("Bad URL specification", mue);
}
}
}
@@ -0,0 +1,62 @@
//
// $Id$
package com.threerings.user;
import java.sql.Timestamp;
import com.samskivert.util.StringUtil;
/**
* Tracks conversion related actions taken by our users.
*/
public class ConversionRecord
{
/** Indicates that an account was created. */
public static final int CREATED = 1;
/** Indicates that an account that was not currently paying us started
* to. This won't be true before 2005/03/01 where we tried (more or
* less) to save everytime someone gave us money. */
public static final int SUBSCRIPTION_START = 2;
/** Indicates that a subscription was canceled or lapsed from use. */
public static final int SUBSCRIPTION_ENDED = 3;
/** The unique identifier of the user that took an action. */
public int userId;
/**
* The partner with whom the user is associated. You should always
* use the accessor method to access this value in order to correctly
* deal with personal referrals.
*/
public int siteId;
/** The identifier indicating the action taken. */
public short action;
/** The date on which the action was taken. */
public Timestamp recorded;
/**
* Returns the site id associated with this record, properly mapping
* negative ids (personal referrals) into a single category.
*/
public int getSiteId ()
{
return (siteId <= 0) ? OOOUser.REFERRAL_SITE_ID : siteId;
}
@Override
public String toString ()
{
return StringUtil.fieldsToString(this);
}
public boolean equals (ConversionRecord cr)
{
return (cr.userId == userId && cr.siteId == siteId &&
cr.action == action && cr.recorded.equals(recorded));
}
}
@@ -0,0 +1,292 @@
//
// $Id$
package com.threerings.user;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Maps;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.JORARepository;
import com.samskivert.jdbc.jora.Table;
import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.IntIntMap;
import com.samskivert.util.Calendars;
import static com.threerings.user.Log.log;
/**
* Provides an interface to the conversion repository. A service that
* makes use of the OOO user management and billing system can make use of
* this conversion service to track conversion related information for
* their users.
*/
public class ConversionRepository extends JORARepository
{
/**
* The database identifier used when establishing a database
* connection. This value being <code>conversiondb</code>.
*/
public static final String CONVERSION_DB_IDENT = "conversiondb";
/**
* Creates the repository and prepares it for operation.
*
* @param provider the database connection provider.
*/
public ConversionRepository (ConnectionProvider provider)
throws PersistenceException
{
super(provider, CONVERSION_DB_IDENT);
// figure out whether or not the forums are in use on this system
execute(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_active = JDBCUtil.tableExists(conn, "CONVERSION");
if (!_active) {
log.info("No conversion table. Disabling.");
}
return null;
}
});
// TEMP CODE To update the repository to using a full integer for
// siteId, as personal referrer ids are -userIds which can be
// quite large~
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
if (!_active) {
return null;
}
if (JDBCUtil.getColumnType(conn, "CONVERSION", "SITE_ID") == Types.SMALLINT) {
JDBCUtil.changeColumn(conn, "CONVERSION", "SITE_ID",
"SITE_ID INTEGER NOT NULL");
}
return null;
}
});
}
/**
* Records an action in the conversion table.
*/
public void recordAction (int userId, int siteId, int action)
throws PersistenceException
{
recordAction(userId, siteId, action, new Timestamp(System.currentTimeMillis()));
}
/**
* Records an action in the conversion table.
*/
public void recordAction (int userId, int siteId, int action, Timestamp recorded)
throws PersistenceException
{
if (!_active) {
return;
}
ConversionRecord record = new ConversionRecord();
record.userId = userId;
record.siteId = siteId;
record.action = (short)action;
record.recorded = recorded;
insert(_ctable, record);
}
/**
* Get the subscriber info for a given date broken up by site. Key 0
* references a total for all sites.
*/
public IntIntMap getSiteSubscribers (java.util.Date date)
{
checkRecomputeSubInfo();
return _subscribers.get(date);
}
/**
* Return the total number of subscribers on then given date.
*/
public int getTotalSubscribers (java.util.Date date)
{
checkRecomputeSubInfo();
return _subscribers.get(date).get(0);
}
/**
* Returns all of our subscription info. Date -> IntIntMap,
* IntIntMap: SiteId -> Subscribers. SiteId 0 is a total of all sites.
*/
public Map<Date, IntIntMap> getSubscriptionInfo ()
{
checkRecomputeSubInfo();
return _subscribers;
}
/**
* Recompute our subscription info if it is older than the cache time.
*/
protected void checkRecomputeSubInfo ()
{
if (_subTime + SUB_CACHE_TIME < System.currentTimeMillis()) {
try {
computeSubscriptionInfo();
} catch (PersistenceException pe) {
log.warning("Failed to compute our subscription info: " + pe);
}
}
}
/**
* Build up all the subscriber data over time and affiliate.
*/
protected void computeSubscriptionInfo ()
throws PersistenceException
{
// get all the raw data from the db, ordered by date
List<ConversionRecord> data = loadAll(_ctable, "where ACTION in (" +
ConversionRecord.SUBSCRIPTION_START + ", " +
ConversionRecord.SUBSCRIPTION_ENDED + ") order by RECORDED");
java.util.Date recent = null;
ArrayIntSet subs = new ArrayIntSet();
HashIntMap<ArrayIntSet> siteSubs = new HashIntMap<ArrayIntSet>();
// these are used to do the right thing if we get both a subscribe
// and unsubscribe action in the same day, but in the wrong order
ArrayIntSet subexcept = new ArrayIntSet();
ArrayIntSet unsubexcept = new ArrayIntSet();
Iterator<ConversionRecord> itr = data.iterator();
while (itr.hasNext()) {
ConversionRecord cr = itr.next();
// we only care about the date, not the time, at which the
// entry was recorded since we are hashing by day
java.util.Date day = Calendars.at(cr.recorded).zeroTime().toDate();
// if the date changes, store the data for the most recent day
if (recent == null) {
recent = day;
} else if (!recent.equals(day)) {
subexcept.clear();
unsubexcept.clear();
addSubscriptionEntry(recent, subs, siteSubs);
recent = day;
}
// make sure we have a sitewise set of subscribers
if (!siteSubs.containsKey(cr.getSiteId())) {
siteSubs.put(cr.getSiteId(), new ArrayIntSet());
}
ArrayIntSet siteMap = siteSubs.get(cr.getSiteId());
// update our data
if (cr.action == ConversionRecord.SUBSCRIPTION_START) {
// handle not-subscribed -> unsubscribe -> subscribe
if (!checkExcept(unsubexcept, subexcept,
subs.contains(cr.userId), cr)) {
subs.add(cr.userId);
siteMap.add(cr.userId);
}
} else if (cr.action == ConversionRecord.SUBSCRIPTION_ENDED) {
// handle is-subscribed -> subscribe -> unsubscribe
if (!checkExcept(subexcept, unsubexcept,
!subs.contains(cr.userId), cr)) {
subs.remove(cr.userId);
siteMap.remove(cr.userId);
}
}
}
// add in the incomplete current day info
addSubscriptionEntry(Calendars.now().zeroTime().toDate(), subs, siteSubs);
// and mark when we finished
_subTime = System.currentTimeMillis();
}
protected boolean checkExcept (ArrayIntSet expect, ArrayIntSet surprise,
boolean currentStatus, ConversionRecord cr)
{
// first check to see if he's in the opposite exception set; in
// which case we clear him out and ignore this action
if (expect.remove(cr.userId)) {
// Log.info("Processed exception [rec=" + cr +
// ", status=" + currentStatus + "].");
return true;
}
// next, check to see if he's already in the expected state, in which
// case we put him into the exception set
return currentStatus;
}
/**
* Add an entry to the _subscriptions data for the given day.
*/
protected void addSubscriptionEntry (java.util.Date day, ArrayIntSet subs,
HashIntMap<ArrayIntSet> siteSubs)
{
IntIntMap daysubs = new IntIntMap();
for (Map.Entry<Integer,ArrayIntSet> entry : siteSubs.entrySet()) {
ArrayIntSet set = entry.getValue();
int key = entry.getKey().intValue();
daysubs.put(key, set.size());
}
daysubs.put(0, subs.size());
_subscribers.put(day, daysubs);
}
@Override
protected void createTables ()
{
_ctable = new Table<ConversionRecord>(
// this isn't the real primary key, but we never load conversion
// records individually
ConversionRecord.class, "CONVERSION", "RECORDED", true);
}
/**
* Contains subscription info about our users over time. Keys are
* dates and the values are IntIntMaps mapping siteId -> subsrivers
* with 0 holding the total for all sites.
*/
protected Map<Date, IntIntMap> _subscribers = Maps.newHashMap();
/** The time at which we last computed our subscriber info. */
protected long _subTime;
/** The time in millis to cache our subscriber info before recomputing. */
protected long SUB_CACHE_TIME = 1l * 60l * 60l * 1000l;
/** If we don't detect our tables, we deactivate ourselves. */
protected boolean _active;
/** The table used to new billing actions that have taken place. */
protected Table<ConversionRecord> _ctable;
/** The date (3/17/2005 in ms) after which we should complain about
wacky conversion data. */
protected static long FIXED_DATE = 1111090429312L;
}
@@ -0,0 +1,34 @@
//
// $Id$
package com.threerings.user;
import java.sql.Date;
/**
* Used to load detailed information about a user from the OOO user
* database tables.
*/
public class DetailedUser
{
/** The user's assigned integer userid. */
public int userId;
/** The user's chosen username. */
public String username;
/** The date this record was created. */
public Date created;
/** The user's email address. */
public String email;
/** The user's birthday. */
public Date birthday;
/** The user's gender. */
public byte gender;
/** The missive provided by the user during registration. */
public String missive;
}
@@ -0,0 +1,66 @@
//
// $Id$
package com.threerings.user;
import com.samskivert.util.ByteEnum;
import com.samskivert.util.StringUtil;
/**
* Represents external authentication sources supported by OOOuser.
*/
public enum ExternalAuther
implements ByteEnum
{
FACEBOOK(1, "facebook.com"),
OPEN_SOCIAL(2, "opensocial.com"),
OPEN_ID(3, "openid.net"),
HEYZAP(4, "heyzap.com") {
@Override public String makeEmail (String externalId) {
return super.makeEmail(StringUtil.encode(externalId));
}
@Override public String getExternalId (String email) {
return StringUtil.decode(super.getExternalId(email));
}
},
STEAM(5, "steampowered.com"),
SEGA_PASS(6, "sega.com"),
KONGREGATE(7, "kongregate.com");
/**
* Creates a fake email address given the supplied external user id.
*/
public String makeEmail (String externalId)
{
return externalId + "@" + _domain;
}
/**
* Returns the external id associated with the given email for this auther
*/
public String getExternalId (String email)
{
int idx = email.indexOf("@");
if (idx <= 0) {
return email;
}
return email.substring(0, idx);
}
// from ByteEnum
public byte toByte ()
{
return _code;
}
ExternalAuther (int code, String domain) {
if (code < 1 || code > 31) { // these must fit in an int
throw new IllegalArgumentException("Code out of byte range: " + code);
}
_code = (byte)code;
_domain = domain;
}
protected byte _code;
protected String _domain;
}
@@ -0,0 +1,164 @@
//
// $Id$
package com.threerings.user;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.JORARepository;
/**
* Stores gameblast aux data for the X users that actually need it, where
* X is a small and annoying number.
*/
public class GameBlastAuxRepository extends JORARepository
{
/**
* Constructs a new GameBlastAuxRepository.
*/
public GameBlastAuxRepository (ConnectionProvider provider)
throws PersistenceException
{
super(provider, OOOUserRepository.USER_REPOSITORY_IDENT);
}
@Override
protected void createTables ()
{
// nada
}
/**
* @return the username/password for the specified userId, or null
* if none exists.
*/
public String[] getAuxData (final int userId)
throws PersistenceException
{
return execute(new Operation<String[]>() {
public String[] invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = conn.createStatement();
try {
String query = "select LOGIN, PASSWORD from " +
"GAMEBLAST_AUX where USER_ID = " + userId;
ResultSet rs = stmt.executeQuery(query);
if (rs.next()) {
return new String[] { rs.getString(1),
rs.getString(2) };
}
return null; // didn't find
} finally {
JDBCUtil.close(stmt);
}
}
});
}
/**
* Ensure that the specified gameblast login is is not present or if it is,
* only belongs to the specified user.
*/
public boolean ensureLoginUnique (final String login, final int userId)
throws PersistenceException
{
Boolean boolval = execute(new Operation<Boolean>() {
public Boolean invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement("select USER_ID from " +
"GAMEBLAST_AUX where LOGIN=?");
stmt.setString(1, login);
ResultSet rs = stmt.executeQuery();
// return true if there are no matches or if the first
// one is us. (There should never be two, anyway).
return Boolean.valueOf(
!rs.next() || (rs.getInt(1) == userId));
} finally {
JDBCUtil.close(stmt);
}
}
});
return boolval.booleanValue();
}
/**
* Save the data for the specified user.
*/
public void saveAuxData (final int userId,
final String login, final String password)
throws PersistenceException
{
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
PreparedStatement stmt = null;
try {
// try updating first
stmt = conn.prepareStatement(
"update GAMEBLAST_AUX set " +
"LOGIN=?, PASSWORD=? where USER_ID=?");
stmt.setString(1, login);
stmt.setString(2, password);
stmt.setInt(3, userId);
if (stmt.executeUpdate() != 1) {
// insert a new one
PreparedStatement stmt2 = null;
try {
stmt2 = conn.prepareStatement(
"insert into GAMEBLAST_AUX " +
"(USER_ID, LOGIN, PASSWORD) values (?, ?, ?)");
stmt2.setInt(1, userId);
stmt2.setString(2, login);
stmt2.setString(3, password);
JDBCUtil.checkedUpdate(stmt2, 1);
} finally {
JDBCUtil.close(stmt2);
}
}
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
/**
* Remove the gameblast mapping for the specified user.
*/
public void removeAuxData (final int userId)
throws PersistenceException
{
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = conn.createStatement();
try {
String query = "delete from GAMEBLAST_AUX where USER_ID=" +
userId;
stmt.executeUpdate(query);
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
}
@@ -0,0 +1,24 @@
//
// $Id$
package com.threerings.user;
import java.sql.Date;
/**
* Retains information about a historical user registration.
*/
public class HistoricalUser
{
/** The user's assigned integer userid. */
public int userId;
/** The user's chosen username. */
public String username;
/** The date this record was created. */
public Date created;
/** The affiliate site with which this user is associated. */
public int siteId;
}
@@ -0,0 +1,15 @@
//
// $Id$
package com.threerings.user;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by the user package.
*/
public class Log
{
/** The log instance that will be used to log all messages for the user package. */
public static Logger log = Logger.getLogger("com.threerings.user");
}
@@ -0,0 +1,106 @@
//
// $Id$
package com.threerings.user;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.samskivert.util.Interval;
import static com.threerings.user.Log.log;
/**
* Tracks logins by a user identifier and if they attempt to login too often, lets us know.
* Note that the period is how often we clear the entire table, so specific users may find
* up to 2x the attempt rate during their first set of attempts if it happens to cross the
* boundary.
*/
public class LoginThrottle<K>
{
public LoginThrottle (int maxLogins, long period)
{
_maxLogins = maxLogins;
(new Interval() {
@Override
public void expired ()
{
synchronized(_recentLogins) {
_recentLogins.clear();
}
}
}).schedule(period, true);
}
/**
* Notes that the user attempted to login.
* @return Whether they're allowed to continue based on throttle settings.
*/
@Deprecated
public boolean noteLogin (K userIdentifier)
{
synchronized(_recentLogins) {
int loginCount = 1 + _recentLogins.add(userIdentifier, 1);
if (loginCount > _maxLogins) {
recordThrottledAttempt(userIdentifier, loginCount);
return false;
} else {
return true;
}
}
}
/**
* Notes that the user successfully logged in (so reduces their count by 1).
*/
@Deprecated
public void noteLoginSuccess (K userIdentifier)
{
synchronized(_recentLogins) {
_recentLogins.add(userIdentifier, -1);
}
}
/**
* Should we block a login attempt because the user has already failed too many times?
*/
public boolean isLoginAttemptBlocked (K userIdentifier)
{
int count;
synchronized (_recentLogins) {
count = _recentLogins.count(userIdentifier);
}
if (_maxLogins <= count) {
recordThrottledAttempt(userIdentifier, count);
return true;
}
return false;
}
/**
* Note a failed login, and return true if it's time to tell them that we'll block
* them for a little while.
*/
public boolean noteFailedLogin (K userIdentifier)
{
synchronized (_recentLogins) {
return _maxLogins <= 1 + _recentLogins.add(userIdentifier, 1);
}
}
/**
* Log that we had a failed attempt. Can be overridden if you really don't care about tracking
* that sort of thing.
*/
protected void recordThrottledAttempt (K userIdentifier, int loginCount)
{
log.info("Throttled login attempt", "identifier", userIdentifier, "loginCount", loginCount);
}
/** How many logins they're allowed to try during a period. */
protected int _maxLogins;
/** Recent login attempt counts by user identifier. */
protected Multiset<K> _recentLogins = HashMultiset.create();
}
@@ -0,0 +1,32 @@
//
// $Id$
package com.threerings.user;
import java.sql.Date;
import com.samskivert.util.StringUtil;
/**
* Auxiliary information relating to our registered users.
*/
public class OOOAuxData
{
/** The user's unique identifier. */
public int userId;
/** The user's birthday. */
public Date birthday;
/** The user's gender. */
public byte gender;
/** The user's personal missive to us. */
public String missive;
@Override
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
@@ -0,0 +1,22 @@
//
// $Id$
package com.threerings.user;
import java.sql.Timestamp;
/**
* Contains additional information on users that have paid us in one form or
* another.
*/
public class OOOBillAuxData
{
/** The user's unique identifier. */
public int userId;
/** The first time the user bought coins **/
public Timestamp firstCoinBuy;
/** The most recent time the user bought coins **/
public Timestamp latestCoinBuy;
}
@@ -0,0 +1,21 @@
//
// $Id$
package com.threerings.user;
import com.samskivert.servlet.Site;
/**
* Used to identify OOO sites.
*/
public class OOOSite extends Site
{
/** The domain at which this site is found, e.g. puzzlepirates.com */
public final String domain;
public OOOSite (int siteId, String siteString, String domain)
{
super(siteId, siteString);
this.domain = domain;
}
}
@@ -0,0 +1,127 @@
//
// $Id$
package com.threerings.user;
import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import com.google.common.collect.Lists;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.servlet.JDBCTableSiteIdentifier;
import com.samskivert.servlet.Site;
import com.samskivert.servlet.util.CookieUtil;
import com.samskivert.util.StringUtil;
import static com.threerings.user.Log.log;
/**
* Handles the identification of OOO sites without requiring us to maintain a domains everywhere we
* use our webapps. It still uses the "sites" table for affiliate handling, so we can set up
* affiliates using the register webapp and whatnot, but it uses a static mapping (defined in
* OOOUser) for our domains and site ids.
*/
public class OOOSiteIdentifier extends JDBCTableSiteIdentifier
{
/** A request property that can be used to override the domain-based site identification. See
* {@link HttpServletRequest#setAttribute}. */
public static final String SITE_ID_OVERRIDE_KEY = "SiteIdentifierOverride";
/** A cookie that can be provided with a request to override the domain-based site
* identification. This is superceded by {@link #SITE_ID_OVERRIDE_KEY}. */
public static final String SITE_COOKIE = "site";
public OOOSiteIdentifier (ConnectionProvider conprov)
throws PersistenceException
{
super(conprov);
}
@Override // from JDBCTableSiteIdentifier
public int identifySite (HttpServletRequest req)
{
// look for request parameter, accept it as an override and store for the session
String requestParam = req.getParameter(SITE_ID_OVERRIDE_KEY);
if (requestParam != null) {
try {
Integer siteId = Integer.parseInt(requestParam);
req.getSession().setAttribute(SITE_ID_OVERRIDE_KEY, siteId);
return siteId;
} catch (Exception e) {
log.warning("Received invalid site override param", "site", requestParam);
// fall through to other methods
}
}
// check for override in the request attributes and store in the session
Integer attributeOverride = (Integer)req.getAttribute(SITE_ID_OVERRIDE_KEY);
if (attributeOverride != null) {
req.getSession().setAttribute(SITE_ID_OVERRIDE_KEY, attributeOverride);
return attributeOverride;
}
// check whether our site id was overridden during this session
Integer override = (Integer)req.getSession().getAttribute(SITE_ID_OVERRIDE_KEY);
if (override != null) {
return override;
}
// check for a site cookie
String sitecookie = CookieUtil.getCookieValue(req, SITE_COOKIE);
if (!StringUtil.isBlank(sitecookie)) {
try {
return Integer.parseInt(sitecookie);
} catch (Exception e) {
log.warning("Received invalid site cookie", "site", sitecookie);
// fall through to the domain parsing
}
}
// otherwise we just use a static mapping
String serverName = req.getServerName();
for (OOOSite site : OOOUser.SITES) {
if (serverName.endsWith(site.domain)) {
return site.siteId;
}
}
return super.identifySite(req);
}
@Override // from JDBCTableSiteIdentifier
public String getSiteString (int siteId)
{
for (OOOSite site : OOOUser.SITES) {
if (site.siteId == siteId) {
return site.siteString;
}
}
return super.getSiteString(siteId);
}
@Override // from JDBCTableSiteIdentifier
public int getSiteId (String siteString)
{
for (OOOSite site : OOOUser.SITES) {
if (site.siteString.equals(siteString)) {
return site.siteId;
}
}
return super.getSiteId(siteString);
}
@Override // from JDBCTableSiteIdentifier
public Iterator<Site> enumerateSites ()
{
List<Site> sites = Lists.newArrayList(super.enumerateSites());
for (OOOSite site : OOOUser.SITES) {
if (!_sitesById.containsKey(site.siteId)) {
sites.add(site);
}
}
return sites.iterator();
}
}
@@ -0,0 +1,617 @@
//
// $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;
/** 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<OOOSite> 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"));
/** 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;
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;
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<Integer> 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 <code>super</code>, it's considered "legal." Yay!
@Override
protected void setDirtyMask (FieldMask mask)
{
super.setDirtyMask(mask);
}
}
@@ -0,0 +1,47 @@
//
// $Id$
package com.threerings.user;
import com.google.common.base.Function;
/**
* Identifying information and flags for a {@link OOOUser} object. This is for use when querying
* a potentially large number of users meeting some criteria and only each user's name and flags
* are required.
*/
public class OOOUserCard
{
/** Converts a card to a username. */
public static Function<OOOUserCard, String> TO_USERNAME = new Function<OOOUserCard, String>() {
public String apply (OOOUserCard card) {
return card.username;
}
};
/**
* Creates a new user card with the given field values.
*/
public OOOUserCard (int userid, String username, int flags)
{
this.userid = userid;
this.username = username;
this.flags = flags;
}
/**
* Creates a new user card for deserialization.
*/
public OOOUserCard ()
{
}
/** The unique id of the user. */
public int userid;
/** The name of the user. */
public String username;
/** The {@link OOOUser#flags} of the user. */
public int flags;
}
@@ -0,0 +1,186 @@
//
// $Id$
package com.threerings.user;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import com.google.common.collect.Maps;
import com.samskivert.io.PersistenceException;
import com.samskivert.util.RunQueue;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.servlet.RedirectException;
import com.samskivert.servlet.user.User;
import com.samskivert.servlet.user.UserManager;
import com.samskivert.servlet.user.UserRepository;
import static com.threerings.user.Log.log;
/**
* Extends the standard user manager with OOO-specific support.
*/
public class OOOUserManager extends UserManager
{
/**
* Creates our OOO User manager and prepares it for operation.
*/
public OOOUserManager (Properties config, ConnectionProvider conprov)
throws PersistenceException
{
this(config, conprov, null);
}
/**
* Creates our OOO user manager and prepares it for operation.
*/
public OOOUserManager (Properties config, ConnectionProvider conprov, RunQueue pruneQueue)
throws PersistenceException
{
// legacy business
init(config, conprov, pruneQueue);
}
/**
* Creates an OOO user manager which must subsequently be initialized with
* a call to {@link #init}.
*/
public OOOUserManager ()
{
}
@Override
public OOOUserRepository getRepository ()
{
return (OOOUserRepository)_repository;
}
@Override
public void init (Properties config, ConnectionProvider conprov, RunQueue pruneQueue)
throws PersistenceException
{
super.init(config, conprov, pruneQueue);
// create the blast repository
_blastRepo = new GameBlastAuxRepository(conprov);
// look up the access denied URL
_accessDeniedURL = config.getProperty("access_denied_url");
if (_accessDeniedURL == null) {
log.warning("No 'access_denied_url' supplied in user manager config. " +
"Restricted pages will behave strangely.");
}
// load up our affiliate tag mappings
for (AffiliateTag sub : getRepository().loadAffiliateTags()) {
_tagMap.put(sub.tag, sub.tagId);
}
}
/**
* Get the gameblast aux data repository.
*/
public GameBlastAuxRepository getBlastRepository ()
{
return _blastRepo;
}
/**
* Returns the id to which the specified tag has been mapped, assigning a new tag id if
* necessary.
*
* @return -1 if an error occurred assigning a new id.
*/
public int getAffiliateTagId (String tag)
{
// if we've already mapped this value, we're good to go
Integer tagId = _tagMap.get(tag);
if (tagId != null) {
return tagId;
}
// register a new affiliate tag and add it to our mappings
try {
tagId = getRepository().registerAffiliateTag(tag);
_tagMap.put(tag, tagId);
return tagId;
} catch (PersistenceException pe) {
log.warning("Failed to register new affiliate tag '" + tag + "'.", pe);
return -1;
}
}
/**
* Both tag strings and ids are unique, so grab the string (key) based on the id (value)
* @param tagId ID to search for (eg 45)
* @return String value of the tag or tags (eg "nov07GroupA::DF34A9")
*/
public String getAffiliateTagString (int tagId)
{
Integer theTagId = Integer.valueOf(tagId);
for (Map.Entry<String,Integer> entry : _tagMap.entrySet()) {
if (theTagId.equals(entry.getValue())) {
return entry.getKey();
}
}
return null;
}
/**
* Extends the standard {@link #requireUser(HttpServletRequest)} with
* the additional requirement that the user hold the specified token.
* If they do not, they will be redirected to a page informing them
* access is denied.
*
* @return the user associated with the request.
*/
public User requireUser (HttpServletRequest req, byte token)
throws PersistenceException, RedirectException
{
OOOUser user = (OOOUser)requireUser(req);
if (!user.holdsToken(token)) {
throw new RedirectException(_accessDeniedURL);
}
return user;
}
/**
* Extends the standard {@link #requireUser(HttpServletRequest)} with
* the additional requirement that the user hold one of the specified
* tokens. If they do not, they will be redirected to a page informing
* them access is denied.
*
* @return the user associated with the request.
*/
public User requireUser (HttpServletRequest req, byte[] tokens)
throws PersistenceException, RedirectException
{
OOOUser user = (OOOUser)requireUser(req);
if (!user.holdsAnyToken(tokens)) {
throw new RedirectException(_accessDeniedURL);
}
return user;
}
@Override
protected UserRepository createRepository (ConnectionProvider conprov)
throws PersistenceException
{
return new OOOUserRepository(conprov);
}
/** The repository for gameblast auxiliary data. */
protected GameBlastAuxRepository _blastRepo;
/** The URL to which we redirect users whose access is denied. */
protected String _accessDeniedURL;
/** Maintains a mapping of affiliate tags. */
protected Map<String,Integer> _tagMap = Maps.newHashMap();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
//
// $Id$
package com.threerings.user;
/**
* Defines an XML-RPC interface to certain ooouser services. This is implemented by the register
* webapp, but can be implemented by other systems that wish to provide API compatibility with a
* potentially different backend implementation.
*/
public interface OOOXmlRpcService
{
/**
* Returns true if the supplied credentials are valid, false otherwise.
*
* @param username the name of the user in question.
* @param password the MD5 encoded password for the user.
*/
boolean authUser (String username, String password);
}
@@ -0,0 +1,25 @@
//
// $Id$
package com.threerings.user;
import java.sql.Date;
/**
* Records information on a particular referral.
*/
public class ReferralRecord
{
/** A unique identifier for this referral record. */
public int referralId;
/** The date on which the referral was made (used to expire old
* unutilized referrals). */
public Date recorded;
/** The OOO user id of the referring user. */
public int referrerId;
/** Data associated with this referral record. */
public String data;
}
@@ -0,0 +1,169 @@
//
// $Id$
package com.threerings.user;
import java.sql.Connection;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.Statement;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JORARepository;
import com.samskivert.jdbc.jora.Table;
import static com.threerings.user.Log.log;
/**
* Tracks the data needed for our personal referral system whereby one
* user sends a referral link (or email) to their friend and we track the
* original referring user if their referred friend actually registers
* within some reasonable time frame (like a couple of weeks).
*/
public class ReferralRepository extends JORARepository
{
/**
* The database identifier used when establishing a database
* connection. This value being <code>referraldb</code>.
*/
public static final String REFERRAL_DB_IDENT = "referraldb";
/**
* Creates the repository and prepares it for operation.
*
* @param provider the database connection provider.
*/
public ReferralRepository (ConnectionProvider provider)
throws PersistenceException
{
super(provider, REFERRAL_DB_IDENT);
// figure out whether or not the referral system is in use
execute(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_active = JDBCUtil.tableExists(conn, "REFERRAL");
if (!_active) {
log.info("No referral table. Disabling.");
}
return null;
}
});
}
/**
* Records a new referral record in the system and returns the unique
* identifier for said record.
*/
public int recordReferral (int referrerId, String data)
throws PersistenceException
{
if (!_active) {
throw new PersistenceException("Referral repository is not available.");
}
final ReferralRecord record = new ReferralRecord();
record.recorded = new Date(System.currentTimeMillis());
record.referrerId = referrerId;
record.data = data;
record.referralId = insert(_rtable, record);
checkPurge();
return record.referralId;
}
/**
* Looks up a referral record with the specified id, returning null if
* no matching record could be found.
*/
public ReferralRecord lookupReferral (int referralId)
throws PersistenceException
{
return lookupReferralBy("where REFERRAL_ID = " + referralId);
}
/**
* Looks up a referral record with the specified referring user id,
* returning null if no matching record could be found.
*/
public ReferralRecord lookupReferrer (int referrerId)
throws PersistenceException
{
return lookupReferralBy("where REFERRER_ID = " + referrerId);
}
/** Looks up a referral record matching the specified query. */
protected ReferralRecord lookupReferralBy (final String query)
throws PersistenceException
{
if (!_active) {
return null;
}
ReferralRecord record = load(_rtable, query);
checkPurge();
return record;
}
/**
* Used to periodically purge old referral records from the
* repository.
*/
protected void checkPurge ()
{
long now = System.currentTimeMillis();
if (now - _lastPurge > PURGE_INTERVAL) {
_lastPurge = now;
try {
purgeStaleReferrals();
} catch (PersistenceException pe) {
log.warning("Error purging referrals: " + pe);
}
}
}
/**
* Purges stale referral records from the repository.
*/
protected void purgeStaleReferrals ()
throws PersistenceException
{
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
String pquery = "delete from REFERRAL " +
"where DATE_SUB(NOW(), INTERVAL 1 MONTH) > RECORDED;";
Statement stmt = conn.createStatement();
try {
stmt.executeUpdate(pquery);
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
@Override
protected void createTables ()
{
_rtable = new Table<ReferralRecord>(ReferralRecord.class, "REFERRAL", "REFERRAL_ID", true);
}
/** The table used to new billing actions that have taken place. */
protected Table<ReferralRecord> _rtable;
/** If we don't detect our tables, we deactivate ourselves. */
protected boolean _active;
/** The last time we purged the repository of stale records. */
protected long _lastPurge;
/** We purge the repository every three hours of stale records. */
protected static final long PURGE_INTERVAL = 3 * 60 * 60 * 1000L;
}
@@ -0,0 +1,59 @@
//
// $Id$
/*
* Copyright (C) 2007-2010 Three Rings Design, Inc.
* Copyright 1999,2004 The Apache Software Foundation.
*/
package com.threerings.user;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.StaticConnectionProvider;
import com.samskivert.servlet.JDBCTableSiteIdentifier;
import com.samskivert.servlet.SiteIdentifier;
import com.samskivert.util.Config;
/**
* Encapsulates multiple repositories, and offers public access to all of them.
*/
public class RepositoryManager {
/** The repo manager will be stored in context under this attribute name */
public static final String ATTRIBUTE_NAME = RepositoryManager.class.getName();
/** Our User Manager */
public OOOUserManager userRepository;
/** Our referral repository. */
public ReferralRepository referralRepository;
/** Our site identification repository. */
public SiteIdentifier siteRepository;
/** For logging tracking events */
public TrackingRepository trackingRepository;
/**
* Initialize a new set of repositories.
* @param config
*/
public RepositoryManager (Config config)
throws PersistenceException
{
// create a static connection provider
StaticConnectionProvider conn = new StaticConnectionProvider(config.getSubProperties("db"));
/* create the referral repository */
referralRepository = new ReferralRepository(conn);
/* create a repository of users */
userRepository = new OOOUserManager(config.getSubProperties("oooauth"), conn);
/* create a repository of sites */
siteRepository = new JDBCTableSiteIdentifier(conn);
/* create a repository for tracking */
trackingRepository = new TrackingRepository(conn);
}
}
@@ -0,0 +1,45 @@
//
// $Id$
package com.threerings.user;
import java.sql.Date;
import com.samskivert.util.StringUtil;
/**
* An object representation of the persistent reward info stored in the {@link RewardRepository}
* REWARD_INFO table.
*/
public class RewardInfo
{
/** Unique reward id. */
public int rewardId;
/** Reward description. */
public String description;
/** A coupon code required to activate this reward or null. */
public String couponCode;
/** Game specific reward data. */
public String data;
/** Date when the reward expires. */
public Date expiration;
/** Max userid that can redeem this reward. */
public int maxEligibleId;
/** Number of activated rewards. */
public int activations;
/** Number of claimed rewards. */
public int redemptions;
@Override // from Object
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
@@ -0,0 +1,41 @@
//
// $Id$
package com.threerings.user;
import com.samskivert.util.StringUtil;
/**
* An object representation of the persistent rewards stored in the {@link RewardRepository} REWARDS
* table.
*/
public class RewardRecord
{
/** RewardInfo reward id. */
public int rewardId;
/** Account name. */
public String account;
/** Unique identification value. */
public String redeemerIdent;
/**
* A generic parameter string that can tell the reward handler more specifically when and what
* item, etc. to hand out for this instance of the reward.
*/
public String param;
/**
* Constructs a blank RewardRecord for unserialization from the repository.
*/
public RewardRecord ()
{
}
@Override // from Object
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
@@ -0,0 +1,450 @@
//
// $Id$
package com.threerings.user;
import static com.threerings.user.Log.log;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.JORARepository;
import com.samskivert.jdbc.jora.Cursor;
import com.samskivert.jdbc.jora.FieldMask;
import com.samskivert.jdbc.jora.Table;
/**
* Maintains persistent reward information.
*/
public class RewardRepository extends JORARepository
{
/**
* Creates the repository with the specified connection provider.
*
* @exception PersistenceException thrown if an error occurs communicating with the underlying
* persistence facilities.
*/
public RewardRepository (ConnectionProvider provider)
throws PersistenceException
{
super(provider, OOOUserRepository.USER_REPOSITORY_IDENT);
}
/**
* Creates a new RewardInfo record in the database. <code>info</code> should have the
* <code>description</code>, <code>data</code> and <code>expiration</code> filled in.
*/
public void createReward (RewardInfo info)
throws PersistenceException
{
info.maxEligibleId = getMaxUserId();
store(_infoTable, info);
}
/**
* Immediately expires a reward by setting the expiration to the current date then making a
* call to <code>purgeExpiredRewards</code>.
*/
public void expireReward (int rewardId)
throws PersistenceException
{
RewardInfo info = new RewardInfo();
info.rewardId = rewardId;
info.expiration = new Date(System.currentTimeMillis());
updateField(_infoTable, info, "expiration");
purgeExpiredRewards();
}
/**
* Attempt to activate the reward for an account. If this account has not already activated
* the reward, a new RewardRecord will be created. Returns true if a new RewardRecord is
* created, false otherwise.
*/
public boolean activateReward (int rewardId, String account)
throws PersistenceException
{
return activateReward(rewardId, account, null);
}
public boolean activateReward (int rewardId, String account, String param)
throws PersistenceException
{
final RewardRecord rr = new RewardRecord();
rr.rewardId = rewardId;
rr.account = account;
rr.param = param == null ? "" : param;
// try to insert it; catching duplicate row exceptions and reporting that the reward is
// already activated
return executeUpdate(new Operation<Boolean>() {
public Boolean invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
try {
_rewardsTable.insert(conn, rr);
return true;
} catch (SQLException sqe) {
if (liaison.isDuplicateRowException(sqe)) {
return false;
} else {
throw sqe;
}
}
}
});
}
/**
* Activates monthly rewards of the specified ID for the account between start and end. If they
* already have monthly rewards for this ID, will add new ones only at appropriate monthly
* intervals from the existing ones.
*/
public boolean activateMonthlyRewards (final int rewardId, final String account,
final java.util.Date start, final java.util.Date end)
throws PersistenceException
{
// Setup an example to find relevant rewards.
final RewardRecord example = new RewardRecord();
example.rewardId = rewardId;
example.account = account;
final FieldMask mask = _rewardsTable.getFieldMask();
mask.setModified("rewardId");
mask.setModified("account");
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return executeUpdate(new Operation<Boolean>() {
public Boolean invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
// Find the most recent reward this account already has of this type.
java.util.Date latest = null;
Cursor<RewardRecord> matches =
_rewardsTable.queryByExample(conn, example, mask);
RewardRecord rec;
while ((rec = matches.next()) != null) {
if (rec.param == null) {
log.warning("Missing monthly reward param: ", "rewardId", rewardId,
"account", account);
continue;
}
try {
java.util.Date thisDate = dateFormat.parse(rec.param);
if (latest == null || thisDate.after(latest)) {
latest = thisDate;
}
} catch (ParseException pe) {
log.warning("Bogus reward param: ", "rewardId", rewardId,
"account", account, "param", rec.param);
continue;
}
}
// We use the maximum of our provided start time and a month-after-latest
// as the first time we'll input a reward. Note that this can result
// in no rewards being given.
Calendar startCal = Calendar.getInstance();
if (latest != null) {
startCal.setTime(latest);
startCal.add(Calendar.MONTH, 1);
if (startCal.getTime().before(start)) {
startCal.setTime(start);
}
} else {
startCal.setTime(start);
}
// Add in any appropriate rewards between the modified start and end.
while (startCal.getTime().before(end)) {
final RewardRecord rr = new RewardRecord();
rr.rewardId = rewardId;
rr.account = account;
rr.param = dateFormat.format(startCal.getTime());
_rewardsTable.insert(conn, rr);
startCal.add(Calendar.MONTH, 1);
}
return true;
}
});
}
/**
* Deactivates all rewards of the specified ID and account during the time window.
* Returns the number of rewards deleted.
*/
public int deactivateMonthlyRewards (final int rewardId, final String account,
final java.util.Date start, final java.util.Date end)
throws PersistenceException
{
return executeUpdate(new Operation<Integer>() {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
String delQuery = "delete from REWARD_RECORDS where REWARD_ID = ? and ACCOUNT = ?" +
" and PARAM >= ? and PARAM <= ? and REDEEMER_IDENT is null";
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String startStr = dateFormat.format(start);
String endStr = dateFormat.format(end);
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(delQuery);
stmt.setInt(1, rewardId);
stmt.setString(2, account);
stmt.setString(3, startStr);
stmt.setString(4, endStr);
return stmt.executeUpdate();
} finally {
JDBCUtil.close(stmt);
}
}
});
}
/**
* Returns all currently active rewards.
*/
public List<RewardInfo> loadActiveRewards ()
throws PersistenceException
{
return loadAll(_infoTable, "where EXPIRATION > NOW()");
}
/**
* Returns all rewards.
*/
public List<RewardInfo> loadRewards ()
throws PersistenceException
{
return loadAll(_infoTable, "order by REWARD_ID desc");
}
/**
* Returns all <code>RewardRecord</code>s that match the given account name and whose eligible
* date is in the past. The returned list will be sorted by <code>rewardId</code>.
*/
public List<RewardRecord> loadActivatedRewards (String account)
throws PersistenceException
{
String condition = "where ACCOUNT = " + JDBCUtil.escape(account) +
" order by REWARD_ID";
return loadAll(_rewardsTable, condition);
}
/**
* Returns all <code>RewardRecord</code>s that match either the account or redeemer identifier.
* The returned list will be sorted by <code>rewardId</code>.
*/
public List<RewardRecord> loadActivatedRewards (String account, String redeemerIdent)
throws PersistenceException
{
String condition = "where ACCOUNT = " + JDBCUtil.escape(account) +
" or REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) +
" order by REWARD_ID, PARAM";
return loadAll(_rewardsTable, condition);
}
/**
* Returns all <code>RewardRecord</code>s for a specified <code>rewardId</code> that match
* either the account or redeemer identifier.
*/
public List<RewardRecord> loadActivatedReward (
String account, String redeemerIdent, int rewardId)
throws PersistenceException
{
String condition = "where REWARD_ID = " + rewardId +
" and (ACCOUNT = " + JDBCUtil.escape(account) +
" or REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) + ")";
return loadAll(_rewardsTable, condition);
}
/**
* Returns all <code>RewardRecord</code>s for a specified <code>rewardId</code> and
* <code>param</code> that match either the account or redeemer identifier.
*/
public List<RewardRecord> loadActivatedReward (
String account, String redeemerIdent, int rewardId, String param)
throws PersistenceException
{
String paramStr = (param == null) ? "PARAM is NULL" : "PARAM = " + JDBCUtil.escape(param);
String condition = "where REWARD_ID = " + rewardId +
" and " + paramStr +
" and (ACCOUNT = " + JDBCUtil.escape(account) +
" or REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) + ")";
return loadAll(_rewardsTable, condition);
}
/**
* Updates the supplied RewardRecord with the indicated <code>redeemerIdent</code> and store it
* to the database.
*/
public boolean redeemReward (RewardRecord record, String redeemerIdent)
throws PersistenceException
{
String update = "update " + _rewardsTable.getName() + " set REDEEMER_IDENT = " +
JDBCUtil.escape(redeemerIdent) + " where REDEEMER_IDENT is NULL and REWARD_ID = " +
record.rewardId + " and ACCOUNT = " + JDBCUtil.escape(record.account) +
" and PARAM = " + JDBCUtil.escape(record.param);
return update(update) > 0;
}
/**
* Summarizes the reward data and purges expired rewards.
*/
public void purgeExpiredRewards ()
throws PersistenceException
{
executeUpdate(new Operation<Object> () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
summarizeAndUpdate(conn, "", "ACTIVATIONS");
summarizeAndUpdate(conn, "and REDEEMER_IDENT is NOT NULL", "REDEMPTIONS");
return null;
}
});
update("delete from REWARD_RECORDS using REWARD_RECORDS, REWARD_INFO " +
"where REWARD_INFO.REWARD_ID = REWARD_RECORDS.REWARD_ID " +
"and REWARD_INFO.EXPIRATION <= NOW()");
}
/**
* Summarizes the reward data and stores it in the reward info records.
*/
protected void summarizeAndUpdate (Connection conn, String condition, String column)
throws PersistenceException, SQLException
{
String activatedQuery = "select count(*), REWARD_INFO.REWARD_ID " +
"from REWARD_RECORDS, REWARD_INFO " +
"where REWARD_INFO.REWARD_ID = REWARD_RECORDS.REWARD_ID " + condition +
" group by REWARD_ID";
String activatedUpdate = "update REWARD_INFO set " + column + " = ? where REWARD_ID = ?";
PreparedStatement ustmt = null;
Statement stmt = null;
try {
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(activatedQuery);
ustmt = conn.prepareStatement(activatedUpdate);
while (rs.next()) {
ustmt.setInt(1, rs.getInt(1));
ustmt.setInt(2, rs.getInt(2));
ustmt.executeUpdate();
}
} finally {
JDBCUtil.close(ustmt);
JDBCUtil.close(stmt);
}
}
/**
* Returns the max userid currently in use.
*/
protected int getMaxUserId ()
throws PersistenceException
{
return execute(new Operation<Integer> () {
public Integer invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
String query = "select MAX(userId) from users";
Statement stmt = null;
int maxUserId = 0;
try {
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
if (rs.next()) {
maxUserId = rs.getInt(1);
} else {
log.warning("No users found!?");
}
} finally {
JDBCUtil.close(stmt);
}
return maxUserId;
}
});
}
@Override
protected void migrateSchema (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
String[] rewardInfoTable = {
"REWARD_ID INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY",
"DESCRIPTION VARCHAR(255) NOT NULL",
"COUPON_CODE VARCHAR(255)",
"DATA VARCHAR(255) NOT NULL",
"EXPIRATION DATE NOT NULL",
"MAX_ELIGIBLE_ID INTEGER NOT NULL",
"ACTIVATIONS INTEGER NOT NULL",
"REDEMPTIONS INTEGER NOT NULL"
};
JDBCUtil.createTableIfMissing(conn, "REWARD_INFO", rewardInfoTable, "");
// TEMP: (2006-12-22) add COUPON_CODE column
JDBCUtil.addColumn(conn, "REWARD_INFO", "COUPON_CODE", "VARCHAR(255)", "DESCRIPTION");
// END TEMP
String[] rewardsTable = {
"REWARD_ID INTEGER NOT NULL",
"ACCOUNT VARCHAR(255)",
"REDEEMER_IDENT VARCHAR(64)",
"PARAM VARCHAR(255)",
"PRIMARY KEY (REWARD_ID, ACCOUNT, PARAM)",
"INDEX (ACCOUNT)",
"INDEX (REDEEMER_IDENT)"
};
JDBCUtil.createTableIfMissing(conn, "REWARD_RECORDS", rewardsTable, "");
// Add a parameter column. This allows a particular reward to be granted more than once, and
// pass specific details to the reward handler.
if (!JDBCUtil.tableContainsColumn(conn, "REWARD_RECORDS", "PARAM")) {
// unfortunately, we need to recreate the primary key
JDBCUtil.dropPrimaryKey(conn, "REWARD_RECORDS");
// add generic parameter column, and include
JDBCUtil.addColumn(conn, "REWARD_RECORDS", "PARAM", "VARCHAR(255), " +
"ADD PRIMARY KEY (REWARD_ID, ACCOUNT, PARAM)", null);
}
}
@Override
protected void createTables ()
{
_infoTable = new Table<RewardInfo>(
RewardInfo.class, "REWARD_INFO", REWARD_INFO_PRIMARY_KEY, true);
_rewardsTable = new Table<RewardRecord>(
RewardRecord.class, "REWARD_RECORDS", REWARD_RECORDS_PRIMARY_KEYS, true);
}
/** The primary key for the reward info table. */
protected static final String REWARD_INFO_PRIMARY_KEY = "REWARD_ID";
/** The primary keys for the rewards table. */
protected static final String[] REWARD_RECORDS_PRIMARY_KEYS = {
"REWARD_ID", "ACCOUNT", "PARAM"
};
/** The table used to store reward information. */
protected Table<RewardInfo> _infoTable;
/** The table used to store activated rewards. */
protected Table<RewardRecord> _rewardsTable;
}
@@ -0,0 +1,24 @@
//
// $Id$
package com.threerings.user;
/**
* Represents a row in the TAINTED_IDENTS table.
*/
public class TaintedIdent
{
/** A 'unique' id for a specific machine we have seen. */
public String machIdent;
/** Blank constructor for the unserialization buisness. */
public TaintedIdent ()
{
}
@Override
public String toString ()
{
return machIdent;
}
}
@@ -0,0 +1,90 @@
//
// $Id$
package com.threerings.user;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.SimpleRepository;
/**
* Manages database access for doing our own quick and dirty tracking ala clientstep.
*/
public class TrackingRepository extends SimpleRepository
{
/** Name of the tracking table. */
public static final String TRACKING_TABLE = "TRACKING";
/**
* The database identifier used when establishing a database
* connection. This value being <code>trackingdb</code>.
*/
public static final String TRACKING_DB_IDENT = "trackingdb";
/**
* Creates the repository and prepares it for operation.
*
* @param provider the database connection provider.
*/
public TrackingRepository (ConnectionProvider provider)
throws PersistenceException
{
super(provider, TRACKING_DB_IDENT);
}
/**
* Records a tracking event to the database.
*/
public void addTrackingEvent (final String event, final String description,
final int accountId, final int siteId)
throws PersistenceException
{
executeUpdate(new Operation<Object>() {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement("insert into " + TRACKING_TABLE +
" (TIME, EVENT, DESCRIPTION, ACCOUNT_ID, SITE_ID)" +
" values (NOW(), ?, ?, ?, ?)");
stmt.setString(1, event);
stmt.setString(2, description);
stmt.setInt(3, accountId);
stmt.setInt(4, siteId);
JDBCUtil.checkedUpdate(stmt, 1);
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
@Override
protected void migrateSchema (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
String[] TrackingTable = {
"TIME DATETIME NOT NULL",
"EVENT VARCHAR(255)",
"DESCRIPTION VARCHAR(255)",
"ACCOUNT_ID INTEGER",
"SITE_ID INTEGER",
};
JDBCUtil.createTableIfMissing(conn, TRACKING_TABLE, TrackingTable, "");
if (!JDBCUtil.tableContainsColumn(conn, TRACKING_TABLE, "ACCOUNT_ID")) {
JDBCUtil.addColumn(conn, TRACKING_TABLE, "ACCOUNT_ID", "INTEGER", "DESCRIPTION");
JDBCUtil.addColumn(conn, TRACKING_TABLE, "SITE_ID", "INTEGER", "ACCOUNT_ID");
}
}
}
@@ -0,0 +1,241 @@
//
// $Id$
package com.threerings.user;
import javax.servlet.http.HttpServletRequest;
import com.samskivert.net.MailUtil;
import com.samskivert.servlet.util.DataValidationException;
import com.samskivert.servlet.util.ParameterUtil;
import com.samskivert.util.StringUtil;
/**
* Miscellaneous user data related routines.
*/
public class UserDataUtil
{
/** The minimum length of a valid password. */
public static final int MIN_PASSWORD_LENGTH = 4;
/** The minimum length of a valid username. */
public static final int MIN_USERNAME_LENGTH = 3;
/** The maximum length of a valid username. */
public static final int MAX_USERNAME_LENGTH = 12;
/**
* Returns the username in the supplied request, or throws an
* informative exception if the username is absent or invalid.
*/
public static String getUsername (HttpServletRequest req, String page)
throws DataValidationException
{
String username = ParameterUtil.requireParameter(
req, "username", page + ".error.missing_username");
username = username.trim();
int len = username.length();
if (len < MIN_USERNAME_LENGTH || len > MAX_USERNAME_LENGTH ||
!username.matches("[a-zA-Z0-9_]+")) {
throw new DataValidationException(page + ".error.invalid_username");
}
return username;
}
/**
* Returns the password in the supplied request, or throws an
* informative exception if the password is absent or invalid.
*/
public static String getPassword (
HttpServletRequest req, String page, String ptype)
throws DataValidationException
{
return getPassword(req, page, ptype, false);
}
/**
* Returns the password in the supplied request, or throws an
* informative exception if the password is absent or invalid.
*
* @optional If true, null may be returned and if the parameter is absent no exception will be
* thrown. If the parameter is present it is still checked for validity.
*/
public static String getPassword (
HttpServletRequest req, String page, String ptype, boolean optional)
throws DataValidationException
{
String password = optional ? ParameterUtil.getParameter(req, ptype, true) :
ParameterUtil.requireParameter(req, ptype, page + ".error.missing_" + ptype);
if (password != null) {
checkPassword(page, password, ptype);
}
return password;
}
/**
* Throws an informative exception if the password is absent or
* invalid.
*/
public static void checkPassword (
String page, String password, String ptype)
throws DataValidationException
{
int len = password.length();
if (len < MIN_PASSWORD_LENGTH) {
throw new DataValidationException(
page + ".error.invalid_" + ptype);
}
}
/**
* Returns the email in the supplied request, or throws an informative
* exception if the email is absent or invalid.
*/
public static String getEmail (
HttpServletRequest req, String page, boolean require)
throws DataValidationException
{
String email = ParameterUtil.getParameter(req, "email", true);
if (StringUtil.isBlank(email)) {
if (require) {
throw new DataValidationException(
page + ".error.missing_email");
}
return "";
}
email = email.trim();
if (email.length() > MAX_EMAIL_LENGTH ||
!MailUtil.isValidAddress(email)) {
throw new DataValidationException(page + ".error.invalid_email");
}
return email;
}
/**
* Returns the specified partial (first or last) name in the supplied
* request, or throws an informative exception if the name is absent
* or invalid.
*/
public static String getName (
HttpServletRequest req, String page, String pname)
throws DataValidationException
{
String name = ParameterUtil.requireParameter(
req, pname, page + ".error.missing_" + pname).trim();
int len = name.length();
if (len < MIN_NAME_LENGTH || len > MAX_NAME_LENGTH ||
!name.matches("[a-zA-Z]+")) {
throw new DataValidationException(
page + ".error.invalid_" + pname);
}
return name;
}
/**
* Returns the state in the supplied request, or throws an informative
* exception if the state is absent or invalid.
*/
public static String getState (HttpServletRequest req, String page)
throws DataValidationException
{
String state = ParameterUtil.getParameter(req, "state", false);
state = state.trim();
if (state.length() > MAX_STATE_LENGTH) {
throw new DataValidationException(
page + ".error.invalid_state");
}
return state;
}
/**
* Returns the country in the supplied request, or throws an
* informative exception if the country is absent or invalid.
*/
public static String getCountry (HttpServletRequest req, String page)
throws DataValidationException
{
String country = ParameterUtil.requireParameter(
req, "country", page + ".error.missing_country");
country = country.trim();
int len = country.length();
if (len < MIN_COUNTRY_LENGTH || len > MAX_COUNTRY_LENGTH) {
throw new DataValidationException(
page + ".error.invalid_country");
}
return country;
}
/**
* Returns the missive in the supplied request, or throws an
* informative exception if the missive is absent or invalid.
*/
public static String getMissive (HttpServletRequest req, String page)
throws DataValidationException
{
String missive = ParameterUtil.getParameter(req, "missive", false);
missive = missive.trim();
if (missive.length() > MAX_MISSIVE_LENGTH) {
throw new DataValidationException(
page + ".error.invalid_missive");
}
return missive;
}
/**
* Returns 1 if the named parameter is present in the supplied
* request, 0 if not.
*/
public static byte isChecked (HttpServletRequest req, String name)
throws DataValidationException
{
String value = ParameterUtil.getParameter(req, name, true);
return (byte)((value != null) ? 1 : 0);
}
/**
* Returns the value of the given select menu, or throws an error if
* the value is not defined or 0 (denoting the default "[Select One]"
* selection.)
*/
public static byte requireSelectItem (
HttpServletRequest req, String name, String error)
throws DataValidationException
{
byte value = (byte)ParameterUtil.getIntParameter(req, name, 0, error);
if (value == 0) {
throw new DataValidationException(error);
}
return value;
}
/** The maximum length of a valid email address. */
protected static final int MAX_EMAIL_LENGTH = 128;
/** The minimum length of a valid name in characters. */
protected static final int MIN_NAME_LENGTH = 1;
/** The maximum length of a valid name in characters. */
protected static final int MAX_NAME_LENGTH = 63;
/** The minimum birth year a user can enter. God help us if we have
* many centegenarians. */
protected static final int MIN_BIRTHYEAR = 1900;
/** The minimum birth date a user can enter. */
protected static final int MIN_BIRTHDATE = 1;
/** The maximum birth date a user can enter. */
protected static final int MAX_BIRTHDATE = 31;
/** The minimum length of a valid country in characters. */
protected static final int MIN_COUNTRY_LENGTH = 2;
/** The maximum length of a valid state in characters. */
protected static final int MAX_STATE_LENGTH = 64;
/** The maximum length of a valid country in characters. */
protected static final int MAX_COUNTRY_LENGTH = 64;
/** The maximum length of a valid missive in characters. */
protected static final int MAX_MISSIVE_LENGTH = 32768;
}
@@ -0,0 +1,28 @@
//
// $Id$
package com.threerings.user;
/**
* Represents a row in the USER_IDENT table mapping a user to all their
* machine identifieres.
*/
public class UserIdent
{
/** The id of the user in question. */
public int userId;
/** A 'unique' id for a specific machine we have seen the user come from. */
public String machIdent;
/** Construct a blank record for unserialization purposes. */
public UserIdent ()
{
}
@Override
public String toString ()
{
return "[id=" + userId + ", ident=" + machIdent + "]";
}
}
@@ -0,0 +1,21 @@
//
// $Id$
package com.threerings.user;
import java.sql.Date;
/**
* Maintains information on users that have not yet validated their
* account.
*/
public class ValidateRecord
{
public String secret;
public int userId;
public boolean persist;
public Date inserted;
}
@@ -0,0 +1,66 @@
//
// $Id$
package com.threerings.user;
import java.sql.Date;
import com.samskivert.util.StringUtil;
/**
* A record detailing a user's non-critical responses to the Yohoho beta
* registration.
*/
public class YoBetaInfo
{
/** The user id of the user. */
public int userId;
/** The user's birthday. */
public Date birthday;
/** The user's state or province. */
public String state;
/** The user's country. */
public String country;
/** The user's connection speed. */
public byte connection;
/** The user's computer operating system. */
public byte os;
/** The user's computer CPU speed. */
public byte cpu;
/** The user's computer memory size. */
public byte memory;
/** Whether the user has played an MMORPG. */
public byte playMmorpg;
/** Whether the user has played a beta MMORPG. */
public byte playBetaMmorpg;
/** Whether the user has played a text MUD. */
public byte playMud;
/** Whether the user plays puzzle games. */
public byte playPuzzle;
/** The user's testing availability this summer. */
public byte summer;
/** The source from which the user heard about Yohoho. */
public byte source;
/** The user's personal missive to us. */
public String missive;
@Override
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
@@ -0,0 +1,139 @@
//
// $Id$
package com.threerings.user;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.JORARepository;
import com.samskivert.jdbc.jora.FieldMask;
import com.samskivert.jdbc.jora.Table;
import static com.threerings.user.Log.log;
/**
* Provides facilities for saving a user's Yohoho beta information to the
* persistent beta info table in the database.
*/
public class YoBetaRepository extends JORARepository
{
/**
* The database identifier used when establishing a database
* connection. This value being <code>yobetadb</code>.
*/
public static final String YOBETA_DB_IDENT = "yobetadb";
/**
* Creates the yo beta repository.
*
* @param provider the database connection provider.
*/
public YoBetaRepository (ConnectionProvider provider)
throws PersistenceException
{
super(provider, YOBETA_DB_IDENT);
// figure out whether or not the YOBETA table is in use on this system
execute(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_active = JDBCUtil.tableExists(conn, YOBETA_TABLE_NAME);
if (!_active) {
log.info("No beta repository table. Disabling.");
}
return null;
}
});
}
/**
* Inserts the supplied yo beta info for the specified user into the
* database.
*/
public void insertYoBetaInfo (final YoBetaInfo ybi)
throws PersistenceException
{
if (!_active) {
return;
}
insert(_table, ybi);
}
/**
* Loads and returns the yo beta info for the specified user id from
* the database, or <code>null</code> if no beta info for the user
* exists.
*/
public YoBetaInfo loadYoBetaInfo (final int userId)
throws PersistenceException
{
if (!_active) {
return null;
}
YoBetaInfo proto = new YoBetaInfo();
proto.userId = userId;
FieldMask mask = _table.getFieldMask();
mask.setModified("userId");
return loadByExample(_table, proto, mask);
}
/**
* Deletes the yo beta info for the specified user id from the
* database.
*/
public void deleteYoBetaInfo (final int userId)
throws PersistenceException
{
if (!_active) {
return;
}
executeUpdate(new Operation<Void>() {
public Void invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException
{
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(
"delete from " + YOBETA_TABLE_NAME + " " +
"where USER_ID = ?");
stmt.setInt(1, userId);
stmt.executeUpdate();
} finally {
JDBCUtil.close(stmt);
}
return null;
}
});
}
@Override
protected void createTables ()
{
_table = new Table<YoBetaInfo>(
YoBetaInfo.class, YOBETA_TABLE_NAME,
YOBETA_TABLE_PRIMARY_KEY, true);
}
/** A wrapper that provides access to the yo beta table. */
protected Table<YoBetaInfo> _table;
/** Whether or not we are actually in use. If our table doesn't exist,
* we quietly disable ourselves. */
protected boolean _active;
/** The name of the yobeta table. */
protected static final String YOBETA_TABLE_NAME = "YOBETA";
/** The primary key for the yobeta table. */
protected static final String YOBETA_TABLE_PRIMARY_KEY = "USER_ID";
}
@@ -0,0 +1,97 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Timestamp;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.GeneratedValue;
import com.samskivert.depot.annotation.GenerationType;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
import com.threerings.user.AccountAction;
/**
* Emulates {@link AccountAction} for the Depot.
*/
@Entity(name="ACCOUNT_ACTIONS")
public class AccountActionRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<AccountActionRecord> _R = AccountActionRecord.class;
public static final ColumnExp<Integer> ACTION_ID = colexp(_R, "actionId");
public static final ColumnExp<String> ACCOUNT_NAME = colexp(_R, "accountName");
public static final ColumnExp<Integer> ACTION = colexp(_R, "action");
public static final ColumnExp<String> DATA = colexp(_R, "data");
public static final ColumnExp<Timestamp> ENTERED = colexp(_R, "entered");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The action id. */
@Id @Column(name="ACTION_ID") @GeneratedValue(strategy=GenerationType.AUTO)
public int actionId;
/** The account name. */
@Column(name="ACCOUNT_NAME")
public String accountName;
/** The action that took place. */
@Column(name="ACTION")
public int action;
/** Data that is interpreted depending on the action type. */
@Column(name="DATA", length=65536, nullable=true)
public String data;
/** When the action took place. */
@Column(name="ENTERED")
public Timestamp entered;
/**
* Creates an AccountActionRecord from a AccountAction.
*/
public static AccountActionRecord fromAccountAction (AccountAction aa)
{
AccountActionRecord record = new AccountActionRecord();
record.actionId = aa.actionId;
record.accountName = aa.accountName;
record.action = aa.action;
record.data = aa.data;
record.entered = aa.entered;
return record;
}
/**
* Returns an AccountAction version of this record.
*/
public AccountAction toAccountAction ()
{
AccountAction aa = new AccountAction();
aa.actionId = actionId;
aa.accountName = accountName;
aa.action = action;
aa.data = data;
aa.entered = entered;
return aa;
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link AccountActionRecord}
* with the supplied key values.
*/
public static Key<AccountActionRecord> getKey (int actionId)
{
return newKey(_R, actionId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(ACTION_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,274 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.Ops;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.clause.Where;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.util.StringUtil;
import com.threerings.user.AccountAction;
import static com.threerings.user.Log.log;
/**
* Provides access to the account actions records.
*/
@Singleton
public class AccountActionRepository extends DepotRepository
{
/**
* The database identifier used when establishing a database connection.
* This value being <code>actiondb</code>.
*/
public static final String ACTION_DB_IDENT = "actiondb";
@Inject public AccountActionRepository (PersistenceContext ctx)
{
super(ctx);
}
public AccountActionRepository (ConnectionProvider conprov)
{
super(new PersistenceContext(ACTION_DB_IDENT, conprov, null));
}
/**
* Return the list of actions that have not yet been processed by the specified server.
* Returns all actions if no server name is specified.
*
* <em>Note:</em> this method has two side effects: the first time it is called, it will
* register this server as a action participant (assuming server is non-null) and
* subsequently, it will call {@link #pruneActions} if it has not done so within the last
* hour.
*/
public List<AccountAction> getActions (String server)
{
return getActions(server, Integer.MAX_VALUE);
}
/**
* Return the list of actions that have not yet been processed by the specified server.
* Returns all actions if no server name is specified.
*
* @param maxActions the maximum number of actions to return.
*
* <em>Note:</em> this method has two side effects: the first time it is called, it will
* register this server as a action participant (assuming server is non-null) and
* subsequently, it will call {@link #pruneActions} if it has not done so within the last
* hour.
*/
public List<AccountAction> getActions (String server, int maxActions)
{
// TODO: Figure out how to do this in one query
List<Integer> processedIds = from(ProcessedActionRecord.class)
.where(ProcessedActionRecord.SERVER.eq(server))
.select(ProcessedActionRecord.ACTION_ID);
List<AccountActionRecord> actions = findAll(AccountActionRecord.class,
new Where(Ops.not(AccountActionRecord.ACTION_ID.in(processedIds))));
List<AccountAction> list = Lists.newArrayListWithCapacity(actions.size());
for (AccountActionRecord record : actions) {
list.add(record.toAccountAction());
}
if (StringUtil.isBlank(server)) {
return list;
}
// if this is the first time this method is called, register ourselves
// with the action system
long now = System.currentTimeMillis();
if (_lastActionPrune == 0L) {
_lastActionPrune = now;
registerActionServer(server);
} else if (now - _lastActionPrune > ACTION_PRUNE_INTERVAL) {
_lastActionPrune = now;
pruneActions();
}
return list;
}
/**
* Adds a new action to the repository.
*/
public void addAction (String accountName, int action)
{
addAction(accountName, null, action, "");
}
/**
* Adds a new action to the repository.
*/
public void addAction (String accountName, String data, int action)
{
addAction(accountName, data, action, "");
}
/**
* Adds a new action to the repository.
*/
public void addAction (String accountName, int action, String server)
{
addAction(accountName, null, action, server);
}
/**
* Adds a new action to the repository, with the specified server being marked as already
* having processed the action.
*/
public void addAction (String accountName, String data, int action, final String server)
{
AccountActionRecord record = new AccountActionRecord();
record.accountName = accountName;
record.data = data;
record.action = action;
record.entered = new Timestamp(System.currentTimeMillis());
insert(record);
// note that it was processed by this server if appropriate
if (!StringUtil.isBlank(server)) {
noteProcessed(record.actionId, server);
}
}
/**
* Updates the single specified action to reflect that the specified server has processed it.
*/
public void updateAction (AccountAction action, String server)
{
updateActions(Collections.singletonList(action), server);
}
/**
* Batch update a list of actions.
*/
public void updateActions (List<AccountAction> actions, String server)
{
for (int ii = 0, ll = actions.size(); ii < ll; ii++) {
noteProcessed(actions.get(ii).actionId, server);
}
}
/**
* Deletes an action from the repository.
*/
public void deleteAction (AccountAction action)
{
delete(AccountActionRecord.fromAccountAction(action));
deleteAll(ProcessedActionRecord.class,
new Where(ProcessedActionRecord.ACTION_ID.eq(action.actionId)));
}
/**
* Notes that the specified server has processed the specified action.
*/
public void noteProcessed (int actionId, String server)
{
ProcessedActionRecord record = new ProcessedActionRecord();
record.actionId = actionId;
record.server = server;
insert(record);
}
/**
* Prunes actions that have been processed by all registered servers to keep the actions table
* small. This method need not be called by hand as it is called automatically by
* {@link #getActions(String,int)}, but no more frequently than once an hour.
*/
public void pruneActions ()
{
Set<String> servers = loadActionServers();
Map<Integer, Set<String>> procmap = Maps.newHashMap();
Set<Integer> actids = Sets.newHashSet();
// determine which servers have processed which actions
for (ProcessedActionRecord record : findAll(ProcessedActionRecord.class)) {
Set<String> procset = procmap.get(record.actionId);
if (procset == null) {
procmap.put(record.actionId, procset = Sets.newHashSet());
}
procset.add(record.server);
}
// determine which of those have been fully processed
for (Map.Entry<Integer, Set<String>> entry : procmap.entrySet()) {
if (entry.getValue().containsAll(servers)) {
actids.add(entry.getKey());
}
}
// now wipe out the actions (and processed entries for all actions that have been fully
// processed
if (actids.size() > 0) {
deleteAll(AccountActionRecord.class,
new Where(AccountActionRecord.ACTION_ID.in(actids)));
deleteAll(ProcessedActionRecord.class,
new Where(ProcessedActionRecord.ACTION_ID.in(actids)));
}
}
/**
* Registers a server with the action system. This method is called automatically the first
* time {@link #getActions(String)} is called.
*/
protected void registerActionServer (String server)
{
// see if we're already registered
Set<String> set = loadActionServers();
if (set.contains(server)) {
return;
}
ActionServerRecord record = new ActionServerRecord();
record.server = server;
insert(record);
log.info("Registered action server", "server", server);
}
/**
* Loads up the set of action servers.
*/
protected Set<String> loadActionServers ()
{
Set<String> set = Sets.newHashSet();
for (ActionServerRecord record : findAll(ActionServerRecord.class)) {
set.add(record.server);
}
return set;
}
@Override
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
{
classes.add(AccountActionRecord.class);
classes.add(ActionServerRecord.class);
classes.add(ProcessedActionRecord.class);
}
/** Used by {@link #getActions(String)}. */
protected long _lastActionPrune;
/** We automatically prune the actions table once an hour. */
protected static final long ACTION_PRUNE_INTERVAL = 60 * 60 * 1000L;
}
@@ -0,0 +1,43 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
/**
* A server that processes actions.
*/
@Entity(name="ACTION_SERVERS")
public class ActionServerRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<ActionServerRecord> _R = ActionServerRecord.class;
public static final ColumnExp<String> SERVER = colexp(_R, "server");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The server name. */
@Id @Column(name="SERVER")
public String server;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link ActionServerRecord}
* with the supplied key values.
*/
public static Key<ActionServerRecord> getKey (String server)
{
return newKey(_R, server);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(SERVER); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,30 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Computed;
import com.samskivert.depot.expression.ColumnExp;
/**
* The number of users referred by an affiliate created on a particular day.
*/
@Computed(shadowOf=HistoricalUserRecord.class)
public class AffiliateCountRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<AffiliateCountRecord> _R = AffiliateCountRecord.class;
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
public static final ColumnExp<Integer> COUNT = colexp(_R, "count");
// AUTO-GENERATED: FIELDS END
/** The day that these users were created */
public Date created;
/** The number of users. */
@Computed(fieldDefinition="count(*)")
public int count;
}
@@ -0,0 +1,716 @@
//
// $Id$
package com.threerings.user.depot;
import static com.threerings.user.Log.log;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.samskivert.servlet.SiteIdentifier;
import com.samskivert.servlet.util.CookieUtil;
import com.samskivert.servlet.util.ParameterUtil;
import com.samskivert.util.StringUtil;
import com.threerings.user.AffiliateUtils;
import com.threerings.user.OOOUser;
public abstract class AffiliateInfo
{
/** The name of the session attribute used to hold the affiliate suffix */
public static final String AFFILIATE_SUFFIX_ATTRIBUTE =
AffiliateInfo.class.getName() + ".AffiliateSuffix";
/**
* Class determining the behavior if the affiliate is 'new referral' i.e. the URL
* has a 'from' and optionally, a 'tag' parameter.
*/
protected static class NewReferral extends AffiliateInfo
{
protected final String fromParameter;
protected final int siteId;
protected final int tagId;
public NewReferral (String fromParameter, int siteId, int tagId)
{
this.fromParameter = fromParameter;
this.siteId = siteId;
this.tagId = tagId;
}
@Override
public void addToContext (HttpServletRequest req)
{
if (isPersonalSuffix(fromParameter)) {
setAffiliateSuffix(req, fromParameter, tagId);
} else {
setAffiliateSuffix(req, "" + siteId, tagId);
}
}
@Override
public String getAffiliateName ()
{
return fromParameter;
}
@Override
public boolean hasName ()
{
return !isPersonalSuffix(fromParameter);
}
@Override
public boolean hasSiteId ()
{
return true;
}
@Override
public int getSiteId ()
{
return siteId;
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "" + tagId);
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
if (isPersonalSuffix(fromParameter)) {
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, fromParameter);
} else {
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + siteId);
}
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// TODO probably need nothing
giveCookie(req, rsp, SITE_REFER_COOKIE, fromParameter);
}
@Override
public int getTagId ()
{
return tagId;
}
}
/**
* Class determining the behavior if the wasn't referred to us in this particular
* request, but they were referred to us before, and we've recorded that in cookies.
*/
protected static class CurrentCookies extends AffiliateInfo
{
protected final String siteIdCookieValue;
protected final int tagIdCookieValue;
public CurrentCookies (String siteIdCookieValue, int tagIdCookieValue)
{
this.siteIdCookieValue = siteIdCookieValue;
this.tagIdCookieValue = tagIdCookieValue;
}
@Override
public void addToContext (HttpServletRequest req)
{
setAffiliateSuffix(req, siteIdCookieValue, tagIdCookieValue);
}
@Override
public String getAffiliateName ()
{
return null;
}
@Override
public boolean hasName ()
{
return false;
}
@Override
public boolean hasSiteId ()
{
return true;
}
@Override
public int getSiteId ()
{
try {
return Integer.parseInt(siteIdCookieValue);
} catch (NumberFormatException nfe) {
return 0;
}
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "" + tagIdCookieValue);
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, siteIdCookieValue);
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, SITE_REFER_COOKIE, "");
}
@Override
public int getTagId ()
{
return tagIdCookieValue;
}
}
/**
* Class determining the behavior if the user has an 'old-style' cookie indicating
* that they came to us as a referral.
*/
protected static class LegacyCookies extends AffiliateInfo
{
protected final String siteRefer;
protected final int siteId;
public LegacyCookies (int siteId, String siteReferCookie)
{
this.siteRefer = siteReferCookie;
this.siteId = siteId;
}
@Override
public void addToContext (HttpServletRequest req)
{
if (isPersonalSuffix(siteRefer)) {
setAffiliateSuffix(req, siteRefer, AffiliateTagRecord.NO_TAG);
} else {
setAffiliateSuffix(req, "" + siteId, AffiliateTagRecord.NO_TAG);
}
}
@Override
public String getAffiliateName ()
{
if (!isPersonalSuffix(siteRefer)) {
return siteRefer;
} else {
return "";
}
}
@Override
public boolean hasName ()
{
return !isPersonalSuffix(siteRefer);
}
@Override
public boolean hasSiteId ()
{
return true;
}
@Override
public int getSiteId ()
{
return siteId;
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// TODO: should we actually be removing the cookie here
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "");
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
if (isPersonalSuffix(siteRefer)) {
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, siteRefer);
} else {
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + siteId);
}
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// TODO: should be be removing this cookie?
giveCookie(req, rsp, SITE_REFER_COOKIE, siteRefer);
}
@Override
public int getTagId ()
{
return AffiliateTagRecord.NO_TAG;
}
}
/**
* Define the behavior if we don't have any affiliate information but we want to track the user
* in a separate category from the 'default site'.
*/
protected static class AlternateDefault extends AffiliateInfo
{
protected final int site;
protected AlternateDefault (int site)
{
this.site = site;
}
@Override
public boolean isDefaultAffiliate ()
{
return true;
}
@Override
public void addToContext (HttpServletRequest req)
{
setAffiliateSuffix(req, "" + site, AffiliateTagRecord.NO_TAG);
}
@Override
public String getAffiliateName ()
{
return "";
}
@Override
public int getTagId ()
{
return AffiliateTagRecord.NO_TAG;
}
@Override
public boolean hasName ()
{
return false;
}
@Override
public boolean hasSiteId ()
{
return true;
}
@Override
public int getSiteId ()
{
return site;
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// Don't issue a cookie;
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + site);
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// Don't issue a cookie;
}
}
/**
* Determines the behavior if a tag cookie exists, but not an affiliate (site id) cookie.
* "0" will be supplied as the affiliate to the 'affsuf' context attribute.
*/
protected static class TagOnly extends AffiliateInfo
{
protected final int tagId;
public TagOnly (int tagId)
{
this.tagId = tagId;
}
@Override
public void addToContext (HttpServletRequest req)
{
setAffiliateSuffix(req, "0", tagId);
}
@Override
public String getAffiliateName ()
{
return "";
}
@Override
public boolean hasName ()
{
return false;
}
@Override
public boolean hasSiteId ()
{
return false;
}
@Override
public int getSiteId ()
{
return 0;
}
@Override
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
{
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, String.valueOf(tagId));
}
@Override
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// do not issue a cookie
}
@Override
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
{
// do not issue a cookie
}
@Override
public int getTagId ()
{
return tagId;
}
}
/**
* Find out whether we know the name of the affiliate.
*/
public abstract boolean hasName ();
/**
* Get the name of the affiliate.
*
* @return - The name as a string.
*/
public abstract String getAffiliateName ();
/**
* Find out whether we know the site id of the affiliate
*/
public abstract boolean hasSiteId ();
/**
* Get the site id of the affiliate.
*
* @return - the site id of the affiliate
*/
public abstract int getSiteId ();
/**
* Get the id for the affiliate tag.
*/
public abstract int getTagId ();
/**
* Compute the appropriate affiliate 'affsuf' and add it to the session along with the
* 'affid' used by some of the web templates. Now with less Velocity.
*/
public abstract void addToContext (HttpServletRequest req);
/**
* Issue the site_id cookie to the browser (non-velocity).
*/
public abstract void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp);
/**
* Issue the site_refer cookie to the browser.
*/
public abstract void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp);
/**
* Issue the affiliate_tag cookie to the browser (non-velocity).
*/
public abstract void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp);
/**
* Normally returns false, but can be overridden to indicate that we've been given
* a cookie with a default affiliate passed in to us. "Default" isn't quite the right word
* since this is really used for letting pages hardwire their default affiliate to be something
* other than the default, but everything else uses this word, and I don't have a better idea
* offhand.
*/
public boolean isDefaultAffiliate ()
{
return false;
}
/**
* Make an affiliate info object from the cookies and parameters in the request.
*/
public static AffiliateInfo getAffiliateInfo (
DepotUserManager usermgr, ReferralRepository repository, SiteIdentifier identifier,
HttpServletRequest req, boolean parseFromParameter)
{
int siteId = identifier.identifySite(req);
return getAffiliateInfo(usermgr, repository, identifier, req, siteId, parseFromParameter);
}
/**
* Make an affiliate info object from the cookies and parameters in the request.
*
* @param alternateDefault - override default affiliate (hard-coded into affiliate pages)
* @param parseFromParameter - Should we look at the request parameter "from"?
* @return - The affiliate info or null if there wasn't enough information in the request to
* construct it.
*/
public static AffiliateInfo getAffiliateInfo (
DepotUserManager usermgr, ReferralRepository repository, SiteIdentifier identifier,
HttpServletRequest req, int alternateDefault, boolean parseFromParameter)
{
// if they're already a user with us, bail
if (null != usermgr.loadUser(req)) {
return null;
}
String fromParameter = parseFromParameter ? parseFromParameter(req) : "";
// is this a new referral - i.e. this request was referred from a referral link?
if (!StringUtil.isBlank(fromParameter)) {
String tagParameter = readAffiliateTag(req);
int siteId = parseReferrer(repository, identifier, fromParameter);
if (siteId == 0) {
log.warning("User referred by bogus referrer [referrer=" + fromParameter + "].");
return null;
}
if (!StringUtil.isBlank(tagParameter) && !isPersonalSuffix(fromParameter)) {
int tagId = usermgr.getAffiliateTagId(tagParameter);
return new NewReferral(fromParameter, siteId, tagId);
} else {
return new NewReferral(fromParameter, siteId, AffiliateTagRecord.NO_TAG);
}
}
// do we have modern cookies for this referral?
String siteIdCookieValue = readSiteIdCookie(req);
if (!StringUtil.isBlank(siteIdCookieValue)) {
int tagIdCookieValue = readTagId(req);
return new CurrentCookies(siteIdCookieValue, tagIdCookieValue);
}
// do we have old fashioned cookies for this referral?
String siteReferCookie = CookieUtil.getCookieValue(req, SITE_REFER_COOKIE);
if (!StringUtil.isBlank(siteReferCookie)) {
int siteId = parseReferrer(repository, identifier, siteReferCookie);
return new LegacyCookies(siteId, siteReferCookie);
}
// we have no useful affiliate information. If we have been provided with an alternative
// default site, then use that as the site-id.
if (alternateDefault != identifier.identifySite(req)) {
return new AlternateDefault(alternateDefault);
}
// we have a tag cookie but no affiliate cookie
int tagIdCookieValue = readTagId(req);
if (tagIdCookieValue != AffiliateTagRecord.NO_TAG) {
return new TagOnly(tagIdCookieValue);
}
// we couldn't figure out anything useful to do with whatever affiliate related info
// we had, so we return null and the caller can do nothing.
return null;
}
/**
* @param repository For verifying referrer ids against the database
* @param identifier Velocity site repository
* @param referrer Referral request string, may be a personal referrer of
* the format r[0-9]+ or user-[0-9]+, or a site referrer of format [0-9]+
* @return the referrer siteId, which may be a positive or negative number,
* or 0 if no match.
*/
public static int parseReferrer (ReferralRepository repository, SiteIdentifier identifier,
String referrer)
{
// blank referrer string
if (StringUtil.isBlank(referrer)) {
return 0;
}
// handle new-style personal referrals
if (referrer.matches("r[0-9]+")) {
int refId = 0;
try {
refId = Integer.parseInt(referrer.substring(1));
} catch (NumberFormatException nfe) {
log.warning("Bogus user referral: " + referrer);
return 0;
}
// look up the record in the referrer repository
ReferralRecord rec = repository.lookupReferral(refId);
return (rec == null) ? OOOUser.DEFAULT_SITE_ID : (-1 * rec.referrerId);
}
// handle old-style personal referrals
if (referrer.startsWith("user-")) {
try {
return -1 * Integer.parseInt(referrer.substring(5));
} catch (NumberFormatException nfe) {
log.warning("Bogus user referral: " + referrer);
return 0;
}
}
// check referrer string against site repository
int site = identifier.getSiteId(referrer);
if (site != -1) {
return site;
}
log.warning("Bogus site specified in referrer " + "[value=" + referrer + "].");
return 0;
}
/**
* Parse the contents of the 'from' parameter.
*/
public static String parseFromParameter (HttpServletRequest req)
{
String value = ParameterUtil.getParameter(req, "from", true);
if (value != null) {
// strip the crap some affiliates append to their ID
Matcher am = _affregex.matcher(value);
if (am.find()) {
value = am.group();
}
}
return value;
}
/**
* Find out whether the affiliate name is actually a personal referral as
* opposed to a site referral.
*
* @return - True if the affiliate name is a personal referral. False if
* it's a site or unknown.
*/
protected static boolean isPersonalSuffix (String name)
{
return name != null && name.matches("r[0-9]+");
}
/**
* Get the value stored in the site id cookie.
*/
protected static String readSiteIdCookie (HttpServletRequest req)
{
return CookieUtil.getCookieValue(req, AffiliateUtils.AFFILIATE_ID_COOKIE);
}
/**
* Calculate and add the affiliate id and affiliate suffix to the session. No longer
* added to the Velocity context; this is velocity-independant.
* @param req
* @param referrer
* @param tagId
*/
protected void setAffiliateSuffix (HttpServletRequest req, String referrer, int tagId)
{
if (tagId != AffiliateTagRecord.NO_TAG) {
req.getSession().setAttribute(AFFILIATE_SUFFIX_ATTRIBUTE, "-" + referrer + "-" + tagId);
} else {
req.getSession().setAttribute(AFFILIATE_SUFFIX_ATTRIBUTE, "-" + referrer);
}
req.getSession().setAttribute(AffiliateUtils.AFFILIATE_ID_ATTRIBUTE, referrer);
}
/**
* Add a cookie to the response (velocity independant).
* @param req servlet request object
* @param rsp sevlet response object
* @param name cookie identifier
* @param value value for the cookie
*/
protected static void giveCookie (HttpServletRequest req, HttpServletResponse rsp,
String name, String value)
{
Cookie cookie = new Cookie(name, value);
// cookie expires in one month
cookie.setMaxAge(30 * 24 * 60 * 60);
// set the path and widened domain (eg www.domain.com -> .domain.com) of the cookie
cookie.setPath("/");
CookieUtil.widenDomain(req, cookie);
rsp.addCookie(cookie);
}
/**
* Get the affiliate tag from the tag parameter in the request.
*
* @param req - The request.
* @return - The tag string.
*/
protected static String readAffiliateTag (HttpServletRequest req)
{
return req.getParameter("tag");
}
protected static int readTagId (HttpServletRequest req)
{
// if they have an affiliate tag cookie, get it.
String cookie = CookieUtil.getCookieValue(req, AffiliateUtils.AFFILIATE_TAG_COOKIE);
if (cookie != null) {
try {
return Integer.parseInt(cookie);
} catch (NumberFormatException e) {
return AffiliateTagRecord.NO_TAG;
}
} else {
return AffiliateTagRecord.NO_TAG;
}
}
/** A regexp that matches just the proper parts of an affiliate id. */
protected static Pattern _affregex = Pattern.compile(OOOUser.SITE_STRING_REGEX);
/**
* The name of the legacy cookie used to store the affiliate name passed by
* them to us with their original request.
*/
protected static final String SITE_REFER_COOKIE = "site_refer";
}
@@ -0,0 +1,53 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.GeneratedValue;
import com.samskivert.depot.annotation.GenerationType;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
/**
* Maintains a mapping from an arbitrary text tag to an integer identifier.
*/
@Entity(name="afftags")
public class AffiliateTagRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<AffiliateTagRecord> _R = AffiliateTagRecord.class;
public static final ColumnExp<Integer> TAG_ID = colexp(_R, "tagId");
public static final ColumnExp<String> TAG = colexp(_R, "tag");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** Value indicating that the affiliate did not provide a tag **/
public static final int NO_TAG = 0;
/** The automatically generated tag id. */
@Id @GeneratedValue(strategy=GenerationType.AUTO)
public int tagId;
/** The arbitrary text tag. */
@Column(unique=true)
public String tag;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link AffiliateTagRecord}
* with the supplied key values.
*/
public static Key<AffiliateTagRecord> getKey (int tagId)
{
return newKey(_R, tagId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(TAG_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,25 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Computed;
import com.samskivert.depot.expression.ColumnExp;
@Computed(shadowOf=HistoricalUserRecord.class)
public class AffiliateUserCountRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<AffiliateUserCountRecord> _R = AffiliateUserCountRecord.class;
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
public static final ColumnExp<Integer> COUNT = colexp(_R, "count");
// AUTO-GENERATED: FIELDS END
/** The affiliate site ID. */
public int siteId;
/** The number of users. */
@Computed(fieldDefinition="count(*)")
public int count;
}
@@ -0,0 +1,60 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
/**
* Represents a row in the BANNED_IDENTS table, Depot version.
*/
@Entity(name="BANNED_IDENTS")
public class BannedIdentRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<BannedIdentRecord> _R = BannedIdentRecord.class;
public static final ColumnExp<String> MACH_IDENT = colexp(_R, "machIdent");
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** a 'unique' id for the specific machine we have seen. */
@Id @Column(name="MACH_IDENT")
public String machIdent;
/** The site if which this machine is banned from. */
@Id @Column(name="SITE_ID")
public int siteId;
/** Blank constructor for the unserialization business. */
public BannedIdentRecord ()
{
}
/** A constructor that populates this record. */
public BannedIdentRecord (String machIdent, int siteId)
{
this.machIdent = machIdent;
this.siteId = siteId;
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link BannedIdentRecord}
* with the supplied key values.
*/
public static Key<BannedIdentRecord> getKey (String machIdent, int siteId)
{
return newKey(_R, machIdent, siteId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(MACH_IDENT, SITE_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,99 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Timestamp;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.util.StringUtil;
import com.threerings.user.OOOUser;
/**
* Emulates {@link ConversionRecord} for the Depot.
*/
@Entity(name="CONVERSION")
public class ConversionRecord extends PersistentRecord
{
/** Indicates that an account was created. */
public static final int CREATED = 1;
/** Indicates that an account that was not currently paying us started
* to. This won't be true before 2005/03/01 where we tried (more or
* less) to save everytime someone gave us money. */
public static final int SUBSCRIPTION_START = 2;
/** Indicates that a subscription was canceled or lapsed from use. */
public static final int SUBSCRIPTION_ENDED = 3;
// AUTO-GENERATED: FIELDS START
public static final Class<ConversionRecord> _R = ConversionRecord.class;
public static final ColumnExp<Timestamp> RECORDED = colexp(_R, "recorded");
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
public static final ColumnExp<Short> ACTION = colexp(_R, "action");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The timestamp on which the action was taken. */
@Id @Column(name="RECORDED")
public Timestamp recorded;
/** The unique identifier of the user that took an action. */
@Column(name="USER_ID")
public int userId;
/**
* The partner with whom the user is associated. You should always
* use the accessor method to access this value in order to correctly
* deal with personal referrals.
*/
@Column(name="SITE_ID")
public int siteId;
/** The identifier indicating the action taken. */
@Column(name="ACTION")
public short action;
/**
* Returns the site id associated with this record, properly mapping
* negative ids (personal referrals) into a single category.
*/
public int getSiteId ()
{
return (siteId <= 0) ? OOOUser.REFERRAL_SITE_ID : siteId;
}
@Override
public String toString ()
{
return StringUtil.fieldsToString(this);
}
public boolean equals (ConversionRecord cr)
{
return (cr.userId == userId && cr.siteId == siteId &&
cr.action == action && cr.recorded.equals(recorded));
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link ConversionRecord}
* with the supplied key values.
*/
public static Key<ConversionRecord> getKey (Timestamp recorded)
{
return newKey(_R, recorded);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(RECORDED); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,234 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Timestamp;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.clause.OrderBy;
import com.samskivert.depot.clause.Where;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.util.IntIntMap;
import com.samskivert.util.Calendars;
/**
* Provides an interface to the conversion repository. A service that
* makes use of the OOO user management and billing system can make use of
* this conversion service to track conversion related information for
* their users.
*/
@Singleton
public class ConversionRepository extends DepotRepository
{
/**
* The database identifier used when establishing a database
* connection. This value being <code>conversiondb</code>.
*/
public static final String CONVERSION_DB_IDENT = "conversiondb";
@Inject public ConversionRepository (PersistenceContext ctx)
{
super(ctx);
}
/**
* Creates the repository and prepares it for operation.
*
* @param conprov the database connection provider.
*/
public ConversionRepository (ConnectionProvider conprov)
{
super(new PersistenceContext(CONVERSION_DB_IDENT, conprov, null));
}
/**
* Records an action in the conversion table.
*/
public void recordAction (int userId, int siteId, int action)
{
recordAction(userId, siteId, action, new Timestamp(System.currentTimeMillis()));
}
/**
* Records an action in the conversion table.
*/
public void recordAction (int userId, int siteId, int action, Timestamp recorded)
{
ConversionRecord record = new ConversionRecord();
record.userId = userId;
record.siteId = siteId;
record.action = (short)action;
record.recorded = recorded;
insert(record);
}
/**
* Get the subscriber info for a given date broken up by site. Key 0
* references a total for all sites.
*/
public IntIntMap getSiteSubscribers (java.util.Date date)
{
checkRecomputeSubInfo();
return _subscribers.get(date);
}
/**
* Return the total number of subscribers on then given date.
*/
public int getTotalSubscribers (java.util.Date date)
{
checkRecomputeSubInfo();
return _subscribers.get(date).get(0);
}
/**
* Returns all of our subscription info. Date -> IntIntMap,
* IntIntMap: SiteId -> Subscribers. SiteId 0 is a total of all sites.
*/
public Map<Date, IntIntMap> getSubscriptionInfo ()
{
checkRecomputeSubInfo();
return _subscribers;
}
/**
* Recompute our subscription info if it is older than the cache time.
*/
protected void checkRecomputeSubInfo ()
{
if (_subTime + SUB_CACHE_TIME < System.currentTimeMillis()) {
computeSubscriptionInfo();
}
}
/**
* Build up all the subscriber data over time and affiliate.
*/
protected void computeSubscriptionInfo ()
{
// get all the raw data from the db, ordered by date
List<ConversionRecord> data = findAll(ConversionRecord.class,
new Where(ConversionRecord.ACTION.in(ConversionRecord.SUBSCRIPTION_START,
ConversionRecord.SUBSCRIPTION_ENDED)),
OrderBy.ascending(ConversionRecord.RECORDED));
java.util.Date recent = null;
Set<Integer> subs = Sets.newHashSet();
Map<Integer, Set<Integer>> siteSubs = Maps.newHashMap();
// these are used to do the right thing if we get both a subscribe
// and unsubscribe action in the same day, but in the wrong order
Set<Integer> subexcept = Sets.newHashSet();
Set<Integer> unsubexcept = Sets.newHashSet();
for (ConversionRecord cr : data) {
// we only care about the date, not the time, at which the
// entry was recorded since we are hashing by day
java.util.Date day = Calendars.at(cr.recorded).zeroTime().toDate();
// if the date changes, store the data for the most recent day
if (recent == null) {
recent = day;
} else if (!recent.equals(day)) {
subexcept.clear();
unsubexcept.clear();
addSubscriptionEntry(recent, subs, siteSubs);
recent = day;
}
// make sure we have a sitewise set of subscribers
if (!siteSubs.containsKey(cr.getSiteId())) {
siteSubs.put(cr.getSiteId(), Sets.<Integer>newHashSet());
}
Set<Integer> siteMap = siteSubs.get(cr.getSiteId());
// update our data
if (cr.action == ConversionRecord.SUBSCRIPTION_START) {
// handle not-subscribed -> unsubscribe -> subscribe
if (!checkExcept(unsubexcept, subexcept,
subs.contains(cr.userId), cr)) {
subs.add(cr.userId);
siteMap.add(cr.userId);
}
} else if (cr.action == ConversionRecord.SUBSCRIPTION_ENDED) {
// handle is-subscribed -> subscribe -> unsubscribe
if (!checkExcept(subexcept, unsubexcept,
!subs.contains(cr.userId), cr)) {
subs.remove(cr.userId);
siteMap.remove(cr.userId);
}
}
}
// add in the incomplete current day info
addSubscriptionEntry(Calendars.now().zeroTime().toDate(), subs, siteSubs);
// and mark when we finished
_subTime = System.currentTimeMillis();
}
protected boolean checkExcept (
Set<Integer> expect, Set<Integer> surprise, boolean currentStatus, ConversionRecord cr)
{
// first check to see if he's in the opposite exception set; in
// which case we clear him out and ignore this action
if (expect.remove(cr.userId)) {
return true;
}
// next, check to see if he's already in the expected state, in which
// case we put him into the exception set
return currentStatus;
}
/**
* Add an entry to the _subscriptions data for the given day.
*/
protected void addSubscriptionEntry (
java.util.Date day, Set<Integer> subs, Map<Integer, Set<Integer>> siteSubs)
{
IntIntMap daysubs = new IntIntMap();
for (Map.Entry<Integer, Set<Integer>> entry : siteSubs.entrySet()) {
Set<Integer> set = entry.getValue();
int key = entry.getKey().intValue();
daysubs.put(key, set.size());
}
daysubs.put(0, subs.size());
_subscribers.put(day, daysubs);
}
@Override
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
{
classes.add(ConversionRecord.class);
}
/**
* Contains subscription info about our users over time. Keys are
* dates and the values are IntIntMaps mapping siteId -> subsrivers
* with 0 holding the total for all sites.
*/
protected Map<Date, IntIntMap> _subscribers = Maps.newHashMap();
/** The time at which we last computed our subscriber info. */
protected long _subTime;
/** The time in millis to cache our subscriber info before recomputing. */
protected long SUB_CACHE_TIME = 1l * 60l * 60l * 1000l;
}
@@ -0,0 +1,536 @@
//
// $Id$
package com.threerings.user.depot;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.common.collect.Maps;
import com.samskivert.depot.DatabaseException;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.servlet.RedirectException;
import com.samskivert.servlet.user.AuthenticationFailedException;
import com.samskivert.servlet.user.Authenticator;
import com.samskivert.servlet.user.InvalidPasswordException;
import com.samskivert.servlet.user.NoSuchUserException;
import com.samskivert.servlet.user.Password;
import com.samskivert.servlet.user.User;
import com.samskivert.servlet.util.CookieUtil;
import com.samskivert.servlet.util.RequestUtils;
import com.samskivert.util.Interval;
import com.samskivert.util.RunQueue;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Tuple;
import com.threerings.user.OOOUser;
import static com.threerings.user.Log.log;
/**
* A OOOUserManager using a depot repository.
*/
public class DepotUserManager
{
/** An instance of the insecure authenticator for general-purpose use. */
public static final Authenticator AUTH_INSECURE = new InsecureAuthenticator();
/** An instance of the password authenticator for general-purpose use. */
public static final Authenticator AUTH_PASSWORD = new PasswordAuthenticator();
/**
* A totally insecure authenticator that authenticates any user. <em>Note:</em> Applications
* that make use of this authenticator should make sure the user has already been authenticated
* through some other means.
*/
public static class InsecureAuthenticator implements Authenticator
{
// documentation inherited
public void authenticateUser (User user, String username, Password password)
throws InvalidPasswordException
{
// don't care
}
}
/**
* An authenticator that requires that the user-supplied password match the actual user
* password.
*/
public static class PasswordAuthenticator implements Authenticator
{
// documentation inherited
public void authenticateUser (User user, String username, Password password)
throws AuthenticationFailedException
{
if (!user.passwordsMatch(password)) {
throw new InvalidPasswordException("error.invalid_password");
}
}
}
/**
* Creates our Depot user manager and prepares it for operation.
*/
public DepotUserManager (Properties config, ConnectionProvider conprov)
throws DatabaseException
{
this(config, new PersistenceContext("userdb", conprov, null));
}
/**
* Creates our Depot user manager and prepares it for operation.
*/
public DepotUserManager (Properties config, PersistenceContext pctx)
throws DatabaseException
{
this(config, pctx, null);
}
/**
* Creates our Depot user manager and prepares it for operation.
*/
public DepotUserManager (Properties config, PersistenceContext pctx, RunQueue pruneQueue)
throws DatabaseException
{
init(config, pctx, pruneQueue);
}
/**
* Creates a depot user manager which must subsequently be initialized with a call to
* {@link #init}.
*/
public DepotUserManager ()
{
}
/**
* Prepares this user manager for operation. Presently the user manager requires the
* following configuration information:
*
* <ul>
* <li><code>login_url</code>: Should be set to the URL to which to redirect a requester if
* they are required to login before accessing the requested page. For example:
*
* <pre>
* login_url = /usermgmt/login.ajsp?return=%R
* </pre>
*
* The <code>%R</code> will be replaced with the URL encoded URL the user is currently
* requesting (complete with query parameters) so that the login code can redirect the user
* back to this request once they are authenticated.
* </ul>
*
* @param config the user manager configuration properties.
* @param pctx the persistence context
*/
public void init (Properties config, PersistenceContext pctx)
throws DatabaseException
{
init(config, pctx, null);
}
/**
* Prepares this user manager for operation. See {@link #init(Properties,PersistenceContext)}.
*
* @param pruneQueue an optional run queue on which to run our periodic session pruning task.
*/
public void init (Properties config, PersistenceContext pctx, RunQueue pruneQueue)
throws DatabaseException
{
// save this for later
_config = config;
// create the user repository
_repository = createRepository(pctx);
// fetch the login URL from the properties
_loginURL = config.getProperty("login_url");
if (_loginURL == null) {
log.warning("No login_url supplied in user manager config. Authentication won't work.");
_loginURL = "/missing_login_url";
}
// look up any override to our user auth cookie
String authCook = config.getProperty("auth_cookie.name");
if (!StringUtil.isBlank(authCook)) {
_userAuthCookie = authCook;
}
if (USERMGR_DEBUG) {
log.info("UserManager initialized", "acook", _userAuthCookie, "login", _loginURL);
}
// register a cron job to prune the session table every hour
_pruner = new Interval() {
@Override public void expired () {
_repository.pruneSessions();
}
};
if (pruneQueue != null) {
_pruner.setRunQueue(pruneQueue);
}
_pruner.schedule(SESSION_PRUNE_INTERVAL, true);
// look up the access denied URL
_accessDeniedURL = config.getProperty("access_denied_url");
if (_accessDeniedURL == null) {
log.warning("No 'access_denied_url' supplied in user manager config. " +
"Restricted pages will behave strangely.");
}
// load up our affiliate tag mappings
for (AffiliateTagRecord sub : getRepository().loadAffiliateTags()) {
_tagMap.put(sub.tag, sub.tagId);
}
}
public void shutdown ()
{
// cancel our session table pruning thread
_pruner.cancel();
}
/**
* Returns a reference to the repository in use by this user manager.
*/
public DepotUserRepository getRepository ()
{
return _repository;
}
/**
* Returns the id to which the specified tag has been mapped, assigning a new tag id if
* necessary.
*
* @return -1 if an error occurred assigning a new id.
*/
public int getAffiliateTagId (String tag)
{
// if we've already mapped this value, we're good to go
Integer tagId = _tagMap.get(tag);
if (tagId != null) {
return tagId;
}
// register a new affiliate tag and add it to our mappings
tagId = getRepository().registerAffiliateTag(tag);
_tagMap.put(tag, tagId);
return tagId;
}
/**
* Both tag strings and ids are unique, so grab the string (key) based on the id (value)
* @param tagId ID to search for (eg 45)
* @return String value of the tag or tags (eg "nov07GroupA::DF34A9")
*/
public String getAffiliateTagString (int tagId)
{
Integer theTagId = Integer.valueOf(tagId);
for (Map.Entry<String,Integer> entry : _tagMap.entrySet()) {
if (theTagId.equals(entry.getValue())) {
return entry.getKey();
}
}
return null;
}
/**
* Returns the authentication token for the given request
*/
public String getAuthToken (HttpServletRequest req)
{
return CookieUtil.getCookieValue(req, _userAuthCookie);
}
/**
* Fetches the necessary authentication information from the http request and loads the user
* identified by that information.
*
* @return the user associated with the request or null if no user was associated with the
* request or if the authentication information is bogus.
*/
public OOOUser loadUser (HttpServletRequest req)
{
String authcook = getAuthToken(req);
if (USERMGR_DEBUG) {
log.info("Loading user by cookie", _userAuthCookie, authcook);
}
return loadUser(authcook);
}
/**
* Loads up a user based on the supplied session authentication token.
*/
public OOOUser loadUser (String authcode)
{
OOOUser user = (authcode == null) ? null : _repository.loadUserBySession(authcode, false);
if (USERMGR_DEBUG) {
log.info("Loaded user by authcode", "code", authcode, "user", user);
}
return user;
}
/**
* Fetches the necessary authentication information from the http request and loads the user
* identified by that information. If no user could be loaded (because the requester is not
* authenticated), a redirect exception will be thrown to redirect the user to the login page
* specified in the user manager configuration.
*
* @return the user associated with the request.
*/
public OOOUser requireUser (HttpServletRequest req)
throws RedirectException
{
OOOUser user = loadUser(req);
// if no user was loaded, we need to redirect these fine people to the login page
if (user == null) {
String eurl = RequestUtils.getLocationEncoded(req);
String target = _loginURL.replace("%R", eurl);
if (USERMGR_DEBUG) {
log.info("No user found in require, redirecting", "to", target);
}
throw new RedirectException(target);
}
return user;
}
/**
* Extends the standard {@link #requireUser(HttpServletRequest)} with
* the additional requirement that the user hold the specified token.
* If they do not, they will be redirected to a page informing them
* access is denied.
*
* @return the user associated with the request.
*/
public OOOUser requireUser (HttpServletRequest req, byte token)
throws RedirectException
{
OOOUser user = requireUser(req);
if (!user.holdsToken(token)) {
throw new RedirectException(_accessDeniedURL);
}
return user;
}
/**
* Extends the standard {@link #requireUser(HttpServletRequest)} with
* the additional requirement that the user hold one of the specified
* tokens. If they do not, they will be redirected to a page informing
* them access is denied.
*
* @return the user associated with the request.
*/
public OOOUser requireUser (HttpServletRequest req, byte[] tokens)
throws RedirectException
{
OOOUser user = requireUser(req);
if (!user.holdsAnyToken(tokens)) {
throw new RedirectException(_accessDeniedURL);
}
return user;
}
/**
* Attempts to authenticate the requester and initiate an authenticated session for them. An
* authenticated session involves their receiving a cookie that proves them to be authenticated
* and an entry in the session database being created that maps their information to their
* userid. If this call completes, the session was established and the proper cookies were set
* in the supplied response object. If invalid authentication information is provided or some
* other error occurs, an exception will be thrown.
*
* @param username The username supplied by the user.
* @param password The password supplied by the user.
* @param persist If true, the cookie will expire in one month, if false, the cookie will
* expire at the end of the user's browser session.
* @param req The request via which the login page was loaded.
* @param rsp The response in which the cookie is to be set.
* @param auth The authenticator used to check whether the user should be authenticated.
*
* @return the user object of the authenticated user.
*/
public OOOUser login (String username, Password password, boolean persist,
HttpServletRequest req, HttpServletResponse rsp, Authenticator auth)
throws AuthenticationFailedException
{
// load up the requested user
OOOUser user = _repository.loadUser(username);
if (user == null) {
throw new NoSuchUserException("error.no_such_user");
}
// run the user through the authentication gamut
auth.authenticateUser(user, username, password);
// give them the necessary cookies and business
effectLogin(user, persist, req, rsp);
return user;
}
/**
* Attempts to authenticate the requester and initiate an authenticated session for them. A
* session token will be assigned to the user and returned along with the associated {@link
* User} record. It is assumed that the client will maintain the session token via its own
* means.
*
* @param username the username supplied by the user.
* @param password the password supplied by the user.
* @param expires the number of days in which this session should expire.
* @param auth the authenticator used to check whether the user should be authenticated.
*
* @return the user object of the authenticated user.
*/
public Tuple<OOOUser,String> login (
String username, Password password, int expires, Authenticator auth)
throws AuthenticationFailedException
{
// load up the requested user
OOOUser user = _repository.loadUser(username);
if (user == null) {
throw new NoSuchUserException("error.no_such_user");
}
// run the user through the authentication gamut
auth.authenticateUser(user, username, password);
// register a session for this user
String authcode = _repository.registerSession(user, expires);
if (USERMGR_DEBUG) {
log.info("Session started", "user", username, "code", authcode);
}
return new Tuple<OOOUser,String>(user, authcode);
}
/**
* If a user is already known to be authenticated for one reason or other, this method can be
* used to give them the appropriate authentication cookies to effect their login.
*
* @param persist If true, the cookie will expire in one month, if false, the cookie will
* expire at the end of the user's browser session.
* @return The registered session authcode
*/
public String effectLogin (
OOOUser user, boolean persist, HttpServletRequest req, HttpServletResponse rsp)
{
return effectLogin(user, persist ? PERSIST_EXPIRE_DAYS : NON_PERSIST_EXPIRE_DAYS, req, rsp);
}
/**
* If a user is already known to be authenticated for one reason or other, this method can be
* used to give them the appropriate authentication cookies to effect their login.
*
* @param expires the number of days in which to expire the session cookie, 0 means expire at
* the end of the browser session.
* @return The registered session authcode
*/
public String effectLogin (
OOOUser user, int expires, HttpServletRequest req, HttpServletResponse rsp)
{
String authcode = _repository.registerSession(user, Math.max(expires, 1));
Cookie acookie = new Cookie(_userAuthCookie, authcode);
// strip the hostname from the server and use that as the domain unless configured not to
if (!"false".equalsIgnoreCase(_config.getProperty("auth_cookie.strip_hostname"))) {
CookieUtil.widenDomain(req, acookie);
}
acookie.setPath("/");
acookie.setMaxAge((expires > 0) ? (expires*24*60*60) : -1);
if (USERMGR_DEBUG) {
log.info("Setting cookie " + acookie + ".");
}
rsp.addCookie(acookie);
return authcode;
}
/**
* Logs the user out.
*/
public void logout (HttpServletRequest req, HttpServletResponse rsp)
{
// nothing to do if they don't already have an auth cookie
String authcode = CookieUtil.getCookieValue(req, _userAuthCookie);
if (authcode == null) {
return;
}
// set them up the bomb
Cookie c = new Cookie(_userAuthCookie, "x");
c.setPath("/");
c.setMaxAge(0);
CookieUtil.widenDomain(req, c);
if (USERMGR_DEBUG) {
log.info("Clearing cookie " + c + ".");
}
rsp.addCookie(c);
// we need an unwidened one to ensure that old-style cookies are wiped as well
c = new Cookie(_userAuthCookie, "x");
c.setPath("/");
c.setMaxAge(0);
rsp.addCookie(c);
}
/**
* Validates that the supplied session key is still valid and if so, refreshes it for the
* specified number of days.
*
* @return true if the session was located and refreshed, false otherwise.
*/
public boolean refreshSession (String sessionKey, int expireDays)
{
return _repository.refreshSession(sessionKey, expireDays);
}
/**
* Called by the user manager to create the user repository. Derived classes can override this
* and create a specialized repository if they so desire.
*/
protected DepotUserRepository createRepository (PersistenceContext pctx)
throws DatabaseException
{
return new DepotUserRepository(pctx);
}
/** Our user manager configuration. */
protected Properties _config;
/** The user repository. */
protected DepotUserRepository _repository;
/** The interval for user session pruning. */
protected Interval _pruner;
/** The URL for the user login page. */
protected String _loginURL;
/** The name of our user authentication cookie. */
protected String _userAuthCookie = USERAUTH_COOKIE;
/** Maintains a mapping of affiliate tags. */
protected Map<String,Integer> _tagMap = Maps.newHashMap();
/** The URL to which we redirect users whose access is denied. */
protected String _accessDeniedURL;
/** The user authentication cookie name. */
protected static final String USERAUTH_COOKIE = "id_";
/** Prune the session table every hour. */
protected static final long SESSION_PRUNE_INTERVAL = 60L * 60L * 1000L;
/** Indicates how long (in days) that a "persisting" session token should last. */
protected static final int PERSIST_EXPIRE_DAYS = 30;
/** Indicates how long (in days) that a "non-persisting" session token should last. */
protected static final int NON_PERSIST_EXPIRE_DAYS = 1;
/** Change this to true and recompile to debug cookie handling. */
protected static final boolean USERMGR_DEBUG = false;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Computed;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
import com.threerings.user.DetailedUser;
/**
* A computed persistent entity that loads detailed user information.
*/
@Computed
@Entity
public class DetailedUserRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<DetailedUserRecord> _R = DetailedUserRecord.class;
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<String> USERNAME = colexp(_R, "username");
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
public static final ColumnExp<String> EMAIL = colexp(_R, "email");
public static final ColumnExp<Date> BIRTHDAY = colexp(_R, "birthday");
public static final ColumnExp<Byte> GENDER = colexp(_R, "gender");
public static final ColumnExp<String> MISSIVE = colexp(_R, "missive");
// AUTO-GENERATED: FIELDS END
/** The user's assigned userid. */
@Id @Computed(shadowOf=OOOUserRecord.class)
public int userId;
/** The user's chosen username. */
@Computed(shadowOf=OOOUserRecord.class)
public String username;
/** The date this record was created. */
@Computed(shadowOf=OOOUserRecord.class)
public Date created;
/** The user's email address. */
@Computed(shadowOf=OOOUserRecord.class)
public String email;
/** The user's birthday. */
@Column(name="BIRTHDAY") @Computed(shadowOf=OOOAuxDataRecord.class)
public Date birthday;
/** The user's gender. */
@Column(name="GENDER") @Computed(shadowOf=OOOAuxDataRecord.class)
public byte gender;
/** The user's personal missive to us. */
@Column(name="MISSIVE") @Computed(shadowOf=OOOAuxDataRecord.class)
public String missive;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link DetailedUserRecord}
* with the supplied key values.
*/
public static Key<DetailedUserRecord> getKey (int userId)
{
return newKey(_R, userId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(USER_ID); }
// AUTO-GENERATED: METHODS END
/**
* Creates a DetailedUser from the record.
*/
public DetailedUser toDetailedUser ()
{
DetailedUser user = new DetailedUser();
user.userId = userId;
user.username = username;
user.created = created;
user.email = email;
user.birthday = birthday;
user.gender = gender;
user.missive = missive;
return user;
}
}
@@ -0,0 +1,60 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
import com.samskivert.depot.expression.ColumnExp;
import com.threerings.user.ExternalAuther;
/**
* Maintains information on a mapping from an external authentication source to a OOOUser.
*/
public class ExternalAuthRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<ExternalAuthRecord> _R = ExternalAuthRecord.class;
public static final ColumnExp<ExternalAuther> AUTHER = colexp(_R, "auther");
public static final ColumnExp<String> EXTERNAL_ID = colexp(_R, "externalId");
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<String> SESSION_KEY = colexp(_R, "sessionKey");
// AUTO-GENERATED: FIELDS END
/** Increment this value if you modify the definition of this persistent object in a way that
* will result in a change to its SQL counterpart. */
public static final int SCHEMA_VERSION = 1;
/** The auther that maintains the external user id. */
@Id public ExternalAuther auther;
/** The external user identifier. */
@Id public String externalId;
/** The id of the OOOUser account associated with the specified external account. */
@Index(name="ixUserId")
public int userId;
/** The most recent session key provided by the external site, for use in making API requests
* to said site based on our most recently active session. */
@Column(nullable=true)
public String sessionKey;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link ExternalAuthRecord}
* with the supplied key values.
*/
public static Key<ExternalAuthRecord> getKey (ExternalAuther auther, String externalId)
{
return newKey(_R, auther, externalId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(AUTHER, EXTERNAL_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,128 @@
//
// $Id$
package com.threerings.user.depot;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.expression.FluentExp;
import com.samskivert.util.Tuple;
import com.threerings.user.ExternalAuther;
/**
* Maps authentication information from external sources (like Facebook, OAuth providers, etc.) to
* our internal (ooouser) userId number.
*/
@Singleton
public class ExternalAuthRepository extends DepotRepository
{
@Inject public ExternalAuthRepository (PersistenceContext ctx)
{
super(ctx);
}
/**
* Returns the OOOUser id of the member with the supplied external credentials, or 0 if the
* supplied creds are not mapped to a OOOUser account.
*/
public int loadUserIdForExternal (ExternalAuther auther, String externalId)
{
Preconditions.checkNotNull(auther);
ExternalAuthRecord exrec = load(ExternalAuthRecord.getKey(auther, externalId));
return (exrec == null) ? 0 : exrec.userId;
}
/**
* Returns the external id of the given OOOUser with the given ExternalAuther.
*/
public String loadExternalIdForUser (ExternalAuther auther, int userId)
{
Preconditions.checkNotNull(auther);
ExternalAuthRecord exrec = from(ExternalAuthRecord.class).where(
ExternalAuthRecord.USER_ID.eq(userId).and(ExternalAuthRecord.AUTHER.eq(auther))).load();
return (exrec == null) ? null : exrec.externalId;
}
/**
* Returns the most recently saved session key for the supplied external account, or null if no
* mapping exists for said account or no key has yet been stored for it.
*/
public String loadExternalSessionKey (ExternalAuther auther, String externalId)
{
Preconditions.checkNotNull(auther);
ExternalAuthRecord exrec = load(ExternalAuthRecord.getKey(auther, externalId));
return (exrec == null) ? null : exrec.sessionKey;
}
/**
* Returns (externalId, sessionKey) for the supplied user, or null if no mapping exists for
* said user. The sessionKey may be null if no key has yet been stored.
*/
public Tuple<String, String> loadExternalAuthInfo (ExternalAuther auther, int userId)
{
Preconditions.checkNotNull(auther);
ExternalAuthRecord exrec = from(ExternalAuthRecord.class).where(
ExternalAuthRecord.AUTHER.eq(auther), ExternalAuthRecord.USER_ID.eq(userId)).load();
return (exrec == null) ? null : Tuple.newTuple(exrec.externalId, exrec.sessionKey);
}
/**
* Loads a mapping of (exid -> ooouser id) for all members in the supplied list that are found
* in the database.
*/
public Map<String, Integer> loadUserIds (ExternalAuther auther, Collection<String> externalIds)
{
Preconditions.checkNotNull(auther);
Map<String, Integer> ids = Maps.newHashMap();
for (ExternalAuthRecord exrec : from(ExternalAuthRecord.class).where(
ExternalAuthRecord.AUTHER.eq(auther),
ExternalAuthRecord.EXTERNAL_ID.in(externalIds)).select()) {
ids.put(exrec.externalId, exrec.userId);
}
return ids;
}
/**
* Creates a mapping for the specified user to the supplied external credentials.
*/
public void mapExternalAccount (int userId, ExternalAuther auther, String externalId,
String sessionKey)
{
Preconditions.checkNotNull(auther);
ExternalAuthRecord exrec = new ExternalAuthRecord();
exrec.auther = auther;
exrec.externalId = externalId;
exrec.userId = userId;
exrec.sessionKey = sessionKey;
insert(exrec);
}
/**
* Updates the session key on file for the specified user's mapping for the specified auther.
*/
public void updateExternalSession (ExternalAuther auther, String externalId, String sessionKey)
{
Preconditions.checkNotNull(auther);
updatePartial(ExternalAuthRecord.getKey(auther, externalId),
ExternalAuthRecord.SESSION_KEY, sessionKey);
}
@Override // from DepotRepository
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
{
classes.add(ExternalAuthRecord.class);
}
}
@@ -0,0 +1,61 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
import com.samskivert.depot.expression.ColumnExp;
/**
* Retains information about a historical user registration.
*/
@Entity(name="history")
public class HistoricalUserRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<HistoricalUserRecord> _R = HistoricalUserRecord.class;
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<String> USERNAME = colexp(_R, "username");
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The user's assigned integer userid. */
@Id
public int userId;
/** The user's chosen username. */
@Column
public String username;
/** The date this record was created. */
@Index
public Date created;
/** The affiliate site with which this user is associated. */
@Index
public int siteId;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link HistoricalUserRecord}
* with the supplied key values.
*/
public static Key<HistoricalUserRecord> getKey (int userId)
{
return newKey(_R, userId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(USER_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,67 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
import com.samskivert.depot.expression.ColumnExp;
/**
* An invitation record.
*/
@Entity
public class InvitationRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<InvitationRecord> _R = InvitationRecord.class;
public static final ColumnExp<String> INVITATION = colexp(_R, "invitation");
public static final ColumnExp<String> EMAIL = colexp(_R, "email");
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<Integer> INVITER_USER_ID = colexp(_R, "inviterUserId");
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
public static final ColumnExp<Date> SENT = colexp(_R, "sent");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 4;
/** The invitation key. */
@Id @Column(length=32)
public String invitation;
/** The e-mail address this invitation was sent to. */
public String email;
/** The account activated with this invitation. */
public int userId;
/** The account used to generate the invitation. */
@Index
public int inviterUserId;
/** The date this invitation was created. */
public Date created;
/** The date the last e-mail was sent. */
public Date sent;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link InvitationRecord}
* with the supplied key values.
*/
public static Key<InvitationRecord> getKey (String invitation)
{
return newKey(_R, invitation);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(INVITATION); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,223 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import java.util.List;
import java.util.Random;
import java.util.Set;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.DuplicateKeyException;
import com.samskivert.depot.Key;
import com.samskivert.depot.KeySet;
import com.samskivert.depot.Ops;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.SchemaMigration;
import com.samskivert.depot.annotation.Computed;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.clause.FromOverride;
import com.samskivert.depot.clause.Limit;
import com.samskivert.depot.clause.OrderBy;
import com.samskivert.depot.clause.Where;
/**
* The invitation repository.
*/
@Singleton
public class InvitationRepository extends DepotRepository
{
@Computed @Entity
public static class CountRecord extends PersistentRecord
{
/** The computed count. */
@Computed(fieldDefinition="count(*)")
public int count;
}
@Inject public InvitationRepository (PersistenceContext ctx)
{
super(ctx);
ctx.registerMigration(InvitationRecord.class,
new SchemaMigration.Retype(3, InvitationRecord.INVITER_USER_ID));
ctx.registerMigration(InvitationRecord.class,
new SchemaMigration.Add(4, InvitationRecord.SENT, "'2000-01-01'"));
}
/**
* Creates a new invitation record.
*/
public InvitationRecord createInvitationRecord (String email)
{
return createInvitationRecord(email, 0);
}
/**
* Creates a new invitation record.
*/
public InvitationRecord createInvitationRecord (String email, String username)
{
OOOUserRecord user = load(
OOOUserRecord.class, new Where(OOOUserRecord.USERNAME.eq(username)));
return user == null ? null : createInvitationRecord(email, user.userId);
}
/**
* Creates a new invitation record.
*/
public InvitationRecord createInvitationRecord (String email, int inviterId)
{
InvitationRecord rec = new InvitationRecord();
rec.email = email;
rec.inviterUserId = inviterId;
rec.created = new Date(System.currentTimeMillis());
rec.sent = rec.created;
for (int ii = 0; ii < 10; ii++) {
rec.invitation = Long.toString(Math.abs((new Random()).nextLong()), 16);
try {
insert(rec);
} catch (DuplicateKeyException e) {
continue;
}
return rec;
}
return null;
}
/**
* Creates a new multiple invitation record.
*/
public MultipleInvitationRecord createMultipleInvitationRecord (String code, int max)
{
MultipleInvitationRecord rec = new MultipleInvitationRecord();
rec.invitation = code;
rec.maxInvitations = max;
try {
insert(rec);
} catch (DuplicateKeyException e) {
return null;
}
return rec;
}
/**
* Fetches an invitation record.
*/
public InvitationRecord getInvitation (String invitation)
{
return load(InvitationRecord.getKey(invitation));
}
/**
* Returns a list of invitations starting with the specified record number and containing at
* most <code>count</code> elements.
*/
public List<InvitationRecord> getInvitationRecords (int start, int count)
{
return findAll(InvitationRecord.class, OrderBy.descending(InvitationRecord.CREATED),
new Limit(start, count));
}
/**
* Returns a list of unused invitations created before a certain date.
*/
public List<InvitationRecord> getUnusedInvitationRecords (Date before)
{
return findAll(InvitationRecord.class,
new Where(Ops.and(InvitationRecord.USER_ID.eq(0),
InvitationRecord.CREATED.lessEq(before),
InvitationRecord.SENT.lessEq(before))),
OrderBy.ascending(InvitationRecord.EMAIL));
}
/**
* Returns true if the multiple invitation code is valid and has invitations remaining.
*/
public MultipleInvitationRecord getOpenMultipleInvitation (String code)
{
MultipleInvitationRecord rec = load(MultipleInvitationRecord.class,
new Where(MultipleInvitationRecord.INVITATION.eq(code)));
if (rec == null) {
return null;
}
return isMultipleInvitationOpen(rec) ? rec : null;
}
/**
* Returns a list of multiple invtations records.
*/
public List<MultipleInvitationRecord> getMultipleInvitations ()
{
return findAll(MultipleInvitationRecord.class);
}
/**
* Activates an invitation record.
*/
public void activateInvitation (InvitationRecord rec, int userId)
{
rec.userId = userId;
update(rec);
}
/**
* Activates a multiple invitation.
*
* @return false if the user has already claimed an invitation
*/
public boolean activateMultipleInvitation (int invitationId, int userId)
{
UserInvitationRecord uir = new UserInvitationRecord();
uir.invitationId = invitationId;
uir.userId = userId;
try {
insert(uir);
} catch (DuplicateKeyException e) {
return false;
}
return true;
}
/**
* Updates the created date for a set of invitations.
*/
public void pokeInvitations (List<String> invitations)
{
updatePartial(InvitationRecord.class,
new Where(InvitationRecord.INVITATION.in(invitations)),
KeySet.newKeySet(InvitationRecord.class, Lists.transform(invitations,
new Function<String, Key<InvitationRecord>>() {
public Key<InvitationRecord> apply (String invitation) {
return InvitationRecord.getKey(invitation);
}
})),
InvitationRecord.SENT, new Date(System.currentTimeMillis()));
}
/**
* Verifies a multiple invitation record is still open.
*/
protected boolean isMultipleInvitationOpen (MultipleInvitationRecord rec)
{
return (rec.maxInvitations > load(CountRecord.class,
new FromOverride(UserInvitationRecord.class),
new Where(UserInvitationRecord.INVITATION_ID.eq(rec.invitationId))).count);
}
@Override
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
{
classes.add(InvitationRecord.class);
classes.add(MultipleInvitationRecord.class);
classes.add(UserInvitationRecord.class);
}
}
@@ -0,0 +1,24 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.depot.annotation.Computed;
/**
* Used to determine the max userId.
*/
@Computed
public class MaxUserRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<MaxUserRecord> _R = MaxUserRecord.class;
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
// AUTO-GENERATED: FIELDS END
/** The user id. */
@Computed
public int userId;
}
@@ -0,0 +1,54 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.GeneratedValue;
import com.samskivert.depot.annotation.GenerationType;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
/**
* A multiple invitation record.
*/
@Entity
public class MultipleInvitationRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<MultipleInvitationRecord> _R = MultipleInvitationRecord.class;
public static final ColumnExp<Integer> INVITATION_ID = colexp(_R, "invitationId");
public static final ColumnExp<String> INVITATION = colexp(_R, "invitation");
public static final ColumnExp<Integer> MAX_INVITATIONS = colexp(_R, "maxInvitations");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The invitation id. */
@Id @GeneratedValue(strategy=GenerationType.AUTO)
public int invitationId;
/** The invitation code. */
@Column(length=32, unique=true)
public String invitation;
/** The max number of accounts that can be created with this invitation. */
public int maxInvitations;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link MultipleInvitationRecord}
* with the supplied key values.
*/
public static Key<MultipleInvitationRecord> getKey (int invitationId)
{
return newKey(_R, invitationId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(INVITATION_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,88 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
import com.threerings.user.OOOAuxData;
/**
* Emulates {@link OOOAuxData} for the Depot.
*/
@Entity(name="AUXDATA")
public class OOOAuxDataRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<OOOAuxDataRecord> _R = OOOAuxDataRecord.class;
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<Date> BIRTHDAY = colexp(_R, "birthday");
public static final ColumnExp<Byte> GENDER = colexp(_R, "gender");
public static final ColumnExp<String> MISSIVE = colexp(_R, "missive");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The user's unique identifier. */
@Id @Column(name="USER_ID")
public int userId;
/** The user's birthday. */
@Column(name="BIRTHDAY", defaultValue="'1900-01-01'")
public Date birthday;
/** The user's gender. */
@Column(name="GENDER", defaultValue="0")
public byte gender;
/** The user's personal missive to us. */
@Column(name="MISSIVE")
public String missive;
/**
* Creates a OOOAuxDataRecord from a OOOAuxData.
*/
public static OOOAuxDataRecord fromOOOAuxData (OOOAuxData aux)
{
OOOAuxDataRecord record = new OOOAuxDataRecord();
record.userId = aux.userId;
record.birthday = aux.birthday;
record.gender = aux.gender;
record.missive = aux.missive;
return record;
}
/**
* Returns a OOOAuxData version of this record.
*/
public OOOAuxData toOOOAuxData ()
{
OOOAuxData aux = new OOOAuxData();
aux.userId = userId;
aux.birthday = birthday;
aux.gender = gender;
aux.missive = missive;
return aux;
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link OOOAuxDataRecord}
* with the supplied key values.
*/
public static Key<OOOAuxDataRecord> getKey (int userId)
{
return newKey(_R, userId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(USER_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,81 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Timestamp;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
import com.threerings.user.OOOBillAuxData;
/**
* Emulates {@link OOOBillAuxData} for the Depot.
*/
@Entity(name="BILLAUXDATA")
public class OOOBillAuxDataRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<OOOBillAuxDataRecord> _R = OOOBillAuxDataRecord.class;
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<Timestamp> FIRST_COIN_BUY = colexp(_R, "firstCoinBuy");
public static final ColumnExp<Timestamp> LATEST_COIN_BUY = colexp(_R, "latestCoinBuy");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 2;
/** The user's unique identifier. */
@Id @Column(name="USER_ID")
public int userId;
/** The first time the user bought coins. */
@Column(name="FIRST_COIN_BUY", nullable=true, defaultValue="NULL")
public Timestamp firstCoinBuy;
/** The most recent time the user bought coins. */
@Column(name="LATEST_COIN_BUY", nullable=true, defaultValue="NULL")
public Timestamp latestCoinBuy;
/**
* Creates a OOOBillAuxDataRecord from OOOBillAuxData.
*/
public static OOOBillAuxDataRecord fromOOOBillAuxData (OOOBillAuxData aux)
{
OOOBillAuxDataRecord record = new OOOBillAuxDataRecord();
record.userId = aux.userId;
record.firstCoinBuy = aux.firstCoinBuy;
record.latestCoinBuy = aux.latestCoinBuy;
return record;
}
/**
* Returns a OOOBillAuxData version of this record.
*/
public OOOBillAuxData toOOOBillAuxData ()
{
OOOBillAuxData aux = new OOOBillAuxData();
aux.userId = userId;
aux.firstCoinBuy = firstCoinBuy;
aux.latestCoinBuy = latestCoinBuy;
return aux;
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link OOOBillAuxDataRecord}
* with the supplied key values.
*/
public static Key<OOOBillAuxDataRecord> getKey (int userId)
{
return newKey(_R, userId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(USER_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,214 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import java.util.Set;
import com.google.common.collect.Sets;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.StringFuncs;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.GeneratedValue;
import com.samskivert.depot.annotation.GenerationType;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
import com.samskivert.depot.clause.OrderBy.Order;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.depot.expression.SQLExpression;
import com.samskivert.util.Tuple;
import com.threerings.user.OOOUser;
/**
* Emulates {@link OOOUser} for the Depot.
*/
@Entity(name="users",
indices={ @Index(name="ixLowerUsername", unique=true), @Index(name="ixLowerEmail") })
public class OOOUserRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<OOOUserRecord> _R = OOOUserRecord.class;
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<String> USERNAME = colexp(_R, "username");
public static final ColumnExp<String> PASSWORD = colexp(_R, "password");
public static final ColumnExp<String> EMAIL = colexp(_R, "email");
public static final ColumnExp<String> REALNAME = colexp(_R, "realname");
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
public static final ColumnExp<Integer> AFFILIATE_TAG_ID = colexp(_R, "affiliateTagId");
public static final ColumnExp<Integer> FLAGS = colexp(_R, "flags");
public static final ColumnExp<byte[]> TOKENS = colexp(_R, "tokens");
public static final ColumnExp<Byte> YOHOHO = colexp(_R, "yohoho");
public static final ColumnExp<String> SPOTS = colexp(_R, "spots");
public static final ColumnExp<Integer> SHUN_LEFT = colexp(_R, "shunLeft");
// AUTO-GENERATED: FIELDS END
/** Fields that will be checked for modification and updated when calling
* {@link DepotUserRepository#updateUser}. */
public static ColumnExp<?>[] UPDATABLE_FIELDS = {
USERNAME, PASSWORD, EMAIL, REALNAME, AFFILIATE_TAG_ID,
FLAGS, TOKENS, YOHOHO, SPOTS, SHUN_LEFT
};
public static final int SCHEMA_VERSION = 4;
/** The user's assigned integer userid. */
@Id @GeneratedValue(strategy=GenerationType.AUTO)
public int userId;
/** The user's chosen username. */
@Column(length=128, unique=true)
public String username;
/** The user's chosen password (encrypted). */
@Column(length=128)
public String password;
/** The user's email address. */
@Column(length=128) @Index(name="ixEmail")
public String email;
/** The user's real name (first, last and whatever else they opt to provide). */
@Column(length=128)
public String realname;
/** The date this record was created. */
public Date created;
/** The site identifier of the site through which the user created
* their account. (Their affiliation, if you will.) */
public int siteId;
/** The id of any opaque tag provided by the affiliate to tag this user for their purposes. */
public int affiliateTagId;
/** 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 Puzzle Pirates. (TRIAL_STATE, SUBSCRIBER_STATE, etc) */
public byte yohoho;
/** The spots that have been given to the user by various crews. */
@Column(length=128)
public String spots;
/** The amount of time remaining on the users shun, in minutes. */
public int shunLeft;
/**
* Defines the index on {@link #username} converted to lower case.
*/
public static Tuple<SQLExpression<?>, Order> ixLowerUsername ()
{
return new Tuple<SQLExpression<?>, Order>(
StringFuncs.lower(OOOUserRecord.USERNAME), Order.ASC);
}
/**
* Defines the index on {@link #email} converted to lower case.
*/
public static Tuple<SQLExpression<?>, Order> ixLowerEmail ()
{
return new Tuple<SQLExpression<?>, Order>(StringFuncs.lower(OOOUserRecord.EMAIL), Order.ASC);
}
/**
* Creates a OOOUserRecord from a OOOUser.
*/
public static OOOUserRecord fromUser (OOOUser user)
{
OOOUserRecord record = new OOOUserRecord();
record.userId = user.userId;
record.username = user.username;
record.created = user.created;
record.realname = user.realname;
record.password = user.password;
record.email = user.email;
record.siteId = user.siteId;
record.flags = user.flags;
record.tokens = user.tokens;
record.yohoho = user.yohoho;
record.spots = user.spots;
record.shunLeft = user.shunLeft;
record.affiliateTagId = user.affiliateTagId;
return record;
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link OOOUserRecord}
* with the supplied key values.
*/
public static Key<OOOUserRecord> getKey (int userId)
{
return newKey(_R, userId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(USER_ID); }
// AUTO-GENERATED: METHODS END
/**
* Returns true if this user holds the specified token.
*/
public boolean holdsToken (byte token)
{
if (tokens == null) {
return false;
}
for (byte heldTok : tokens) {
if (heldTok == token) {
return true;
}
}
return false;
}
/**
* Returns a OOOUser version of this record.
*/
public OOOUser toUser ()
{
OOOUser user = new DepotOOOUser();
user.userId = userId;
user.username = username;
user.created = created;
user.realname = realname;
user.password = password;
user.email = email;
user.siteId = siteId;
user.flags = flags;
user.tokens = tokens;
user.yohoho = yohoho;
user.spots = spots;
user.shunLeft = shunLeft;
user.affiliateTagId = affiliateTagId;
return user;
}
/**
* A OOOUser with special dirty handling.
*/
protected class DepotOOOUser extends OOOUser
{
public Set<ColumnExp<?>> mods;
@Override
protected void setModified (String field)
{
if (mods == null) {
mods = Sets.newHashSet();
}
mods.add(colexp(_R, field));
}
}
}
@@ -0,0 +1,48 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
/**
* Actions that have been processed by a server.
*/
@Entity(name="PROCESSED_ACTIONS")
public class ProcessedActionRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<ProcessedActionRecord> _R = ProcessedActionRecord.class;
public static final ColumnExp<Integer> ACTION_ID = colexp(_R, "actionId");
public static final ColumnExp<String> SERVER = colexp(_R, "server");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The action id. */
@Id @Column(name="ACTION_ID")
public int actionId;
/** The name of the server that has processed the action. */
@Id @Column(name="SERVER")
public String server;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link ProcessedActionRecord}
* with the supplied key values.
*/
public static Key<ProcessedActionRecord> getKey (int actionId, String server)
{
return newKey(_R, actionId, server);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(ACTION_ID, SERVER); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,30 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.depot.annotation.Computed;
/**
* Used to summarize recent registrants from the {@link HistoricalUserRecord} table.
*/
@Computed(shadowOf=HistoricalUserRecord.class)
public class RecentUserRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<RecentUserRecord> _R = RecentUserRecord.class;
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
public static final ColumnExp<Integer> ENTRIES = colexp(_R, "entries");
// AUTO-GENERATED: FIELDS END
/** The created date. */
public Date created;
/** The number of users created on the date. */
@Computed(fieldDefinition="count(*)")
public int entries;
}
@@ -0,0 +1,63 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.GeneratedValue;
import com.samskivert.depot.annotation.GenerationType;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
import com.samskivert.depot.expression.ColumnExp;
/**
* Records information on a particular referral.
*/
@Entity(name="REFERRAL")
public class ReferralRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<ReferralRecord> _R = ReferralRecord.class;
public static final ColumnExp<Integer> REFERRAL_ID = colexp(_R, "referralId");
public static final ColumnExp<Date> RECORDED = colexp(_R, "recorded");
public static final ColumnExp<Integer> REFERRER_ID = colexp(_R, "referrerId");
public static final ColumnExp<String> DATA = colexp(_R, "data");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 2;
/** The refferal's assigned integer id. */
@Id @GeneratedValue(strategy=GenerationType.AUTO) @Column(name="REFERRAL_ID")
public int referralId;
/** The date the referral was made. */
@Column(name="RECORDED", defaultValue="'1900-01-01'") @Index
public Date recorded;
/** The user id of the referring user. */
@Column(name="REFERRER_ID", defaultValue="0") @Index
public int referrerId;
/** Data associated with this referral record. */
@Column(name="DATA")
public String data;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link ReferralRecord}
* with the supplied key values.
*/
public static Key<ReferralRecord> getKey (int referralId)
{
return newKey(_R, referralId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(REFERRAL_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,99 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import java.util.Set;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.clause.Where;
import com.samskivert.util.Calendars;
/**
* Tracks the data needed for our personal referral system whereby one
* user sends a referral link (or email) to their friend and we track the
* original referring user if their referred friend actually registers
* within some reasonable time frame (like a couple of weeks).
*/
@Singleton
public class ReferralRepository extends DepotRepository
{
@Inject public ReferralRepository (PersistenceContext ctx)
{
super(ctx);
}
/**
* Records a new referral record in the system and returns the unique
* identifier for said record.
*/
public int recordReferral (int referrerId, String data)
{
final ReferralRecord record = new ReferralRecord();
record.recorded = new Date(System.currentTimeMillis());
record.referrerId = referrerId;
record.data = data;
insert(record);
checkPurge();
return record.referralId;
}
/**
* Looks up a referral record with the specified id, returning null if
* no matching record could be found.
*/
public ReferralRecord lookupReferral (int referralId)
{
return load(ReferralRecord.getKey(referralId));
}
/**
* Looks up a referral record with the specified referring user id,
* returning null if no matching record could be found.
*/
public ReferralRecord lookupReferrer (int referrerId)
{
return load(ReferralRecord.class, new Where(ReferralRecord.REFERRER_ID, referrerId));
}
/**
* Used to periodically purge old referral records from the
* repository.
*/
protected void checkPurge ()
{
long now = System.currentTimeMillis();
if (now - _lastPurge > PURGE_INTERVAL) {
_lastPurge = now;
purgeStaleReferrals();
}
}
/**
* Purges stale referral records from the repository.
*/
protected void purgeStaleReferrals ()
{
deleteAll(ReferralRecord.class, new Where(ReferralRecord.RECORDED.lessThan(
Calendars.now().zeroTime().addMonths(-1).toSQLDate())));
}
@Override // documentation inherited
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
{
classes.add(ReferralRecord.class);
}
/** The last time we purged the repository of stale records. */
protected long _lastPurge;
/** We purge the repository every three hours of stale records. */
protected static final long PURGE_INTERVAL = 3 * 60 * 60 * 1000L;
}
@@ -0,0 +1,28 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.depot.annotation.Computed;
/**
* Used to summarize rewards.
*/
@Computed(shadowOf=RewardInfoRecord.class)
public class RewardCountRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<RewardCountRecord> _R = RewardCountRecord.class;
public static final ColumnExp<Integer> REWARD_ID = colexp(_R, "rewardId");
public static final ColumnExp<Integer> COUNT = colexp(_R, "count");
// AUTO-GENERATED: FIELDS END
/** The reward id. */
public int rewardId;
/** The number of matching records. */
@Computed(fieldDefinition="count(*)")
public int count;
}
@@ -0,0 +1,89 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.GeneratedValue;
import com.samskivert.depot.annotation.GenerationType;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.util.StringUtil;
/**
* A reward being offered.
*/
@Entity(name="REWARD_INFO")
public class RewardInfoRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<RewardInfoRecord> _R = RewardInfoRecord.class;
public static final ColumnExp<Integer> REWARD_ID = colexp(_R, "rewardId");
public static final ColumnExp<String> DESCRIPTION = colexp(_R, "description");
public static final ColumnExp<String> COUPON_CODE = colexp(_R, "couponCode");
public static final ColumnExp<String> DATA = colexp(_R, "data");
public static final ColumnExp<Date> EXPIRATION = colexp(_R, "expiration");
public static final ColumnExp<Integer> MAX_ELIGIBLE_ID = colexp(_R, "maxEligibleId");
public static final ColumnExp<Integer> ACTIVATIONS = colexp(_R, "activations");
public static final ColumnExp<Integer> REDEMPTIONS = colexp(_R, "redemptions");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** Unique reward id. */
@Id @Column(name="REWARD_ID") @GeneratedValue(strategy=GenerationType.AUTO)
public int rewardId;
/** Reward description. */
@Column(name="DESCRIPTION")
public String description;
/** A coupon code required to activate this reward or null. */
@Column(name="COUPON_CODE", nullable=true)
public String couponCode;
/** Game specific reward data. */
@Column(name="DATA")
public String data;
/** Date when the reward expires. */
@Column(name="EXPIRATION")
public Date expiration;
/** Max userid that can redeem this reward. */
@Column(name="MAX_ELIGIBLE_ID")
public int maxEligibleId;
/** Number of activated rewards. */
@Column(name="ACTIVATIONS")
public int activations;
/** Number of claimed rewards. */
@Column(name="REDEMPTIONS")
public int redemptions;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link RewardInfoRecord}
* with the supplied key values.
*/
public static Key<RewardInfoRecord> getKey (int rewardId)
{
return newKey(_R, rewardId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(REWARD_ID); }
// AUTO-GENERATED: METHODS END
@Override // from Object
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
@@ -0,0 +1,62 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
import com.samskivert.depot.expression.ColumnExp;
/**
* A persistent reward.
*/
@Entity(name="REWARD_RECORDS")
public class RewardRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<RewardRecord> _R = RewardRecord.class;
public static final ColumnExp<Integer> REWARD_ID = colexp(_R, "rewardId");
public static final ColumnExp<String> ACCOUNT = colexp(_R, "account");
public static final ColumnExp<String> REDEEMER_IDENT = colexp(_R, "redeemerIdent");
public static final ColumnExp<String> PARAM = colexp(_R, "param");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** RewardInfo reward id. */
@Id @Column(name="REWARD_ID")
public int rewardId;
/** Account name. */
@Id @Index(name="ixAccount") @Column(name="ACCOUNT", nullable=true)
public String account;
/** Unique identification value. */
@Index(name="ixRedeemerIdent") @Column(name="REDEEMER_IDENT", length=64, nullable=true)
public String redeemerIdent;
/**
* A generic parameter string that can tell the reward handler more specifically when and what
* item, etc. to hand out for this instance of the reward.
*/
@Id @Column(name="PARAM", nullable=true)
public String param;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link RewardRecord}
* with the supplied key values.
*/
public static Key<RewardRecord> getKey (int rewardId, String account, String param)
{
return newKey(_R, rewardId, account, param);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(REWARD_ID, ACCOUNT, PARAM); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,304 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;
import java.util.Set;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.DuplicateKeyException;
import com.samskivert.depot.KeySet;
import com.samskivert.depot.Ops;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.clause.FromOverride;
import com.samskivert.depot.clause.GroupBy;
import com.samskivert.depot.clause.OrderBy;
import com.samskivert.depot.clause.Where;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.depot.expression.SQLExpression;
import com.samskivert.jdbc.ConnectionProvider;
import com.threerings.user.OOOUserRepository;
import static com.threerings.user.Log.log;
/**
* Maintains persistent reward information.
*/
@Singleton
public class RewardRepository extends DepotRepository
{
@Inject public RewardRepository (PersistenceContext ctx)
{
super(ctx);
}
/**
* Creates the repository with the specified connection provider.
*
*/
public RewardRepository (ConnectionProvider provider)
{
super(new PersistenceContext(OOOUserRepository.USER_REPOSITORY_IDENT, provider, null));
}
/**
* Creates a new RewardInfo record in the database. <code>info</code> should have the
* <code>description</code>, <code>data</code> and <code>expiration</code> filled in.
*/
public void createReward (RewardInfoRecord info)
{
//info.maxEligibleId = getMaxUserId();
store(info);
}
/**
* Immediately expires a reward by setting the expiration to the current date then making a
* call to <code>purgeExpiredRewards</code>.
*/
public void expireReward (int rewardId)
{
updatePartial(RewardInfoRecord.getKey(rewardId), RewardInfoRecord.EXPIRATION,
new Date(System.currentTimeMillis()));
purgeExpiredRewards();
}
/**
* Attempt to activate the reward for an account. If this account has not already activated
* the reward, a new RewardRecord will be created. Returns true if a new RewardRecord is
* created, false otherwise.
*/
public boolean activateReward (int rewardId, String account)
{
return activateReward(rewardId, account, null);
}
public boolean activateReward (int rewardId, String account, String param)
{
RewardRecord rr = new RewardRecord();
rr.rewardId = rewardId;
rr.account = account;
rr.param = param == null ? "" : param;
try {
insert(rr);
} catch (DuplicateKeyException e) {
return false;
}
return true;
}
/**
* Activates monthly rewards of the specified ID for the account between start and end. If they
* already have monthly rewards for this ID, will add new ones only at appropriate monthly
* intervals from the existing ones.
*/
public boolean activateMonthlyRewards (
int rewardId, String account, java.util.Date start, java.util.Date end)
{
// Find the most recent reward this account already has of this type.
java.util.Date latest = null;
List<RewardRecord> matches = findAll(RewardRecord.class,
new Where(Ops.and(RewardRecord.REWARD_ID.eq(rewardId),
RewardRecord.ACCOUNT.eq(account))));
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// Find the most recent reward this account already has of this type.
for (RewardRecord rec : matches) {
if (rec.param == null) {
log.warning("Missing monthly reward param: ", "rewardId", rewardId,
"account", account);
continue;
}
try {
java.util.Date thisDate = dateFormat.parse(rec.param);
if (latest == null || thisDate.after(latest)) {
latest = thisDate;
}
} catch (ParseException pe) {
log.warning("Bogus reward param: ", "rewardId", rewardId,
"account", account, "param", rec.param);
continue;
}
}
// We use the maximum of our provided start time and a month-after-latest
// as the first time we'll input a reward. Note that this can result
// in no rewards being given.
Calendar startCal = Calendar.getInstance();
if (latest != null) {
startCal.setTime(latest);
startCal.add(Calendar.MONTH, 1);
if (startCal.getTime().before(start)) {
startCal.setTime(start);
}
} else {
startCal.setTime(start);
}
// Add in any appropriate rewards between the modified start and end.
while (startCal.getTime().before(end)) {
RewardRecord rr = new RewardRecord();
rr.rewardId = rewardId;
rr.account = account;
rr.param = dateFormat.format(startCal.getTime());
insert(rr);
startCal.add(Calendar.MONTH, 1);
}
return true;
}
/**
* Deactivates all rewards of the specified ID and account during the time window.
* Returns the number of rewards deleted.
*/
public int deactivateMonthlyRewards (
int rewardId, String account, java.util.Date start, java.util.Date end)
{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String startStr = dateFormat.format(start);
String endStr = dateFormat.format(end);
return deleteAll(RewardRecord.class,
new Where(Ops.and(
RewardRecord.REWARD_ID.eq(rewardId),
RewardRecord.ACCOUNT.eq(account),
RewardRecord.PARAM.greaterEq(startStr),
RewardRecord.PARAM.lessEq(endStr),
RewardRecord.REDEEMER_IDENT.isNull())));
}
/**
* Returns all currently active rewards.
*/
public List<RewardInfoRecord> loadActiveRewards ()
{
return findAll(RewardInfoRecord.class,
new Where(RewardInfoRecord.EXPIRATION.greaterThan(
new Timestamp(System.currentTimeMillis()))));
}
/**
* Returns all rewards.
*/
public List<RewardInfoRecord> loadRewards ()
{
return findAll(RewardInfoRecord.class, OrderBy.descending(RewardInfoRecord.REWARD_ID));
}
/**
* Returns all <code>RewardRecord</code>s that match the given account name and whose eligible
* date is in the past. The returned list will be sorted by <code>rewardId</code>.
*/
public List<RewardRecord> loadActivatedRewards (String account)
{
return findAll(RewardRecord.class,
new Where(RewardRecord.ACCOUNT.eq(account)),
OrderBy.ascending(RewardRecord.REWARD_ID));
}
/**
* Returns all <code>RewardRecord</code>s that match either the account or redeemer identifier.
* The returned list will be sorted by <code>rewardId</code>.
*/
public List<RewardRecord> loadActivatedRewards (String account, String redeemerIdent)
{
return findAll(RewardRecord.class,
new Where(Ops.or(RewardRecord.ACCOUNT.eq(account),
RewardRecord.REDEEMER_IDENT.eq(redeemerIdent))),
OrderBy.ascending(RewardRecord.REWARD_ID).thenAscending(RewardRecord.PARAM));
}
/**
* Returns all <code>RewardRecord</code>s for a specified <code>rewardId</code> that match
* either the account or redeemer identifier.
*/
public List<RewardRecord> loadActivatedReward (
String account, String redeemerIdent, int rewardId)
{
return findAll(RewardRecord.class,
new Where(Ops.and(RewardRecord.REWARD_ID.eq(rewardId),
Ops.or(RewardRecord.ACCOUNT.eq(account),
RewardRecord.REDEEMER_IDENT.eq(redeemerIdent)))));
}
/**
* Returns all <code>RewardRecord</code>s for a specified <code>rewardId</code> and
* <code>param</code> that match either the account or redeemer identifier.
*/
public List<RewardRecord> loadActivatedReward (
String account, String redeemerIdent, int rewardId, String param)
{
SQLExpression<?> paramExp = param == null ?
RewardRecord.PARAM.isNull() : RewardRecord.PARAM.eq(param);
return findAll(RewardRecord.class,
new Where(Ops.and(
RewardRecord.REWARD_ID.eq(rewardId),
paramExp,
Ops.or(
RewardRecord.ACCOUNT.eq(account),
RewardRecord.REDEEMER_IDENT.eq(redeemerIdent)))));
}
/**
* Updates the supplied RewardRecord with the indicated <code>redeemerIdent</code> and store it
* to the database.
*/
public void redeemReward (RewardRecord record, String redeemerIdent)
{
record.redeemerIdent = redeemerIdent;
update(record, RewardRecord.REDEEMER_IDENT);
}
/**
* Summarizes the reward data and purges expired rewards.
*/
public void purgeExpiredRewards ()
{
summarizeAndUpdate(null, RewardInfoRecord.ACTIVATIONS);
summarizeAndUpdate(
Ops.not(RewardRecord.REDEEMER_IDENT.isNull()), RewardInfoRecord.REDEMPTIONS);
deleteAll(RewardRecord.class,
KeySet.newKeySet(RewardRecord.class, findAllKeys(RewardRecord.class, true,
new FromOverride(RewardRecord.class, RewardInfoRecord.class),
new Where(Ops.and(
RewardRecord.REWARD_ID.eq(RewardInfoRecord.REWARD_ID),
RewardInfoRecord.EXPIRATION.lessEq(new Timestamp(System.currentTimeMillis()))
)))));
}
/**
* Summarizes the reward data and stores it in the reward info records.
*/
protected void summarizeAndUpdate (SQLExpression<?> condition, ColumnExp<?> column)
{
SQLExpression<?> where = RewardInfoRecord.REWARD_ID.eq(RewardRecord.REWARD_ID);
if (condition != null) {
where = Ops.and(where, condition);
}
List<RewardCountRecord> counts = findAll(RewardCountRecord.class,
new FromOverride(RewardInfoRecord.class, RewardRecord.class),
new Where(where),
new GroupBy(RewardInfoRecord.REWARD_ID));
for (RewardCountRecord record : counts) {
updatePartial(RewardInfoRecord.getKey(record.rewardId), column, record.count);
}
}
@Override
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
{
classes.add(RewardRecord.class);
classes.add(RewardInfoRecord.class);
}
}
@@ -0,0 +1,56 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
/**
* Maintains information on authenticated HTTP sessions.
*/
@Entity(name="sessions")
public class SessionRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<SessionRecord> _R = SessionRecord.class;
public static final ColumnExp<String> AUTHCODE = colexp(_R, "authcode");
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<Date> EXPIRES = colexp(_R, "expires");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** A unique code that identifies this session. */
@Id @Column(length=32)
public String authcode;
/** The id of the user authenticated in this session. */
@Index(name="userid_index")
public int userId;
/** The date on which the session expires. */
@Index(name="expires_index")
public Date expires;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link SessionRecord}
* with the supplied key values.
*/
public static Key<SessionRecord> getKey (String authcode)
{
return newKey(_R, authcode);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(AUTHCODE); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,54 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
/**
* Represents a row in the TAINTED_IDENTS table, Depot version.
*/
@Entity(name="TAINTED_IDENTS")
public class TaintedIdentRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<TaintedIdentRecord> _R = TaintedIdentRecord.class;
public static final ColumnExp<String> MACH_IDENT = colexp(_R, "machIdent");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** A 'unique' id for a specific machine we have seen. */
@Id @Column(name="MACH_IDENT")
public String machIdent;
/** Blank constructor for the unserialization business. */
public TaintedIdentRecord ()
{
}
/** A constructor that populates this record. */
public TaintedIdentRecord (String machIdent)
{
this.machIdent = machIdent;
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link TaintedIdentRecord}
* with the supplied key values.
*/
public static Key<TaintedIdentRecord> getKey (String machIdent)
{
return newKey(_R, machIdent);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(MACH_IDENT); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,61 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
import com.samskivert.depot.expression.ColumnExp;
/**
* Extends the basic samskivert user record with special Three Rings business.
*/
@Entity(name="USER_IDENTS")
public class UserIdentRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<UserIdentRecord> _R = UserIdentRecord.class;
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<String> MACH_IDENT = colexp(_R, "machIdent");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 4;
/** The id of the user in question. */
@Id @Column(name="USER_ID")
public int userId;
/** A 'unique' id for a specific machine we have seen the user come from. */
@Id @Column(name="MACH_IDENT") @Index(name="ixMachIdent")
public String machIdent;
public UserIdentRecord ()
{
super();
}
public UserIdentRecord (int userId, String machIdent)
{
super();
this.userId = userId;
this.machIdent = machIdent;
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link UserIdentRecord}
* with the supplied key values.
*/
public static Key<UserIdentRecord> getKey (int userId, String machIdent)
{
return newKey(_R, userId, machIdent);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(USER_ID, MACH_IDENT); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,48 @@
//
// $Id$
package com.threerings.user.depot;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
import com.samskivert.depot.expression.ColumnExp;
/**
* Maps a multiple invitation record to a user.
*/
@Entity
public class UserInvitationRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<UserInvitationRecord> _R = UserInvitationRecord.class;
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<Integer> INVITATION_ID = colexp(_R, "invitationId");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The user id. */
@Id
public int userId;
/** The invitation id. */
@Index
public int invitationId;
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link UserInvitationRecord}
* with the supplied key values.
*/
public static Key<UserInvitationRecord> getKey (int userId)
{
return newKey(_R, userId);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(USER_ID); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,85 @@
//
// $Id$
package com.threerings.user.depot;
import java.sql.Date;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
import com.threerings.user.ValidateRecord;
/**
* Emulates {@link ValidateRecord} for the Depot.
*/
@Entity(name="penders")
public class ValidateDepotRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<ValidateDepotRecord> _R = ValidateDepotRecord.class;
public static final ColumnExp<String> SECRET = colexp(_R, "secret");
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
public static final ColumnExp<Boolean> PERSIST = colexp(_R, "persist");
public static final ColumnExp<Date> INSERTED = colexp(_R, "inserted");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 1;
/** The secret key. */
@Id @Column(length=32)
public String secret;
/** The user id. */
public int userId;
/** The persist? */
public boolean persist;
/** The date inserted. */
public Date inserted;
/**
* Creates a ValidateDepotRecord from a ValidateRecord.
*/
public static ValidateDepotRecord fromValidateRecord (ValidateRecord vr)
{
ValidateDepotRecord record = new ValidateDepotRecord();
record.secret = vr.secret;
record.userId = vr.userId;
record.persist = vr.persist;
record.inserted = vr.inserted;
return record;
}
/**
* Returns a ValidateRecord version of this record.
*/
public ValidateRecord toValidateRecord ()
{
ValidateRecord record = new ValidateRecord();
record.secret = secret;
record.userId = userId;
record.persist = persist;
record.inserted = inserted;
return record;
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link ValidateDepotRecord}
* with the supplied key values.
*/
public static Key<ValidateDepotRecord> getKey (String secret)
{
return newKey(_R, secret);
}
/** Register the key fields in an order matching the getKey() factory. */
static { registerKeyFields(SECRET); }
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,68 @@
//
// $Id$
package com.threerings.user.tools;
import java.util.List;
import com.samskivert.util.Config;
import com.samskivert.jdbc.StaticConnectionProvider;
import com.threerings.user.AccountAction;
import com.threerings.user.depot.AccountActionRepository;
/**
* Provides command line inspection and processing of account actions.
*/
public class AccountActionTool
{
public static void main (String[] args)
{
if (args.length == 0) {
System.err.println(USAGE);
System.exit(255);
}
Config config = new Config("threerings");
try {
AccountActionRepository repo = new AccountActionRepository(
new StaticConnectionProvider(config.getSubProperties("db")));
if (args[0].equals("list")) {
while (true) {
String server = (args.length > 1) ? args[1] : "";
List<AccountAction> actions = repo.getActions(server, 999);
for (int ii = 0; ii < actions.size(); ii++) {
System.out.println(actions.get(ii));
}
try { Thread.sleep(1000L); } catch (Exception e) {}
}
} else if (args[0].equals("prune")) {
repo.pruneActions();
} else if (args[0].equals("add")) {
if (args.length != 3) {
System.err.println(USAGE);
System.exit(255);
}
repo.addAction(args[1], Integer.parseInt(args[2]));
} else if (args[0].equals("process")) {
if (args.length != 3) {
System.err.println(USAGE);
System.exit(255);
}
repo.noteProcessed(Integer.parseInt(args[1]), args[2]);
}
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
protected static final String USAGE =
"Usage: AccountActionTool [prune|list|list server|" +
"add account_name action_type|process action_id server]";
}
@@ -0,0 +1,143 @@
//
// $Id$
package com.threerings.user.tools;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.samskivert.jdbc.StaticConnectionProvider;
import com.samskivert.net.MailUtil;
import com.samskivert.servlet.user.User;
import com.samskivert.util.ArrayUtil;
import com.samskivert.util.Config;
import com.threerings.user.OOOUser;
import com.threerings.user.OOOUserRepository;
/**
* A script for activating (or deactivating) the tester flag on users.
*/
public class ActivateTesters
{
public static void main (String[] args)
{
if (args.length == 0 ||
!(args[0].equals("activate") || args[0].equals("clear")) ||
(args[0].equals("activate") && args.length < 3)) {
System.err.println(USAGE);
System.exit(255);
}
Config config = new Config("threerings");
try {
OOOUserRepository urepo = new OOOUserRepository(
new StaticConnectionProvider(config.getSubProperties("db")));
if (args[0].equals("clear")) {
clearTesters(urepo);
} else if (args[0].equals("activate")) {
activateTesters(urepo, Integer.parseInt(args[1]), args[2]);
}
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
protected static void activateTesters (
OOOUserRepository urepo, int count, String emailFile)
throws Exception
{
// load up the emails and randomize them
BufferedReader bin = new BufferedReader(new FileReader(emailFile));
List<String> elist = Lists.newArrayList();
String email;
int rejected = 0;
while ((email = bin.readLine()) != null) {
if (!MailUtil.isValidAddress(email)) {
// System.err.println("Rejecting invalid address: " + email);
rejected++;
continue;
}
elist.add(email);
}
if (rejected > 0) {
System.err.println("Rejected " + rejected +
" invalid addresses, accepted " +
elist.size() + " valid addresses.");
}
String[] emails = elist.toArray(new String[elist.size()]);
ArrayUtil.shuffle(emails);
int activated = 0, offset = 0;
while (activated < count && offset < emails.length) {
// grab the next N addresses from the list and see if any of them
// have an account that we can activate as a tester
StringBuilder where = new StringBuilder();
for (int ii = offset, ll = Math.min(emails.length, offset+CHUNK);
ii < ll; ii++) {
if (where.length() > 0) {
where.append(", ");
}
where.append("'").append(emails[ii]).append("'");
}
List<User> users = urepo.lookupUsersWhere("email in (" + where + ")");
// filter out users that have multiple accounts; cheeky bastards
HashSet<String> seen = Sets.newHashSet(), filter = Sets.newHashSet();
for (User ruser : users) {
OOOUser user = (OOOUser)ruser;
if (seen.contains(user.email)) {
filter.add(user.email);
} else {
seen.add(user.email);
}
}
for (Iterator<User> iter = users.iterator(); iter.hasNext(); ) {
OOOUser user = (OOOUser)iter.next();
if (filter.contains(user.email)) {
iter.remove();
}
}
// now mark anyone that made it through the guantlet as a tester
for (Iterator<User> iter = users.iterator(); iter.hasNext(); ) {
OOOUser user = (OOOUser)iter.next();
if (user.holdsToken(OOOUser.TESTER)) {
continue;
}
user.addToken(OOOUser.TESTER);
System.out.println(user.username + " " + user.email);
urepo.updateUser(user);
activated++;
}
offset += CHUNK;
}
System.err.println("Activated " + activated + " accounts.");
}
protected static void clearTesters (OOOUserRepository urepo)
throws Exception
{
List<User> users = urepo.lookupUsersWhere("HEX(tokens) like '%04%'");
System.err.println("Clearing " + users.size() + " testers.");
for (int ii = 0, ll = users.size(); ii < ll; ii++) {
OOOUser user = (OOOUser)users.get(ii);
user.removeToken(OOOUser.TESTER);
urepo.updateUser(user);
}
}
protected static final String USAGE =
"Usage: ActivateTesters [activate count emails.txt | clear]";
protected static final int CHUNK = 25;
}
@@ -0,0 +1,41 @@
//
// $Id$
package com.threerings.user.tools;
import com.samskivert.jdbc.StaticConnectionProvider;
import com.samskivert.util.Config;
import com.threerings.user.OOOUserRepository;
/**
* Provides command line inspection and processing of account actions.
*/
public class UserTool
{
public static void main (String[] args)
{
if (args.length == 0) {
System.err.println(USAGE);
System.exit(255);
}
Config config = new Config("threerings");
try {
OOOUserRepository repo = new OOOUserRepository(
new StaticConnectionProvider(config.getSubProperties("db")));
if (args[0].equals("prune")) {
repo.pruneUsers(PRUNE_DAYS);
}
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
protected static final String USAGE = "Usage: UserTool [prune]";
/** Users that have been purged from all other games for 30 days are toast. */
protected static final int PRUNE_DAYS = 30;
}