/**********************************************************************
 * PTlink IRC Services is (C) CopyRight PTlink IRC Software 1999-2006 *
 *                     http://software.pt-link.net                    *
 * This program is distributed under GNU Public License               *
 * Please read the file COPYING for copyright information.            *
 **********************************************************************

  Description: chanserv module

*/

#include "chanserv.h"
#include "module.h"
#include "chanrecord.h"
#include "my_sql.h"
#include "dbconf.h"
#include "lang/common.lh"
#include "lang/chanserv.lh"

/* module, version, description */
SVS_Module mod_info =
{"chanserv", "5.1", "chanserv core module" };

#define DB_VERSION	4

/* ChangeLog
  5.1 - #65: Fixed Chan Drop & BotServ
  5.0 - #26, Added chanserv suspensions (DB_VERSION = 4)
  4.1 - 0000321: HelpChan option to set +h when ops join the help chan
  4.0 - 0000320: chanserv is not updating memory records during nick delete
        0000317: ChanServ doesn't keep on the logchan when RestrictedChans is enabled
        0000311: non registered nicks can bypass topiclock
        0000305: foreign keys for data integrity
        Added chanserv.3.sql changes
        0000302: Add dconf option for DefaultMlock          
  3.0 - 0000265: remove nickserv cache system
        0000261: mlock option
  2.2 - 0000258: chanserv topiclock option
  2.1 - sdata is now set/reset also for local users join/parts
  2.0 - Added chanserv.2.sql changes
        0000237: RestrictedChans option (to match the ircd option)
  */
            


/** functions/events we require **/
/* void (*FunctionPointer)(void);*/
int mysql_connection;
int e_nick_delete;
int e_expire;
int irc;

MOD_REQUIRES
  MOD_FUNC(irc)
  MOD_FUNC(dbconf_get_or_build)  
  MOD_FUNC(mysql_connection)
  MOD_FUNC(e_nick_delete)
  MOD_FUNC(e_expire)
MOD_END

/* functions we provide */
ServiceUser* chanserv_suser(void);
int e_regchan_join;
int e_chan_register;
int e_chan_delete;

MOD_PROVIDES                                                                                
  MOD_FUNC(chanserv_suser)
  MOD_FUNC(e_regchan_join)    
  MOD_FUNC(e_chan_register)
  MOD_FUNC(e_chan_delete)
MOD_END

/* core events */
void ev_cs_chan_join(IRC_Chan *chan , IRC_ChanNode *cnode);
void ev_cs_chan_part(IRC_Chan *chan , IRC_User *user);
void ev_cs_chan_topic(IRC_Chan *chan , IRC_User *user);
void ev_cs_deop(IRC_Chan *chan, IRC_User *user);
int ev_cs_nick_delete(int* snid, void *dummy);
int ev_cs_expire(void* dummy1, void* dummy2);
void ev_cs_new_server(IRC_Server* nserver, IRC_Server* from);
int sql_upgrade(int version, int post);

/* commands */
void cs_unknown(IRC_User* s, IRC_User* t);

/** Local config */
static char* Nick;
static char* Username;
static char* Hostname;
static char* Realname;
static char* LogChan;
static int ExpireTime;
static int MaxChansPerUser;
static int RestrictedChans;
static int NeedsAuth;
static char* DefaultMlock;

DBCONF_PROVIDES
  DBCONF_WORD(Nick,     "ChanServ", "Chanserv service nick")
  DBCONF_WORD(Username, "Services", "Chanserv service username")
  DBCONF_WORD(Hostname, "PTlink.net", "Chanserv service hostname")
  DBCONF_STR(Realname, 	"Chanserv Service", "Chanserv service real name")
  DBCONF_WORD_OPT(LogChan,  "#Services.log", "Chanserv log channel")
  DBCONF_TIME(ExpireTime,"15d", 
    "How long a channel will be kept without being used")
  DBCONF_INT(MaxChansPerUser, "20", "How many channels an user can register")
  DBCONF_SWITCH(RestrictedChans, "off", 
    "When services connect chanserv joins and leaves for all registered chans")
  DBCONF_SWITCH(NeedsAuth, "off",
    "Chanserv commands requires the nick to be authenticated")
  DBCONF_STR_OPT(DefaultMlock, "+nt", 
    "Default mlock to be set on new registered channels")
DBCONF_END
/* Local variables */
ServiceUser csu;
int cs_log;

/* this is called before load and at services rehash */
int mod_rehash(void)
{
  if(dbconf_get_or_build(mod_info.name, dbconf_provides) < 0 )
  {
    errlog("Error reading dbconf!");
    return -1;
  }
  return 0;
}

