/*
 * reputation - Provides a scoring system for "known users".
 * (C) Copyright 2015-2019 Bram Matthys (Syzop) and the UnrealIRCd team.
 * License: GPLv2 or later
 *
 * How this works is simple:
 * Every 5 minutes the IP address of all the connected users receive
 * a point. Registered users receive 2 points every 5 minutes.
 * The total reputation score is then later used, by other modules, for
 * example to make decisions such as to reject or allow a user if the
 * server is under attack.
 * The reputation scores are saved in a database. By default this file
 * is data/reputation.db (often ~/unrealircd/data/reputation.db).
 *
 * See also https://www.unrealircd.org/docs/Connthrottle
 */

#include "unrealircd.h"

#define REPUTATION_VERSION "1.2"

/* Change to #define to benchmark. Note that this will add random
 * reputation entries so should never be used on production servers!!!
 */
#undef BENCHMARK
#undef TEST

/* Benchmark results (2GHz Xeon Skylake, compiled with -O2, Linux):
 * 10k random IP's with various expire times:
 * - load db:  23 ms
 * - expiry:    1 ms
 * - save db:   7 ms
 * 100k random IP's with various expire times:
 * - load db: 103 ms
 * - expiry:   10 ms
 * - save db:  32 ms
 * So, even for 100,000 unique IP's, the initial load of the database
 * would delay the UnrealIRCd boot process only for 0.1 second.
 * The writing of the db, which happens every 5 minutes, for such
 * amount of IP's takes 32ms (0.03 second).
 * Of course, exact figures will depend on the storage and cache.
 * That being said, the file for 100k random IP's is slightly under
 * 3MB, so not big, which likely means the timing will be similar
 * for a broad number of (storage) systems.
 */
 
#ifndef TEST
 #define BUMP_SCORE_EVERY	300
 #define DELETE_OLD_EVERY	605
 #define SAVE_DB_EVERY		902
#else
 #define BUMP_SCORE_EVERY 	3
 #define DELETE_OLD_EVERY	3
 #define SAVE_DB_EVERY		3
#endif

#ifndef CALLBACKTYPE_REPUTATION_STARTTIME
 #define CALLBACKTYPE_REPUTATION_STARTTIME 5
#endif

ModuleHeader MOD_HEADER
  = {
	"reputation",
	REPUTATION_VERSION,
	"Known IP's scoring system",
	"UnrealIRCd Team",
	"unrealircd-6",
    };

/* Defines */

#define MAXEXPIRES 10

#define REPUTATION_SCORE_CAP 10000

#define UPDATE_SCORE_MARGIN 1

#define REPUTATION_HASH_TABLE_SIZE 2048

#define Reputation(client)	moddata_client(client, reputation_md).l

#define WARN_WRITE_ERROR(fname) \
	do { \
		unreal_log(ULOG_ERROR, "reputation", "REPUTATION_FILE_WRITE_ERROR", NULL, \
			   "[reputation] Error writing to temporary database file $filename: $system_error", \
			   log_data_string("filename", fname), \
			   log_data_string("system_error", unrealdb_get_error_string())); \
	} while(0)

#define W_SAFE(x) \
	do { \
		if (!(x)) { \
			WARN_WRITE_ERROR(tmpfname); \
			unrealdb_close(db); \
			return 0; \
		} \
	} while(0)


/* Definitions (structs, etc.) */

struct cfgstruct {
	int expire_score[MAXEXPIRES];
	long expire_time[MAXEXPIRES];
	char *database;
	char *db_secret;
	int require_minimum_channel_members;
};

typedef struct ReputationEntry ReputationEntry;

struct ReputationEntry {
	ReputationEntry *prev, *next;
	unsigned short score; /**< score for the user */
	long last_seen; /**< user last seen (unix timestamp) */
	int marker; /**< internal marker, not written to db */
	char ip[]; /*< ip address */
};

/* Global variables */

static struct cfgstruct cfg; /**< Current configuration */
static struct cfgstruct test; /**< Testing configuration (not active yet) */
long reputation_starttime = 0;
long reputation_writtentime = 0;

static ReputationEntry *ReputationHashTable[REPUTATION_HASH_TABLE_SIZE];
static char siphashkey_reputation[SIPHASH_KEY_LENGTH];

static ModuleInfo ModInf;

ModDataInfo *reputation_md; /* Module Data structure which we acquire */

/* Forward declarations */
void reputation_md_free(ModData *m);
const char *reputation_md_serialize(ModData *m);
void reputation_md_unserialize(const char *str, ModData *m);
void reputation_config_setdefaults(struct cfgstruct *cfg);
void reputation_free_config(struct cfgstruct *cfg);
CMD_FUNC(reputation_cmd);
CMD_FUNC(reputationunperm);
int reputation_whois(Client *client, Client *target, NameValuePrioList **list);
int reputation_set_on_connect(Client *client);
int reputation_pre_lconnect(Client *client);
int reputation_ip_change(Client *client, const char *oldip);
int reputation_connect_extinfo(Client *client, NameValuePrioList **list);
int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int reputation_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
int reputation_config_posttest(int *errs);
static uint64_t hash_reputation_entry(const char *ip);
ReputationEntry *find_reputation_entry(const char *ip);
void add_reputation_entry(ReputationEntry *e);
EVENT(delete_old_records);
EVENT(add_scores);
EVENT(reputation_save_db_evt);
int reputation_load_db(void);
int reputation_save_db(void);
int reputation_starttime_callback(void);
void _ban_act_set_reputation(Client *client, BanAction *action);

MOD_TEST()
{
	MARK_AS_OFFICIAL_MODULE(modinfo);
	memcpy(&ModInf, modinfo, modinfo->size);
	memset(&cfg, 0, sizeof(cfg));
	memset(&test, 0, sizeof(cfg));
	reputation_config_setdefaults(&test);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, reputation_config_test);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, reputation_config_posttest);
	CallbackAdd(modinfo->handle, CALLBACKTYPE_REPUTATION_STARTTIME, reputation_starttime_callback);
	EfunctionAddVoid(modinfo->handle, EFUNC_BAN_ACT_SET_REPUTATION, _ban_act_set_reputation);
	return MOD_SUCCESS;
}

