/* +------------------------------------+
* | Inspire Internet Relay Chat Daemon |
* +------------------------------------+
*
* InspIRCd: (C) 2002-2011 InspIRCd Development Team
* See: http://wiki.inspircd.org/Credits
*
* This program is free but copyrighted software; see
* the file COPYING for details.
*
* ---------------------------------------------------
*/
#include "inspircd.h"
#include "protocol.h"
/******************************************************************************
* Flat-file database read/write
******************************************************************************/
/* structure for single entry */
struct EntryDescriptor
{
std::string name, ts, topicset, topicsetby, topic, modes;
};
/** reads the channel database and returns structure with it */
class DatabaseReader
{
/* the entry descriptor */
EntryDescriptor entry;
/* file */
FILE *fd;
public:
/* constructor, opens the file */
DatabaseReader (std::string filename)
{
/* initialize */
fd = NULL;
/* open the file */
fd = fopen (filename.c_str ( ), "r");
/* if we can't open the file, return. */
if (!fd) return;
}
/* destructor will close the file */
~DatabaseReader ( )
{
/* if fd is not null, close it */
if (fd) fclose (fd);
}
/* get next entry */
EntryDescriptor *next ( )
{
/* if fd is NULL, fake eof */
if (!fd) return 0;
/* clear data */
entry.name = "";
entry.ts = "";
entry.topicset = "";
entry.topicsetby = "";
entry.modes = "";
entry.topic = "";
std::string str;
/* read single characters from the file and add them to the string, end at EOF or \n, ignore \r */
while (1)
{
int c = fgetc (fd);
if ((c == EOF) || ((unsigned char)c == '\n')) break;
if ((unsigned char)c == '\r') continue;
str.push_back ((unsigned char)c);
}
/* ready to parse the line */
if (str == "") return 0;
std::string token;
irc::spacesepstream sep(str, false);
/* get first one */
/* malformed if it is not chaninfo */
if (!sep.GetToken (token) || token != "chaninfo")
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed channel database");
return 0;
}
/* okay, read channel name */
/* name was get, but if it's the last token, database is malformed */
if (!sep.GetToken (token))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed channel database");
return 0;
}
/* save name */
entry.name = token;
/* set channel timestamp */
sep.GetToken (entry.ts);
/* initial entry read, read next lines in a loop until end, eof means malformed database again */
while (1)
{
str.clear ( );
/* read the line */
while (1)
{
int c = fgetc (fd);
if (c == EOF)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed channel database");
return 0;
}
unsigned char c2 = (unsigned char)c;
if (c2 == '\n') break;
if (c2 == '\r') continue;
str.push_back (c2);
}
irc::spacesepstream sep2(str, false);
/* get the token */
if (str == "")
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed channel database");
return 0;
}
sep2.GetToken (token);
/* break the loop if token is "end" */
if (token == "end") break;
/* it is not, so the large if statement there */
else if (token == "topic")
{
/* topic info, if end then it is malformed */
if (sep2.StreamEnd ( ))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed topic declaration in channel database for channel %s", entry.name.c_str ( ));
}
/* if not, then read topic set time */
/* if end then malformed */
if (!sep2.GetToken (token))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed topic declaration in channel database for channel %s", entry.name.c_str ( ));
}
/* save that */
entry.topicset = token;
/* get next token */
/* if last then malformed */
if (!sep2.GetToken (token))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed topic declaration in channel database for channel %s", entry.name.c_str ( ));
}
entry.topicsetby = token;
/* get rest of the declaration into the topic string, this is a topic */
entry.topic = sep2.GetRemaining ( );
} else if (token == "modes")
{
/* modes token, used for representing modes, it's the easier one, just load remaining data */
if (sep2.StreamEnd ( ))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed modes declaration in channel database for channel %s", entry.name.c_str ( ));
continue;
}
entry.modes = sep2.GetRemaining ( );
}
}
/* return entry address */
return &entry;
}
};
/* class being a database writer, gets a database file name on construct */
class DatabaseWriter
{
/* file stream */
FILE *fd;
std::string dbname, tmpname;
/* public */
public:
/* constructor */
DatabaseWriter (std::string filename)
{
fd = NULL;
dbname = filename;
tmpname = filename + ".tmp";
/* ready, open temporary database */
fd = fopen (tmpname.c_str ( ), "w");
if (!fd)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "cannot save to the channel database");
}
}
/* destructor */
~DatabaseWriter ( )
{
if (fd)
{
/* saving has been ended, close the database and flush buffers */
fclose (fd);
/* rename the database file */
if (rename (tmpname.c_str ( ), dbname.c_str ( )) == -1)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "can't rename the database file");
}
}
}
/* save the single channel */
void next (Channel *chan)
{
if (!fd) return;
/* first, construct the chaninfo line */
std::string line;
line.append ("chaninfo ").append (chan->name).append (" ").append (ConvToStr(chan->age)).append ("\n");
if (fputs (line.c_str ( ), fd) == EOF)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "unable to write the channel entry");
fclose (fd);
fd = NULL;
return;
}
/* now, write the topic if present */
if (!chan->topic.empty ( ))
{
line.clear ( );
line.append ("topic ").append (ConvToStr (chan->topicset)).append (" ").append (chan->setby).append (" ").append (chan->topic).append ("\n");
if (fputs (line.c_str ( ), fd) == EOF)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "unable to write channel's topic declaration");
fclose (fd);
fd = NULL;
return;
}
}
/* get all channel modes */
line.clear ( );
irc::modestacker ms;
chan->ChanModes (ms, MODELIST_FULL);
line.append ("modes ").append (ms.popModeLine (FORMAT_PERSIST, INT_MAX, INT_MAX)).append ("\nend\n");
if (fputs (line.c_str ( ), fd) == EOF)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "can't write mode declaration");
fclose (fd);
fd = NULL;
return;
}
}
};
class FlatFileChannelDB : public Module
{
private:
std::string filedb;
bool dirty; // filedb needs to be flushed to disk
bool storeregistered, storepermanent;
/** Whether or not to store a channel to the database */
bool ShouldStoreChannel(Channel* c)
{
return (storeregistered && c->IsModeSet("registered")) || (storepermanent && c->IsModeSet("permanent"));
}
void WriteFileDatabase ( )
{
// Dump entire database; open/close in constructor/destructor
DatabaseWriter db (filedb);
for (chan_hash::const_iterator i = ServerInstance->chanlist->begin(); i != ServerInstance->chanlist->end(); i++)
{
if (ShouldStoreChannel(i->second))
db.next(i->second);
}
}
/** Read flat-file database */
void ReadFileDatabase ( )
{
/* create the reader object and open the database */
DatabaseReader db (filedb);
/* start the database read loop */
EntryDescriptor *entry;
while ((entry = db.next ( )))
{
/* we get a database entry */
/* so if channel name doesn't start with #, give an error and continue */
if (entry->name[0] != '#')
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "invalid channel name in channel database");
continue;
}
/* entry is valid */
/* try to find the channel */
Channel *chan = ServerInstance->FindChan (entry->name);
time_t ourTS = atol (entry->ts.c_str ( ));
/* now, things that will be done depends on if channel was found or not */
if (!chan)
{
/* channel does not exist, allocate it, it will be constructed and added to the network as an empty channel */
chan = new Channel (entry->name, ourTS);
/* empty modeless channel added and allocated */
}
else if(chan->age > ourTS)
{
chan = Channel::Nuke(chan, entry->name, ourTS);
}
else if(chan->age < ourTS)
{
continue; // there's a channel older than the one we want to load, so we don't load it
}
/* so, channel exists in the network, then if our topic is newer than the current, set data
* if no topic was ever set, topicset is 0 */
time_t topicTS = atol (entry->topicset.c_str ( ));
if (topicTS && topicTS >= chan->topicset)
{
chan->topic = entry->topic;
chan->topicset = atol (entry->topicset.c_str ( ));
chan->setby = entry->topicsetby;
chan->WriteChannelWithServ(chan->setby, "TOPIC %s :%s", chan->name.c_str(), chan->topic.c_str());
}
/* if modestring is not empty, then set all modes in it */
if (!entry->modes.empty ( ))
{
/* create sepstream */
irc::spacesepstream sep(entry->modes);
/* create modestacker for stacking all mode changes */
irc::modestacker ms;
/* spacesepstream is used to iterate through all modes that were saved for a channel */
/* modestacker ms is used to stack them */
/* iterate through the mode list */
while (!sep.StreamEnd ( ))
{
/* get the mode to be applied */
std::string token, name, value;
sep.GetToken (token);
/* the mode looks like name=value unless it doesn't have any value then it's just name */
/* find the position of = sign */
size_t namepos = token.find ('=');
/* if we didn't find anything, set name to the token because mode is... valueless */
if (namepos == std::string::npos) name = token;
/* if it found that = sign, we have both name and value */
else
{
/* it found it, name is string before the character, value is the string after it */
name = token.substr (0, namepos);
value = token.substr (namepos + 1);
}
/* mode name and value found, we can now add it to the modestacker */
/* hmm, or we can't, reason is that letters are separate in user and channel modes, but mode names are not, so what if this thing we're setting is
an user mode? */
/* to check that, we must find the mode */
ModeHandler *mc = ServerInstance->Modes->FindMode (name);
/* those actions will be taken only if mode was found and if it's the channel mode */
if (mc && mc->GetModeType ( ) == MODETYPE_CHANNEL)
{
/* mode was found and is not an user mode but a channel mode, push it to the modestacker, we use id instead of name for quicker push because we
have the mode handler now */
ms.push (irc::modechange (mc->id, value));
}
}
/* okay, all modes were pushed, modestacker can now produce the mode line */
/* so, process all mode changes and apply them on a channel as a merge */
ServerInstance->Modes->Process (ServerInstance->FakeClient, chan, ms, true);
ServerInstance->Modes->Send (ServerInstance->FakeClient, chan, ms);
ServerInstance->PI->SendMode (ServerInstance->FakeClient, chan, ms, true);
}
}
}
public:
/* module constructor, for initializing stuff */
FlatFileChannelDB() : dirty(true)
{
}
/* get module version and flags. */
Version GetVersion()
{
return Version("Provides channel database storage in a flat-file format", VF_VENDOR);
}
void init ( )
{
Implementation eventlist[] = {
I_OnBackgroundTimer, I_OnMode, I_OnPostTopicChange
};
ServerInstance->Modules->Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation));
ReadFileDatabase();
}
/* rehash event */
void ReadConfig(ConfigReadStatus& status)
{
ConfigTag *tag = ServerInstance->Config->GetTag ("chandb");
filedb = tag->getString ("dbfile");
storeregistered = tag->getBool("storeregistered", true);
storepermanent = tag->getBool("storepermanent", false);
}
/* called on background timer to write all channels to disk if they were changed */
void OnBackgroundTimer (time_t cur)
{
/* if not dirty then don't do anything */
if (!dirty)
return;
/* dirty, one of registered channels was changed, save it */
WriteFileDatabase();
/* clear dirty to prevent next savings */
dirty = false;
}
/* after the topic has been changed, call this to check if channel was registered */
void OnPostTopicChange (User *user, Channel *chan, const std::string &topic)
{
if (ShouldStoreChannel(chan)) dirty = true;
}
/* this event is called after each modechange */
void OnMode (User *user, Extensible *target, const irc::modestacker &modes)
{
Channel* chan = IS_CHANNEL(target);
if (chan && ShouldStoreChannel(chan)) dirty = true;
}
void Prioritize()
{
// database reading may depend on channel modes being loaded
ServerInstance->Modules->SetPriority(this, I_ModuleInit, PRIORITY_LAST);
}
};
/* register the module */
MODULE_INIT(FlatFileChannelDB)