int mod_load(void)
{

  if(ExpireTime> 0 && ExpireTime < 86400)
    {
      errlog("ExpireTime is too low, minimum is 1 day!");
      return -6;
    }
    
  /* first try to open the log file */
  cs_log = log_open("chanserv","chanserv");
  
  if(cs_log<0)
    {
      errlog("Could not open chanserv log file!");
      return -1;
    }

  if(sql_check_inst_upgrade(mod_info.name, DB_VERSION, sql_upgrade) < 0)
    return -4;

  /* Create chanserv user */      
  csu.u = irc_CreateLocalUser(Nick, Username, Hostname, Hostname,
    Realname,"+ro");
  
  if(LogChan)
  {
    IRC_Chan *chan;
    log_set_irc(cs_log, Nick, LogChan);
    chan = irc_ChanJoin(csu.u, LogChan, CU_MODE_ADMIN|CU_MODE_OP);
    irc_ChanMode(csu.u, chan, "+Ostn");
  }
        
  irc_AddUMsgEvent(csu.u, "*", cs_unknown); /* any other msg handler */

  /* Add user events */
  irc_AddEvent(ET_CHAN_JOIN, ev_cs_chan_join);
  irc_AddEvent(ET_CHAN_PART, ev_cs_chan_part);
  irc_AddEvent(ET_CHAN_TOPIC, ev_cs_chan_topic);
/*  irc_AddCmodeChange("-o", ev_cs_deop); */
  
  /* reset chanserv status */
  sql_query("UPDATE chanserv SET status=0 WHERE status<>0");
  
  /* setup delete routines */
  mod_add_event_action(e_nick_delete, (ActionHandler) ev_cs_nick_delete);  
  mod_add_event_action(e_expire, (ActionHandler) ev_cs_expire);  
  
  if(RestrictedChans)
    irc_AddEvent(ET_NEW_SERVER, ev_cs_new_server);
    
  return 0;
}

void mod_unload(void)
{
  
  /* remove chanserv and all associated events */
  irc_QuitLocalUser(csu.u, "Removing service");

  /* delete events */
  irc_DelEvent(ET_CHAN_JOIN, ev_cs_chan_join);
  irc_DelEvent(ET_CHAN_PART, ev_cs_chan_part);  
  if(RestrictedChans)
    irc_DelEvent(ET_NEW_SERVER, ev_cs_new_server);
}
 
void cs_unknown(IRC_User* s, IRC_User* t)
{
  send_lang(t, s, UNKNOWN_COMMAND, irc_GetLastMsgCmd());
}

/* this is called after a join */
void ev_cs_chan_join(IRC_Chan* chan, IRC_ChanNode* cn)
{
  ChanRecord *cr;
  int remote_users;
  
  remote_users = chan->users_count - chan->lusers_count;
  cr = chan->sdata;
  if(cr == NULL)
    {
      cr = OpenCR(chan->name);
      chan->sdata = cr;
    }

  /* we don't do nothing for local users */
  if(irc_IsLocalUser(cn->user))
    return;
    
  /* first remote user joining the channel */
  if(remote_users == 1)
  {        
    /* first remote user joining a registered channel */    
    if(cr) 
    {
      irc_ChanMode(csu.u, chan, "+r");
      if(cr->mlock) /* we just set, the apply is called on next mode  */
        irc_ChanMLockSet(csu.u, chan, cr->mlock);
        
      if(irc_ConnectionStatus() == 2)
        send_lang(cn->user, csu.u, CHAN_X_IS_REGISTERED, chan->name);
      if(cr->last_topic_setter && cr->last_topic)
      {          
        if((chan->last_topic == NULL) || 
           (chan->last_topic && (strcmp(chan->last_topic, cr->last_topic)!=0)))
              irc_ChanTopic(chan, cr->last_topic_setter, cr->t_ltopic, cr->last_topic);
      }
    }
    else if(irc_ConnectionStatus() == 2)
      send_lang(cn->user, csu.u, CHAN_1ST_X_X_X, 
        cn->user->nick, chan->name, chan->name);    
  }  
  
  /* for all registered channels */
  if(cr)
    {
      if(irc_ConnectionStatus() == 2)
        {
          if(cr->url)
            irc_SendRaw(NULL, "328 %s %s : %s",
              cn->user->nick, cr->name, cr->url);
          if(cr->entrymsg)
            irc_SendNotice(cn->user, csu.u, "%s %s", 
              cr->name, cr->entrymsg);
        }
        
      /* update maxusers if required */
      if(remote_users > cr->maxusers) /* new record*/
        {
          cr->maxusers = remote_users;
          cr->t_maxusers = irc_CurrentTime;
          UpdateCR(cr);
        }        
      /* call the event */  
      mod_do_event(e_regchan_join, cr, cn);
    }
}

/* this is called when a user parts  */
void ev_cs_chan_part(IRC_Chan* chan, IRC_User *user)
{  
  if(chan->users_count == 1 && chan->sdata)
  {
    CloseCR(chan->sdata);  
    chan->sdata = NULL;
  }
}