MOD_INIT()
{
	ModDataInfo mreq;

	MARK_AS_OFFICIAL_MODULE(modinfo);
	ModuleSetOptions(modinfo->handle, MOD_OPT_PERM, 1);

	memset(&ReputationHashTable, 0, sizeof(ReputationHashTable));
	siphash_generate_key(siphashkey_reputation);

	memset(&mreq, 0, sizeof(mreq));
	mreq.name = "reputation";
	mreq.free = reputation_md_free;
	mreq.serialize = reputation_md_serialize;
	mreq.unserialize = reputation_md_unserialize;
	mreq.sync = 0; /* local! */
	mreq.type = MODDATATYPE_CLIENT;
	mreq.priority = -1000000;
	reputation_md = ModDataAdd(modinfo->handle, mreq);
	if (!reputation_md)
		abort();

	reputation_config_setdefaults(&cfg);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, reputation_config_run);
	HookAdd(modinfo->handle, HOOKTYPE_WHOIS, 0, reputation_whois);
	HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, reputation_set_on_connect);
	HookAdd(modinfo->handle, HOOKTYPE_IP_CHANGE, 0, reputation_ip_change);
	HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 2000000000, reputation_pre_lconnect); /* (prio: last) */
	HookAdd(modinfo->handle, HOOKTYPE_REMOTE_CONNECT, -1000000000, reputation_set_on_connect); /* (prio: near-first) */
	HookAdd(modinfo->handle, HOOKTYPE_CONNECT_EXTINFO, 0, reputation_connect_extinfo); /* (prio: near-first) */
	CommandAdd(ModInf.handle, "REPUTATION", reputation_cmd, MAXPARA, CMD_USER|CMD_SERVER);
	CommandAdd(ModInf.handle, "REPUTATIONUNPERM", reputationunperm, MAXPARA, CMD_USER|CMD_SERVER);
	return MOD_SUCCESS;
}

#ifdef BENCHMARK
void reputation_benchmark(int entries)
{
	char ip[64];
	int i;
	ReputationEntry *e;

	srand(1234); // fixed seed

	for (i = 0; i < entries; i++)
	{
		ReputationEntry *e = safe_alloc(sizeof(ReputationEntry) + 64);
		snprintf(e->ip, 63, "%d.%d.%d.%d", rand()%255, rand()%255, rand()%255, rand()%255);
		e->score = rand()%255 + 1;
		e->last_seen = TStime();
		if (find_reputation_entry(e->ip))
		{
			safe_free(e);
			continue;
		}
		add_reputation_entry(e);
	}
}
#endif
MOD_LOAD()
{
	reputation_load_db();
	if (reputation_starttime == 0)
		reputation_starttime = TStime();
	EventAdd(ModInf.handle, "delete_old_records", delete_old_records, NULL, DELETE_OLD_EVERY*1000, 0);
	EventAdd(ModInf.handle, "add_scores", add_scores, NULL, BUMP_SCORE_EVERY*1000, 0);
	EventAdd(ModInf.handle, "reputation_save_db", reputation_save_db_evt, NULL, SAVE_DB_EVERY*1000, 0);
#ifdef BENCHMARK
	reputation_benchmark(10000);
#endif
	return MOD_SUCCESS;
}

MOD_UNLOAD()
{
	if (loop.terminating)
		reputation_save_db();
	reputation_free_config(&test);
	reputation_free_config(&cfg);
	return MOD_SUCCESS;
}

void reputation_config_setdefaults(struct cfgstruct *cfg)
{
	/* data/reputation.db */
	safe_strdup(cfg->database, "reputation.db");
	convert_to_absolute_path(&cfg->database, PERMDATADIR);

	/* EXPIRES the following entries if the IP does appear for some time: */
	/* <=2 points after 1 hour */
	cfg->expire_score[0] = 2;
#ifndef TEST
	cfg->expire_time[0]  = 3600;
#else
	cfg->expire_time[0]  = 36;
#endif
	/* <=6 points after 7 days */
	cfg->expire_score[1] = 6;
	cfg->expire_time[1]  = 86400*7;
	/* <=12 points after 30 days */
	cfg->expire_score[2] = 12;
	cfg->expire_time[2]  = 86400*30;
	/* ANY result that has not been seen for 90 days */
	cfg->expire_score[3] = -1;
	cfg->expire_time[3]  = 86400*90;

	/* The 'require' settings */
	cfg->require_minimum_channel_members = 3;
}

void reputation_free_config(struct cfgstruct *cfg)
{
	safe_free(cfg->database);
	safe_free(cfg->db_secret);
}

int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
	int errors = 0;
	ConfigEntry *cep;

	if (type != CONFIG_SET)
		return 0;

	/* We are only interrested in set::reputation.. */
	if (!ce || strcmp(ce->name, "reputation"))
		return 0;

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "score-bump-timer") || !strcmp(cep->name, "target"))
		{
			config_error("%s:%i: this feature is not implemented yet in this UnrealIRCd version",
				cep->file->filename, cep->line_number);
			errors++;
		} else
		if (!cep->value)
		{
			config_error("%s:%i: blank set::reputation::%s without value",
				cep->file->filename, cep->line_number, cep->name);
			errors++;
		} else
		if (!strcmp(cep->name, "score-bump-timer-minimum-channel-members"))
		{
		} else
		if (!strcmp(cep->name, "database"))
		{
			convert_to_absolute_path(&cep->value, PERMDATADIR);
			safe_strdup(test.database, cep->value);
		} else
		if (!strcmp(cep->name, "db-secret"))
		{
			const char *err;
			if ((err = unrealdb_test_secret(cep->value)))
			{
				config_error("%s:%i: set::channeldb::db-secret: %s", cep->file->filename, cep->line_number, err);
				errors++;
				continue;
			}
			safe_strdup(test.db_secret, cep->value);
		} else
		{
			config_error("%s:%i: unknown directive set::reputation::%s",
				cep->file->filename, cep->line_number, cep->name);
			errors++;
			continue;
		}
	}

	*errs = errors;
	return errors ? -1 : 1;
}

int reputation_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
	ConfigEntry *cep;

	if (type != CONFIG_SET)
		return 0;

	/* We are only interrested in set::reputation.. */
	if (!ce || strcmp(ce->name, "reputation"))
		return 0;

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "score-bump-timer-minimum-channel-members"))
		{
			cfg.require_minimum_channel_members = atoi(cep->value);
		} else
		if (!strcmp(cep->name, "database"))
		{
			safe_strdup(cfg.database, cep->value);
		} else
		if (!strcmp(cep->name, "db-secret"))
		{
			safe_strdup(cfg.db_secret, cep->value);
		}
	}
	return 1;
}

int reputation_config_posttest(int *errs)
{
	int errors = 0;
	char *errstr;

	if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret))))
	{
		config_error("[reputation] %s", errstr);
		errors++;
	}

	*errs = errors;
	return errors ? -1 : 1;
}

/** Parse database header and set variables appropriately */
int parse_db_header_old(char *buf)
{
	char *header=NULL, *version=NULL, *starttime=NULL, *writtentime=NULL;
	char *p=NULL;

	if (strncmp(buf, "REPDB", 5))
		return 0;

	header = strtoken(&p, buf, " ");
	if (!header)
		return 0;

	version = strtoken(&p, NULL, " ");
	if (!version || (atoi(version) != 1))
		return 0;

	starttime = strtoken(&p, NULL, " ");
	if (!starttime)
		return 0;

	writtentime = strtoken(&p, NULL, " ");
	if (!writtentime)
		return 0;

	reputation_starttime = atol(starttime);
	reputation_writtentime = atol(writtentime);

	return 1;
}

void reputation_load_db_old(void)
{
	FILE *fd;
	char buf[512], *p;
#ifdef BENCHMARK
	struct timeval tv_alpha, tv_beta;

	gettimeofday(&tv_alpha, NULL);
#endif

	fd = fopen(cfg.database, "r");
	if (!fd)
	{
		config_warn("WARNING: Could not open/read database '%s': %s", cfg.database, strerror(ERRNO));
		return;
	}

	memset(buf, 0, sizeof(buf));
	if (fgets(buf, 512, fd) == NULL)
	{
		config_error("WARNING: Database file corrupt ('%s')", cfg.database);
		fclose(fd);
		return;
	}

	/* Header contains: REPDB <version> <starttime> <writtentime>
	 * Where:
	 * REPDB:        Literally the string "REPDB".
	 * <version>     This is version 1 at the time of this writing.
	 * <starttime>   The time that recording of reputation started,
	 *               in other words: when this module was first loaded, ever.
	 * <writtentime> Time that the database was last written.
	 */
	if (!parse_db_header_old(buf))
	{
		config_error("WARNING: Cannot load database %s. Error reading header. "
		             "Database corrupt? Or are you downgrading from a newer "
		             "UnrealIRCd version perhaps? This is not supported.",
		             cfg.database);
		fclose(fd);
		return;
	}

	while(fgets(buf, 512, fd) != NULL)
	{
		char *ip = NULL, *score = NULL, *last_seen = NULL;
		ReputationEntry *e;

		stripcrlf(buf);
		/* Format: <ip> <score> <last seen> */
		ip = strtoken(&p, buf, " ");
		if (!ip)
			continue;
		score = strtoken(&p, NULL, " ");
		if (!score)
			continue;
		last_seen = strtoken(&p, NULL, " ");
		if (!last_seen)
			continue;

		e = safe_alloc(sizeof(ReputationEntry)+strlen(ip)+1);
		strcpy(e->ip, ip); /* safe, see alloc above */
		e->score = atoi(score);
		e->last_seen = atol(last_seen);

		add_reputation_entry(e);
	}
	fclose(fd);

#ifdef BENCHMARK
	gettimeofday(&tv_beta, NULL);
	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
	           "[reputation] Benchmark: LOAD DB: $time_msec microseconds",
	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
}

#define R_SAFE(x) \
	do { \
		if (!(x)) { \
			config_warn("[reputation] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
			unrealdb_close(db); \
			safe_free(ip); \
			return 0; \
		} \
	} while(0)

int reputation_load_db_new(UnrealDB *db)
{
	uint64_t l_db_version = 0;
	uint64_t l_starttime = 0;
	uint64_t l_writtentime = 0;
	uint64_t count = 0;
	uint64_t i;
	char *ip = NULL;
	uint16_t score;
	uint64_t last_seen;
	ReputationEntry *e;
#ifdef BENCHMARK
	struct timeval tv_alpha, tv_beta;

	gettimeofday(&tv_alpha, NULL);
#endif

	R_SAFE(unrealdb_read_int64(db, &l_db_version)); /* reputation db version */
	if (l_db_version > 2)
	{
		config_error("[reputation] Reputation DB is of a newer version (%ld) than supported by us (%ld). "
		             "Did you perhaps downgrade your UnrealIRCd?",
		             (long)l_db_version, (long)2);
		unrealdb_close(db);
		return 0;
	}
	R_SAFE(unrealdb_read_int64(db, &l_starttime)); /* starttime of data gathering */
	R_SAFE(unrealdb_read_int64(db, &l_writtentime)); /* current time */
	R_SAFE(unrealdb_read_int64(db, &count)); /* number of entries */

	reputation_starttime = l_starttime;
	reputation_writtentime = l_writtentime;

	for (i=0; i < count; i++)
	{
		R_SAFE(unrealdb_read_str(db, &ip));
		R_SAFE(unrealdb_read_int16(db, &score));
		R_SAFE(unrealdb_read_int64(db, &last_seen));

		e = safe_alloc(sizeof(ReputationEntry)+strlen(ip)+1);
		strcpy(e->ip, ip); /* safe, see alloc above */
		e->score = score;
		e->last_seen = last_seen;
		add_reputation_entry(e);
		safe_free(ip);
	}
	unrealdb_close(db);
#ifdef BENCHMARK
	gettimeofday(&tv_beta, NULL);
	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
	           "Reputation benchmark: LOAD DB: $time_msec microseconds",
	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
	return 1;
}

/** Load the reputation DB.
 * Strategy is:
 * 1) Check for the old header "REPDB 1", if so then call reputation_load_db_old().
 * 2) Otherwise, open with unrealdb routine
 * 3) If that fails due to a password provided but the file is unrealdb without password
 *    then fallback to open without a password (so users can easily upgrade to encrypted)
 */
int reputation_load_db(void)
{
	FILE *fd;
	UnrealDB *db;
	char buf[512];

	fd = fopen(cfg.database, "r");
	if (!fd)
	{
		/* Database does not exist. Could be first boot */
		config_warn("[reputation] No database present at '%s', will start a new one", cfg.database);
		return 1;
	}

	*buf = '\0';
	if (fgets(buf, sizeof(buf), fd) == NULL)
	{
		fclose(fd);
		config_warn("[reputation] Database at '%s' is 0 bytes", cfg.database);
		return 1;
	}
	fclose(fd);
	if (!strncmp(buf, "REPDB 1 ", 8))
	{
		reputation_load_db_old();
		return 1; /* not so good to always pretend succes */
	}

	/* Otherwise, it is an unrealdb, crypted or not */
	db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret);
	if (!db)
	{
		if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
		{
			/* Database does not exist. Could be first boot */
			config_warn("[reputation] No database present at '%s', will start a new one", cfg.database);
			return 1;
		} else
		if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED)
		{
			db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL);
		}
		if (!db)
		{
			config_error("[reputation] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string());
			return 0;
		}
	}
	return reputation_load_db_new(db);
}