void ev_cs_deop(IRC_Chan *chan, IRC_User *user)
{
 /* send_lang(user, csu.u, CHAN_DEOP, user->nick, chan->name); */
}

/* to return the nickserv client */
ServiceUser* chanserv_suser(void)
{
  return &csu;
} 

/* this is called when a nick is deleted */
int ev_cs_nick_delete(int *snid, void *dummy)
{
  MYSQL_RES* res;
  MYSQL_ROW row;
  ChanRecord* cr;
  
  /* remove the nick from successors */
  res = sql_query("SELECT scid, name FROM chanserv WHERE successor=%d", *snid);
  while((row = sql_next_row(res)))
  {  
    u_int32_t scid = atoi(row[0]);
    IRC_Chan *chan;
    log_log(cs_log, mod_info.name, "Removing successor on %s (was %d)",
      row[1], *snid);
    chan = irc_FindChan(row[1]);
    if(chan && (cr == chan->sdata))
      cr->successor = 0;
    sql_execute("UPDATE chanserv SET successor=NULL"
      " WHERE scid=%d", scid);
  }  
  sql_free(res);
  
  /* transfer owned channel to successors */
  res = sql_query("SELECT scid, name, successor FROM chanserv WHERE founder=%d"
    " AND successor IS NOT NULL", *snid);
  while((row = sql_next_row(res)))
  {
    u_int32_t scid = atoi(row[0]);
    IRC_Chan *chan;
    log_log(cs_log, mod_info.name, "Transfering channel %s (from %d to %s)",
      row[1], *snid, row[2]);
    chan = irc_FindChan(row[1]);
    if(chan && (cr == chan->sdata))
    {    
      cr->founder = cr->successor;
      cr->successor = 0;
    }
    sql_execute("UPDATE chanserv SET founder=successor, successor=NULL"
      " WHERE scid=%d", scid);
  }
  sql_free(res);
/*  
  sql_execute("UPDATE chanserv SET founder=successor, successor=NULL WHERE founder=%d"
    " AND successor IS NOT NULL", *snid);
*/
  /* now delete remaining owned channels */
  res = sql_query("SELECT scid, name FROM chanserv WHERE founder=%d",
    *snid);
    
  while((row = sql_next_row(res)))
  {
    u_int32_t scid = atoi(row[0]);
    IRC_Chan *chan;
    sql_execute("DELETE FROM chanserv WHERE scid=%d", scid);
    chan = irc_FindChan(row[1]);    
    if(chan && chan->sdata)
    {
      irc_ChanMode(csu.u, chan, "-r");
      CloseCR(chan->sdata);
      chan->sdata = NULL;
      if(chan->local_user)
        irc_ChanPart(chan->local_user, chan);
    }          
    log_log(cs_log, mod_info.name, "Deleted channel %d, %s from deleted nick %d",
      scid, row[1], *snid);
    mod_do_event(e_chan_delete, &scid, NULL);
  }   
  
  sql_free(res);
  return 0;
}

void ev_cs_chan_topic(IRC_Chan *chan , IRC_User *user)
{
  ChanRecord* cr = chan->sdata;
  u_int32_t snid = 0;
  if(user)
    snid = user->snid;
    
  if(cr)
  {
    if(IsTopicLock(cr) && ((snid==0) || (snid != cr->founder)))
    {
      if(cr->last_topic_setter && cr->last_topic)
      {          
        if((chan->last_topic == NULL) || 
          (chan->last_topic && (strcmp(chan->last_topic, cr->last_topic)!=0)))
            irc_ChanTopic(chan, cr->last_topic_setter, cr->t_ltopic, cr->last_topic);
      }
    }
    else
    {
      FREE(cr->last_topic);
      SDUP(cr->last_topic, chan->last_topic);
      FREE(cr->last_topic_setter);
      SDUP(cr->last_topic_setter, chan->last_topic_setter);  
      UpdateCR(cr);
    }
  }  
}

/*
 * Expires chans
 */