int reputation_save_db_old(void)
{
	FILE *fd;
	char tmpfname[512];
	int i;
	ReputationEntry *e;
#ifdef BENCHMARK
	struct timeval tv_alpha, tv_beta;

	gettimeofday(&tv_alpha, NULL);
#endif

	/* We write to a temporary file. Only to rename it later if everything was ok */
	snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());

	fd = fopen(tmpfname, "w");
	if (!fd)
	{
		config_error("ERROR: Could not open/write database '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
		return 0;
	}

	if (fprintf(fd, "REPDB 1 %lld %lld\n", (long long)reputation_starttime, (long long)TStime()) < 0)
		goto write_fail;

	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
	{
		for (e = ReputationHashTable[i]; e; e = e->next)
		{
			if (fprintf(fd, "%s %d %lld\n", e->ip, (int)e->score, (long long)e->last_seen) < 0)
			{
write_fail:
				config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
				fclose(fd);
				return 0;
			}
		}
	}

	if (fclose(fd) < 0)
	{
		config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
		return 0;
	}

	/* Everything went fine. We rename our temporary file to the existing
	 * DB file (will overwrite), which is more or less an atomic operation.
	 */
#ifdef _WIN32
	/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
	unlink(cfg.database);
#endif
	if (rename(tmpfname, cfg.database) < 0)
	{
		config_error("ERROR renaming '%s' to '%s': %s -- DATABASE *NOT* SAVED!!!",
			tmpfname, cfg.database, strerror(ERRNO));
		return 0;
	}

	reputation_writtentime = TStime();

#ifdef BENCHMARK
	gettimeofday(&tv_beta, NULL);
	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
	           "Reputation benchmark: SAVE DB: $time_msec microseconds",
	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif

	return 1;
}

int reputation_save_db(void)
{
	UnrealDB *db;
	char tmpfname[512];
	int i;
	uint64_t count;
	ReputationEntry *e;
#ifdef BENCHMARK
	struct timeval tv_alpha, tv_beta;

	gettimeofday(&tv_alpha, NULL);
#endif

#ifdef TEST
	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_TEST", NULL, "Reputation in running in test mode. Saving DB's....");
#endif

	/* Comment this out after one or more releases (means you cannot downgrade to <=5.0.9.1 anymore) */
	if (cfg.db_secret == NULL)
		return reputation_save_db_old();

	/* We write to a temporary file. Only to rename it later if everything was ok */
	snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());

	db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
	if (!db)
	{
		WARN_WRITE_ERROR(tmpfname);
		return 0;
	}

	/* Write header */
	W_SAFE(unrealdb_write_int64(db, 2)); /* reputation db version */
	W_SAFE(unrealdb_write_int64(db, reputation_starttime)); /* starttime of data gathering */
	W_SAFE(unrealdb_write_int64(db, TStime())); /* current time */

	/* Count entries */
	count = 0;
	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
		for (e = ReputationHashTable[i]; e; e = e->next)
			count++;
	W_SAFE(unrealdb_write_int64(db, count)); /* Number of DB entries */

	/* Now write the actual individual entries: */
	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
	{
		for (e = ReputationHashTable[i]; e; e = e->next)
		{
			W_SAFE(unrealdb_write_str(db, e->ip));
			W_SAFE(unrealdb_write_int16(db, e->score));
			W_SAFE(unrealdb_write_int64(db, e->last_seen));
		}
	}

	if (!unrealdb_close(db))
	{
		WARN_WRITE_ERROR(tmpfname);
		return 0;
	}

	/* Everything went fine. We rename our temporary file to the existing
	 * DB file (will overwrite), which is more or less an atomic operation.
	 */
#ifdef _WIN32
	/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
	unlink(cfg.database);
#endif
	if (rename(tmpfname, cfg.database) < 0)
	{
		config_error("ERROR renaming '%s' to '%s': %s -- DATABASE *NOT* SAVED!!!",
			tmpfname, cfg.database, strerror(ERRNO));
		return 0;
	}

	reputation_writtentime = TStime();

#ifdef BENCHMARK
	gettimeofday(&tv_beta, NULL);
	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
	           "Reputation benchmark: SAVE DB: $time_msec microseconds",
	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
	return 1;
}

static uint64_t hash_reputation_entry(const char *ip)
{
	return siphash(ip, siphashkey_reputation) % REPUTATION_HASH_TABLE_SIZE;
}

void add_reputation_entry(ReputationEntry *e)
{
	int hashv = hash_reputation_entry(e->ip);

	AddListItem(e, ReputationHashTable[hashv]);
}

ReputationEntry *find_reputation_entry(const char *ip)
{
	ReputationEntry *e;
	int hashv = hash_reputation_entry(ip);

	for (e = ReputationHashTable[hashv]; e; e = e->next)
		if (!strcmp(e->ip, ip))
			return e;

	return NULL;
}

int reputation_lookup_score_and_set(Client *client)
{
	char *ip = client->ip;
	ReputationEntry *e;

	Reputation(client) = 0; /* (re-)set to zero (yes, important!) */
	if (ip)
	{
		e = find_reputation_entry(ip);
		if (e)
		{
			Reputation(client) = e->score; /* SET MODDATA */
		}
	}
	return Reputation(client);
}

/** Called when the user connects.
 * Locally: very early, just after the TCP/IP connection has
 * been established, before any data.
 * Remote user: early in the HOOKTYPE_REMOTE_CONNECT hook.
 */
int reputation_set_on_connect(Client *client)
{
	reputation_lookup_score_and_set(client);
	return 0;
}

int reputation_ip_change(Client *client, const char *oldip)
{
	reputation_lookup_score_and_set(client);
	return 0;
}

int reputation_pre_lconnect(Client *client)
{
	/* User will likely be accepted. Inform other servers about the score
	 * we have for this user. For more information about this type of
	 * server to server traffic, see the reputation_server_cmd function.
	 *
	 * Note that we use reputation_lookup_score_and_set() here
	 * and not Reputation(client) because we want to RE-LOOKUP
	 * the score for the IP in the database. We do this because
	 * between reputation_set_on_connect() and reputation_pre_lconnect()
	 * the IP of the user may have been changed due to IP-spoofing
	 * (WEBIRC).
	 */
	int score = reputation_lookup_score_and_set(client);

	sendto_server(NULL, 0, 0, NULL, ":%s REPUTATION %s %d", me.id, GetIP(client), score);

	return 0;
}

EVENT(add_scores)
{
	static int marker = 0;
	char *ip;
	Client *client;
	ReputationEntry *e;

	/* This marker is used so we only bump score for an IP entry
	 * once and not twice (or more) if there are multiple users
	 * with the same IP address.
	 */
	marker += 2;

	/* These macros make the code below easier to read. Also,
	 * this explains why we just did marker+=2 and not marker++.
	 */
	#define MARKER_UNREGISTERED_USER (marker)
	#define MARKER_REGISTERED_USER (marker+1)

	list_for_each_entry(client, &client_list, client_node)
	{
		if (!IsUser(client))
			continue; /* skip servers, unknowns, etc.. */

		ip = client->ip;
		if (!ip)
			continue;

		/* Enforce set::reputation::score-bump-timer-minimum-channel-members.
		 * Simply skip all the bumping and setting if this requirement is not met.
		 */
		if (cfg.require_minimum_channel_members > 0)
		{
			if (highest_channel_member_count(client) < cfg.require_minimum_channel_members)
				continue;
		}

		e = find_reputation_entry(ip);
		if (!e)
		{
			/* Create */
			e = safe_alloc(sizeof(ReputationEntry)+strlen(ip)+1);
			strcpy(e->ip, ip); /* safe, allocated above */
			add_reputation_entry(e);
		}

		/* If this is not a duplicate entry, then bump the score.. */
		if ((e->marker != MARKER_UNREGISTERED_USER) && (e->marker != MARKER_REGISTERED_USER))
		{
			e->marker = MARKER_UNREGISTERED_USER;
			if (e->score < REPUTATION_SCORE_CAP)
			{
				/* Regular users receive a point. */
				e->score++;
				/* Registered users receive an additional point */
				if (IsLoggedIn(client) && (e->score < REPUTATION_SCORE_CAP))
				{
					e->score++;
					e->marker = MARKER_REGISTERED_USER;
				}
			}
		} else
		if ((e->marker == MARKER_UNREGISTERED_USER) && IsLoggedIn(client) && (e->score < REPUTATION_SCORE_CAP))
		{
			/* This is to catch a special case:
			 * If there are 2 or more users with the same IP
			 * address and the first user was not registered
			 * then the IP entry only received a score bump of +1.
			 * If the 2nd user (with same IP) is a registered
			 * user then the IP should actually receive a
			 * score bump of +2 (in total).
			 */
			e->score++;
			e->marker = MARKER_REGISTERED_USER;
		}

		e->last_seen = TStime();
		Reputation(client) = e->score; /* update moddata */

		/* Possible transition to known-users:
		 * - logged in is already handled by HOOKTYPE_ACCOUNT_LOGIN so we don't care about those
		 * - score reached (or just over) the minimum reputation score
		 * Caveat: if having multiple connections from the same IP then
		 * the first one may theoretically not have crossed in some cases.
		 * Ah well, it is a cache, not some precise thingy.
		 */
		if (!IsLoggedIn(client) && known_users &&
		    ((e->score == known_users->reputation_score) || (e->score == known_users->reputation_score+1)))
		{
			update_known_user_cache(client);
		}
	}
}

/** Is this entry expired? */
static inline int is_reputation_expired(ReputationEntry *e)
{
	int i;
	for (i = 0; i < MAXEXPIRES; i++)
	{
		if (cfg.expire_time[i] == 0)
			break; /* end of all entries */
		if (cfg.expire_score[i] == -1)
		{
			/* For -1 it means ANY score will expire after this time.. */
			if (TStime() - e->last_seen > cfg.expire_time[i])
				return 1;
		} else {
			/* Otherwise we only expire if the score is less than <X> */
			if ((e->score <= cfg.expire_score[i]) && (TStime() - e->last_seen > cfg.expire_time[i]))
				return 1;
		}
	}
	return 0;
}

/** If the reputation changed (due to server syncing) then update the
 * individual users reputation score as well.
 */