int ev_cs_expire(void* dummy1, void* dummy2)
{
  time_t expire_start;
  MYSQL_RES* res;
  MYSQL_ROW row;
  int rowc = 0;

  /* expire code goes here */
  res = sql_query ("SELECT scid, name FROM chanserv WHERE "
    "(flags & %d = 0) AND t_last_use < %d",
    CFL_NOEXPIRE, time(NULL)-ExpireTime);
  if(res)
    rowc = mysql_num_rows(res);
    
  if(rowc == 0)
    {
      sql_free(res);
      return 0;
    }

  log_log(cs_log, mod_info.name, "Will expire %d chan(s)", rowc);
  expire_start = time(NULL);
 
  rowc = 0; 
  /* fetch the data */
  while((row = sql_next_row(res)))
    {
      IRC_Chan* chan;
      u_int32_t scid = atoi(row[0]);
      rowc++;
      chan = irc_FindChan(row[1]);
      if(chan && chan->sdata)
        {
          irc_ChanMode(csu.u, chan, "-r");
          CloseCR(chan->sdata);
          chan->sdata = NULL;
          if(chan->local_user)
            irc_ChanPart(chan->local_user, chan);
        }        
      log_log(cs_log, mod_info.name, "Expiring scid %d,  %s", scid, row[1]);
      /* first call related actions */
      mod_do_event(e_chan_delete, &scid, NULL);
      /* now really delete it */
      sql_execute("DELETE FROM chanserv WHERE scid=%d", scid);
    }
  
  sql_free(res);
  log_log(cs_log, mod_info.name, "Expired %d channel(s), took %s",
    rowc, str_time(time(NULL) - expire_start));

  /* expire suspensions */
  res = sql_query("SELECT scid FROM chanserv_suspensions "
    "WHERE duration>0 AND t_when+duration<%d", irc_CurrentTime);
  while((row = sql_next_row(res)))
  {
    u_int32_t scid = atoi(row[0]);
    log_log(cs_log, mod_info.name, "Expiring chan suspension for %d",
      scid);
    sql_execute("DELETE FROM chanserv_suspensions WHERE scid=%d", scid);
    sql_execute("UPDATE chanserv SET flags = (flags & ~%d)"
      " WHERE scid=%d", CFL_SUSPENDED, scid);
/*
    sql_execute("UPDATE chanserv SET flags = (flags & ~%d), t_expire=%d"
      " WHERE snid=%d", CFL_SUSPENDED, irc_CurrentTime+ExpireTime, snid);
*/
  }
  sql_free(res);    
  return 0;
}

void ev_cs_new_server(IRC_Server* nserver, IRC_Server *from)
{
  static int already_loaded = 0;
  MYSQL_RES* res;
  MYSQL_ROW row;
  
  if(already_loaded)
    return;

  res = sql_query("SELECT name FROM chanserv");
  
  while((row = sql_next_row(res)))
    {
      IRC_Chan *chan = irc_FindChan(row[0]);
      if(chan)
        continue;
      chan = irc_ChanJoin(csu.u, row[0], 0);
      irc_ChanMode(csu.u, chan, "+r");
      irc_ChanPart (csu.u, chan);
    }
    
  sql_free(res);
  already_loaded = -1;   
}

/* this version takes care of sql upgrades */
int sql_upgrade(int version, int post)
{
  MYSQL_RES* res;
  MYSQL_ROW row;
  
  switch(version)
  {
    case 3: /* pre-validation of integrity rules */
    if(!post) /* we need to check for "lost" memos */
    {
      int rowc = 0;
      res = sql_query("SELECT"
        " chanserv.successor, chanserv.name, chanserv.scid FROM chanserv"
        " LEFT JOIN nickserv ON (chanserv.successor = nickserv.snid)"
        " WHERE chanserv.successor IS NOT NULL AND nickserv.snid IS NULL");
      while((row = sql_next_row(res)))
      {
        if(atoi(row[0]) == 0) /* this will be set to null */
          continue;
        log_log(cs_log, mod_info.name, "Removing lost successor %s on %s",
          row[0], row[1]);
        sql_execute("UPDATE chanserv SET successor=NULL WHERE scid=%s",
          row[2]);
        ++rowc;
      }
      if(rowc)
        log_log(cs_log, mod_info.name, "Removed %d lost successor(s)", rowc);
      sql_free(res);
      rowc = 0;
      res = sql_query("SELECT"
        " chanserv.founder, chanserv.name, chanserv.successor,"
        " chanserv.scid FROM chanserv"
        " LEFT JOIN nickserv ON (chanserv.founder = nickserv.snid)"
        " WHERE chanserv.successor IS NOT NULL AND nickserv.snid IS NULL");
      while((row = sql_next_row(res)))
      {
        if(atoi(row[0]) == 0) /* this will be set to null */
          continue;
        if(row[2] && atoi(row[2]) != 0)
        {
          log_log(cs_log, mod_info.name, 
            "Transfering lost channel %s to %s", row[1], row[2]);
          sql_execute("UPDATE chanserv SET founder=%s, successor=NULL"
            " WHERE scid=%s", row[2], row[3]);
        }
        else
        {
          log_log(cs_log, mod_info.name, 
            "Deleting lost channel %s, founder was %s", row[3], row[0]);
          sql_execute("DELETE FROM chanserv WHERE scid=%s",
            row[3]);
          ++rowc;
        }
      }
      if(rowc)
        log_log(cs_log, mod_info.name, "Deleted %d lost channels(s)", rowc);
      sql_free(res);            
    }
  }    
  return 1;
}



syntax highlighted by Code2HTML, v. 0.9.1