void reputation_changed_update_users(ReputationEntry *e)
{
	Client *client;

	list_for_each_entry(client, &client_list, client_node)
	{
		if (client->ip && !strcmp(e->ip, client->ip))
			Reputation(client) = e->score;
	}
	list_for_each_entry(client, &unknown_list, lclient_node)
	{
		if (client->ip && !strcmp(e->ip, client->ip))
			Reputation(client) = e->score;
	}
}

EVENT(delete_old_records)
{
	int i;
	ReputationEntry *e, *e_next;
#ifdef BENCHMARK
	struct timeval tv_alpha, tv_beta;

	gettimeofday(&tv_alpha, NULL);
#endif

	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
	{
		for (e = ReputationHashTable[i]; e; e = e_next)
		{
			e_next = e->next;

			if (is_reputation_expired(e))
			{
#ifdef DEBUGMODE
				unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_EXPIRY", NULL,
				           "Deleting expired entry for $ip (score $score, last seen $time_delta seconds ago)",
				           log_data_string("ip", e->ip),
				           log_data_integer("score", e->score),
				           log_data_integer("time_delta", TStime() - e->last_seen));
#endif
				DelListItem(e, ReputationHashTable[i]);
				safe_free(e);
			}
		}
	}

#ifdef BENCHMARK
	gettimeofday(&tv_beta, NULL);
	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
	           "Reputation benchmark: EXPIRY IN MEM: $time_msec microseconds",
	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
}

EVENT(reputation_save_db_evt)
{
	reputation_save_db();
}

CMD_FUNC(reputationunperm)
{
	if (!IsOper(client))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	ModuleSetOptions(ModInf.handle, MOD_OPT_PERM, 0);

	unreal_log(ULOG_INFO, "reputation", "REPUTATIONUNPERM_COMMAND", client,
	           "$client used /REPUTATIONUNPERM. On next REHASH the module can be RELOADED or UNLOADED. "
	           "Note however that for a few minutes the scoring may be skipped, so don't do this too often.");
}

int reputation_connect_extinfo(Client *client, NameValuePrioList **list)
{
	add_fmt_nvplist(list, 0, "reputation", "%d", GetReputation(client));
	return 0;
}

int count_reputation_records(void)
{
	int i;
	ReputationEntry *e;
	int total = 0;

	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
		for (e = ReputationHashTable[i]; e; e = e->next)
			total++;

	return total;
}

void reputation_channel_query(Client *client, Channel *channel)
{
	Member *m;
	char buf[512];
	char tbuf[256];
	char **nicks;
	int *scores;
	int cnt = 0, i, j;
	ReputationEntry *e;

	sendtxtnumeric(client, "Users and reputation scores for %s:", channel->name);

	/* Step 1: build a list of nicks and their reputation */
	nicks = safe_alloc((channel->users+1) * sizeof(char *));
	scores = safe_alloc((channel->users+1) * sizeof(int));
	for (m = channel->members; m; m = m->next)
	{
		nicks[cnt] = m->client->name;
		if (m->client->ip)
		{
			e = find_reputation_entry(m->client->ip);
			if (e)
				scores[cnt] = e->score;
		}
		if (++cnt > channel->users)
		{
			unreal_log(ULOG_WARNING, "bug", "REPUTATION_CHANNEL_QUERY_BUG", client,
				   "[BUG] reputation_channel_query() expected $expected_users users, but $found_users (or more) users were present in $channel",
				   log_data_integer("expected_users", channel->users),
				   log_data_integer("found_users", cnt),
				   log_data_string("channel", channel->name));
#ifdef DEBUGMODE
			abort();
#endif
			break; /* safety net */
		}
	}

	/* Step 2: lazy selection sort */
	for (i = 0; i < cnt && nicks[i]; i++)
	{
		for (j = i+1; j < cnt && nicks[j]; j++)
		{
			if (scores[i] < scores[j])
			{
				char *nick_tmp;
				int score_tmp;
				nick_tmp = nicks[i];
				score_tmp = scores[i];
				nicks[i] = nicks[j];
				scores[i] = scores[j];
				nicks[j] = nick_tmp;
				scores[j] = score_tmp;
			}
		}
	}

	/* Step 3: send the (ordered) list to the user */
	*buf = '\0';
	for (i = 0; i < cnt && nicks[i]; i++)
	{
		snprintf(tbuf, sizeof(tbuf), "%s\00314(%d)\003 ", nicks[i], scores[i]);
		if ((strlen(tbuf)+strlen(buf) > 400) || !nicks[i+1])
		{
			sendtxtnumeric(client, "%s%s", buf, tbuf);
			*buf = '\0';
		} else {
			strlcat(buf, tbuf, sizeof(buf));
		}
	}
	sendtxtnumeric(client, "End of list.");
	safe_free(nicks);
	safe_free(scores);
}

void reputation_list_query(Client *client, int maxscore)
{
	Client *target;
	ReputationEntry *e;

	sendtxtnumeric(client, "Users and reputation scores <%d:", maxscore);

	list_for_each_entry(target, &client_list, client_node)
	{
		int score = 0;

		if (!IsUser(target) || IsULine(target) || !target->ip)
			continue;

		e = find_reputation_entry(target->ip);
		if (e)
			score = e->score;
		if (score >= maxscore)
			continue;
		sendtxtnumeric(client, "%s!%s@%s [%s] \017(score: %d)",
			target->name,
			target->user->username,
			target->user->realhost,
			target->ip,
			score);
	}
	sendtxtnumeric(client, "End of list.");
}

CMD_FUNC(reputation_user_cmd)
{
	ReputationEntry *e;
	const char *ip;

	if (!IsOper(client))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if ((parc < 2) || BadPtr(parv[1]))
	{
		sendnotice(client, "Reputation module statistics:");
		sendnotice(client, "Recording for: %lld seconds (since unixtime %lld)",
			(long long)(TStime() - reputation_starttime),
			(long long)reputation_starttime);
		if (reputation_writtentime)
		{
			sendnotice(client, "Last successful db write: %lld seconds ago (unixtime %lld)",
				(long long)(TStime() - reputation_writtentime),
				(long long)reputation_writtentime);
		} else {
			sendnotice(client, "Last successful db write: never");
		}
		sendnotice(client, "Current number of records (IP's): %d", count_reputation_records());
		sendnotice(client, "-");
		sendnotice(client, "Available commands:");
		sendnotice(client, "/REPUTATION <nick>          Show reputation info about nick name");
		sendnotice(client, "/REPUTATION <nick> <value>  Adjust the reputation score of the IP address of nick to <value>");
		sendnotice(client, "/REPUTATION <ip>            Show reputation info about IP address");
		sendnotice(client, "/REPUTATION <ip> <value>    Adjust the reputation score of the IP address to <value>");
		sendnotice(client, "/REPUTATION <channel>       List users in channel along with their reputation score");
		sendnotice(client, "/REPUTATION <NN             List users with reputation score below value NN");
		return;
	}

	if (strchr(parv[1], '.') || strchr(parv[1], ':'))
	{
		ip = parv[1];
	} else
	if (parv[1][0] == '#')
	{
		Channel *channel = find_channel(parv[1]);
		if (!channel)
		{
			sendnumeric(client, ERR_NOSUCHCHANNEL, parv[1]);
			return;
		}
		/* corner case: ircop without proper permissions and not in channel */
		if (!ValidatePermissionsForPath("channel:see:names:invisible",client,NULL,NULL,NULL) && !IsMember(client,channel))
		{
			sendnumeric(client, ERR_NOTONCHANNEL, channel->name);
			return;
		}
		reputation_channel_query(client, channel);
		return;
	} else
	if (parv[1][0] == '<')
	{
		int max = atoi(parv[1] + 1);
		if (max < 1)
		{
			sendnotice(client, "REPUTATION: Invalid search value specified. Use for example '/REPUTATION <5' to search on less-than-five");
			return;
		}
		reputation_list_query(client, max);
		return;
	} else {
		Client *target = find_user(parv[1], NULL);
		if (!target)
		{
			sendnumeric(client, ERR_NOSUCHNICK, parv[1]);
			return;
		}
		ip = target->ip;
		if (!ip)
		{
			sendnotice(client, "No IP address information available for user '%s'.", parv[1]); /* e.g. services */
			return;
		}
	}

	e = find_reputation_entry(ip);
	if (!e)
	{
		sendnotice(client, "No reputation record found for IP %s", ip);
		return;
	}

	if ((parc > 2) && !BadPtr(parv[2]))
	{
		/* Request to change value */
		if (!ValidatePermissionsForPath("client:set:reputation",client,NULL,NULL,NULL))
		{
			sendnumeric(client, ERR_NOPRIVILEGES);
			return;
		}
		int v = atoi(parv[2]);
		if (v > REPUTATION_SCORE_CAP)
			v = REPUTATION_SCORE_CAP;
		if (v < 0)
			v = 0;
		e->score = v;
		reputation_changed_update_users(e);
		sendto_server(&me, 0, 0, NULL,
			      ":%s REPUTATION %s *%d*",
			      me.id,
			      e->ip,
			      e->score);
		sendnotice(client, "Reputation of IP %s set to %hd", e->ip, e->score);
		return;
	}

	sendnotice(client, "****************************************************");
	sendnotice(client, "Reputation record for IP %s:", ip);
	sendnotice(client, "    Score: %hd", e->score);
	sendnotice(client, "Last seen: %lld seconds ago (unixtime: %lld)",
		(long long)(TStime() - e->last_seen),
		(long long)e->last_seen);
	sendnotice(client, "****************************************************");
}

/** The REPUTATION server command handler.
 * Syntax: :server REPUTATION <ip> <score>
 * Where the <score> may be prefixed by an asterisk (*).
 *
 * The best way to explain this command is to illustrate by example:
 * :servera REPUTATION 1.2.3.4 0
 * Then serverb, which might have a score of 2 for this IP, will:
 * - Send back to the servera direction:  :serverb REPUTATION 1.2.3.4 *2
 *   So the original server (and direction) receive a score update.
 * - Propagate to non-servera direction: :servera REPUTATION 1.2.3.4 2
 *   So use the new higher score (2 rather than 0).
 * Then the next server may do the same. It MUST propagate to non-serverb
 * direction and MAY (again) update the score even higher.
 *
 * If the score is not prefixed by * then the server may do as above and
 * send back to the uplink an "update" of the score. If, however, the
 * score is prefixed by * then the server will NEVER send back to the
 * uplink, it may only propagate. This is to prevent loops.
 *
 * Since UnrealIRCd 6.0.2+ there is now also asterisk-score-asterisk:
 * :server REPUTATION 1.2.3.4 *2*
 * The leading asterisk means no reply will be sent back, ever, and the
 * trailing asterisk will mean it is a "FORCED SET", which means that
 * servers should set the reputation to that value, even if it is lower.
 * This way reputation can be reduced and the reducation can be synced
 * across servers, which was not possible before 6.0.2.
 *
 * Note that some margin is used when deciding if the server should send
 * back score updates. This is defined by UPDATE_SCORE_MARGIN.
 * If this is for example set to 1 then a point difference of 1 will not
 * yield a score update since such a minor score update is not worth the
 * server to server traffic. Also, due to timing differences a score
 * difference of 1 is quite likely to hapen in normal circumstances.
 */
CMD_FUNC(reputation_server_cmd)
{
	ReputationEntry *e;
	const char *ip;
	int score;
	int allow_reply;
	int forced = 0;

	/* :server REPUTATION <ip> <score> */
	if ((parc < 3) || BadPtr(parv[2]))
	{
		sendnumeric(client, ERR_NEEDMOREPARAMS, "REPUTATION");
		return;
	}

	ip = parv[1];

	if (parv[2][0] == '*')
	{
		allow_reply = 0;
		score = atoi(parv[2]+1);
		if (parv[2][1] && (parv[2][strlen(parv[2])-1] == '*'))
			forced = 1;
	} else {
		allow_reply = 1;
		score = atoi(parv[2]);
	}

	if (score > REPUTATION_SCORE_CAP)
		score = REPUTATION_SCORE_CAP;
	if (score < 0)
		score = 0;

	e = find_reputation_entry(ip);
	if (allow_reply && e && (e->score > score) && (e->score - score > UPDATE_SCORE_MARGIN))
	{
		/* We have a higher score, inform the client direction about it.
		 * This will prefix the score with a * so servers will never reply to it.
		 */
		sendto_one(client, NULL, ":%s REPUTATION %s *%d", me.id, parv[1], e->score);
#ifdef DEBUGMODE
		unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_DIFFERS", client,
			   "Reputation score for for $ip from $client is $their_score, but we have $score, sending back $score",
			   log_data_string("ip", ip),
			   log_data_integer("their_score", score),
			   log_data_integer("score", e->score));
#endif
		score = e->score; /* Update for propagation in the non-client direction */
	}

	/* Update our score if sender has a higher score */
	if (e && (score > e->score))
	{
#ifdef DEBUGMODE
		unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_DIFFERS", client,
			   "Reputation score for for $ip from $client is $their_score, but we have $score, updating our score to $score",
			   log_data_string("ip", ip),
			   log_data_integer("their_score", score),
			   log_data_integer("score", e->score));
#endif
		e->score = score;
		reputation_changed_update_users(e);
	} else
	if (e && forced)
	{
#ifdef DEBUGMODE
		unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_DECREASE", client,
			   "Reputation score for for $ip from $client is $score, force-setting to $their_score.",
			   log_data_string("ip", ip),
			   log_data_integer("their_score", score),
			   log_data_integer("score", e->score));
#endif
		e->score = score;
		reputation_changed_update_users(e);
	}

	/* If we don't have any entry for this IP, add it now. */
	if (!e && (score > 0))
	{
#ifdef DEBUGMODE
		unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_NEW", client,
			   "Reputation score for for $ip from $client is $their_score, we had no entry, adding it",
			   log_data_string("ip", ip),
			   log_data_integer("their_score", score),
			   log_data_integer("score", 0));
#endif
		e = safe_alloc(sizeof(ReputationEntry)+strlen(ip)+1);
		strcpy(e->ip, ip); /* safe, see alloc above */
		e->score = score;
		e->last_seen = TStime();
		add_reputation_entry(e);
		reputation_changed_update_users(e);
	}

	/* Propagate to the non-client direction (score may be updated) */
	sendto_server(client, 0, 0, NULL,
	              ":%s REPUTATION %s %s%d%s",
	              client->id,
	              parv[1],
	              allow_reply ? "" : "*",
	              score,
	              forced ? "*" : "");
}

CMD_FUNC(reputation_cmd)
{
	if (MyUser(client))
		CALL_CMD_FUNC(reputation_user_cmd);
	else if (IsServer(client) || IsMe(client))
		CALL_CMD_FUNC(reputation_server_cmd);
}

int reputation_whois(Client *client, Client *target, NameValuePrioList **list)
{
	int reputation;

	if (whois_get_policy(client, target, "reputation") != WHOIS_CONFIG_DETAILS_FULL)
		return 0;

	reputation = Reputation(target);
	if (reputation > 0)
	{
		add_nvplist_numeric_fmt(list, 0, "reputation", client, RPL_WHOISSPECIAL,
		                        "%s :is using an IP with a reputation score of %d",
		                        target->name, reputation);
	}
	return 0;
}

void reputation_md_free(ModData *m)
{
	/* we have nothing to free actually, but we must set to zero */
	m->l = 0;
}

const char *reputation_md_serialize(ModData *m)
{
	static char buf[32];
	if (m->i == 0)
		return NULL; /* not set (reputation always starts at 1) */
	snprintf(buf, sizeof(buf), "%d", m->i);
	return buf;
}

void reputation_md_unserialize(const char *str, ModData *m)
{
	m->i = atoi(str);
}

int reputation_starttime_callback(void)
{
	/* NB: fix this by 2038 */
	return (int)reputation_starttime;
}

void _ban_act_set_reputation(Client *client, BanAction *action)
{
	ReputationEntry *e;
	int value;

	if ((client->ip == NULL) || IsDead(client))
		return; /* Wait, this is impossible, right? */

	/* There's a problem with adjusting reputation scores
	 * when a user is in "unknown state", especially when decreasing
	 * it, which is the common case here.
	 * This is because we might be a new server with not much
	 * reputation scores in the database, so giving everyone a 0,
	 * this is automatically fixed post-connect by other servers
	 * syncing in normally.
	 * However, if we allow setting/decreasing reputation here in
	 * this function, before the adjustment happens, then we may be
	 * decreasing/setting to a very low value (because we think
	 * the user has 0 rep) while in fact the user has high rep
	 * and should still have high rep (just a little lower than before).
	 * Instead of breaking my head on this, let's just not deal with
	 * unknown users for now. Can always enhance this later.
	 */
	if (!IsUser(client))
		return;

	e = find_reputation_entry(client->ip);
	if (!e)
	{
		/* Create */
		e = safe_alloc(sizeof(ReputationEntry)+strlen(client->ip)+1);
		strcpy(e->ip, client->ip); /* safe, allocated above */
		add_reputation_entry(e);
	}

	value = e->score;

	switch (action->var_action)
	{
		case VAR_ACT_SET:
			value = action->value;
			break;
		case VAR_ACT_INCREASE:
			value += action->value;
			if (value > REPUTATION_SCORE_CAP)
				value = REPUTATION_SCORE_CAP;
			break;
		case VAR_ACT_DECREASE:
			value -= action->value;
			if (value < 0)
				value = 0;
			break;
		default:
			abort();
	}

	/* Nothing changed? Then we are done. */
	if (e->score == value)
		return;

	e->score = value;
	reputation_changed_update_users(e);
	sendto_server(&me, 0, 0, NULL,
	              ":%s REPUTATION %s *%d*",
	              me.id,
	              e->ip,
	              e->score);
}
