Kaynağa Gözat

Merge branch '21-botgroups'

* 21-botgroups: (31 commits)
  * Display unknown nodes as '(unknown)' and sort at top
  * Fix crash when there's no nodename
  * Sort cmd_bots output by nodename
  * Add '%group' support to 'bots' command.
  * Specify bot to cmd_groups to show groups for that bot
  * Fix cmd_channels showing 'inactive' on hub bots
  * Support group in cmd_channels
  * Const cleanup
  * Add command 'groups' to list all groups and which bots are in them.
  * Only match botcmd on non-hubs for groups
  * Remove unneeded botjoin/botpart
  * When overriding botset 'groups', auto include the global to avoid mistakingly making the bot part all channels immediately
  * Return the global set for default, then use the default
  * Fix groups not being 'main' if leaf had no local setting
  * Add botcmd support for groups
  * Update bdlib
  * Unpack all USERENTRY_SET regardless of being a hub
  * Share groups with all bots
  * Properly check the correct bots' groups in bot_shouldjoin
  * Remove obsolete code botshouldjoin()
  ...

Conflicts:
	doc/help.txt
Bryan Drewery 14 yıl önce
ebeveyn
işleme
3ed8d7dd36

+ 8 - 0
doc/UPDATES

@@ -18,6 +18,14 @@
   * Add cmd_newhub for adding a new hub. All bots will add this to their config.
   * Hubs can now be overridden inside the botconfig (-C)
   * Optimize userfile writing by doing it asynchronously
+  * Add [bot]set var 'groups' to configure what groups bots are in
+  * Add chanset 'groups' to take a list of groups that should join. 'chanset #chan groups { main backup }'
+  * Added group support to 'botcmd': 'botcmd %group cmd'
+  * 'botjoin' and 'botpart' have been removed. Just use .+chan, .chanset, .botset to control groups.
+  * Add command 'groups' to list all groups and which bots are in them.
+  * Command 'channels' now accepts a '%group' param to show which channels a group are in.
+  * Add '%group' support to 'bots' command.
+  * Improve output of 'bots' command by displaying and sorting nodename
 
 maint
   * Fix various compile warnings with newer GCC

+ 27 - 27
doc/help.txt

@@ -235,7 +235,7 @@ See also: -ban, +ban, console%{+m|m}, chanset, chaninfo%{-}, stick, unstick
    one. You can also specify a bot, and attempt to boot someone from another
    bot on the botnet. You can not boot a bot owner.
 :hub:botcmd:
-###  $bbotcmd$b <bot> <cmd> [params]
+###  $bbotcmd$b <bot|*|?|&|%%group> <cmd> [params]
    The specified cmd and optional parameters are executed on the specified bot, 
    all results are displayed back on DCC. For example:
      [19:29] #bryan# botcmd wtest whom ...
@@ -253,6 +253,7 @@ See also: -ban, +ban, console%{+m|m}, chanset, chaninfo%{-}, stick, unstick
  
    Just using '?' for 'bot' will choose a random leaf bot.
    Just using '&' for 'bot' will do the cmd on all localhub bots. (first bot in config).
+   Use '%%group' to match a specific group. (see also: '%dhelp set')
  
    This cmd cannot be chained over the botnet, ie, no: '%dbotcmd [bot] botcmd [bot2] ...'
  
@@ -261,15 +262,6 @@ See also: -ban, +ban, console%{+m|m}, chanset, chaninfo%{-}, stick, unstick
      bl -> botcmd ?
  
 See also: cmdpass
-::botjoin
-###  $bbotjoin$b <bot> <channel> [options]
-   Adds a channel to the bot's channel list. If options are specified, the 
-   channel will be configured with the options.
- 
-   $bTHIS CMD IS CURRENTLY EXPERIMENTAL AND WILL NOT ACTUALLY SAVE STATE$b
-   $bMEANING, ALL BOTS WILL JOIN EVENTUALLY AFTER DOING BOTJOIN$b
- 
-See also: botpart, +chan, -chan, chanset, chaninfo
 :hub:botjump:
 ###  $bbotjump$b <bot> [server [port [pass]]]
    Makes the bot jump to another server. If you don't specify a
@@ -291,13 +283,8 @@ See also: msg%{+n}
    The specified bot will display its current nick over DCC.
  
 See also: netnick, nick
-::botpart
-###  $bbotpart$b <bot> <channel>
-   Removes bot from the specified channel
- 
-See also: botjoin, +chan, -chan, chanset, chaninfo
 :hub:bots
-###  $bbots$b [nodename]
+###  $bbots$b [nodename|%group]
    Shows the list of bots currently on the botnet.
    Example:
       Bots: cEvin, ruthie, Killa1
@@ -305,9 +292,10 @@ See also: botjoin, +chan, -chan, chanset, chaninfo
    bot. %{+n}Use $b'%dwho'$b or $b'%dbottree'$b for that information.%{-}
  
    Specifying a nodename will display all bots up/down on that nodename.
-   Bots with a * preceeding its name is down.
+   Bots with a * preceeding its name are down.
+   Specifying a %%group will only show bots in that group.
  
-See also: downbots%{+n}, bottree%{-}
+See also: groups, downbots%{+n}, bottree%{-}
 :hub:botserver:
 ###  $bbotserver$b <bot>
    The bot will display its current server and lag over DCC.
@@ -338,7 +326,7 @@ See also: set
    Red: localhubs (first bot in binary, see '%dhelp conf')
    Other: normal leaf bots
  
-See also: bots, downbots
+See also: bots, downbots, groups
 ::botversion:
 ###  $bbotversion$b <bot>
    The bot will display its pack version and uname.
@@ -359,8 +347,12 @@ See also: link%{+a}, newhub%{-}
 ###  $bchaninfo$b <channel>
    This lists all the settings for the bot on the given channel.
    It shows any of the following:
-        $bchanmode$b   These modes are enforced on the channel. Both + and -
-                   modes can be enforced.
+        $bchanmode$b         These modes are enforced on the channel.
+                          Both + and - modes can be enforced. Use {} to
+                          add a key. '%dchanset #chan chanmode { +nt key }'
+        $bgroups$b            List of groups that should join the channel.
+                          Use {} to add multiple groups. See also '%dhelp set'
+                          '%dchanset #chan groups { main alt }'
 %{+m|m}
         $bauto-delay$b        Amount of seconds to wait before auto opping,
                               or auto voicing a client based on flags or
@@ -592,13 +584,14 @@ See also: %{-}%{+n}+chan, -chan%{-}%{+m|m}, chanset%{-}
  
 See also: status, whois
 ::channels:
-###  $bchannels$b %{+m}[user]%{-}
+###  $bchannels$b %{+m}[user|%group]%{-}
    Displays channels that you have access to, and any important 
    flags that are set on them.
 %{+m}
    Masters: You can specify other users to see what channels their 
             flags grant them access to.%{-}
-See also: whois%{+m}, chattr, chaninfo%{-}
+   Special %%group to see what channels a particular group is in.
+See also: whois%{+m}, chattr, chaninfo, groups%{-}
 ::chanset
 ###  $bchanset$b [#&!+channel|*|default] <settings>
    Allows you to change the channel settings (see $b'chaninfo'$b for the
@@ -649,8 +642,8 @@ See also: whois
 :hub:checkchannels
 ###  $bcheckchannels$b
    This will make all leaf bots display which channels they are currently
-   not in but *should* be in.  This command will not show channels which
-   the bot is not active in due to '$bbotpart$b'.
+   not in but *should* be in.  This will only shows channels that contain
+   the groups the bot are in.
  
 See also: botcmd, channels, channel, status
 :hub:chhandle
@@ -964,7 +957,7 @@ See also: cycle
    Example:
       Down bots: cEvin, ruthie, Killa1
  
-See also: bots
+See also: bots, groups
 :leaf:dump
 ###  $bdump$b <text>
    dumps the text to the server.  keep in mind that this bot doesn't
@@ -1061,6 +1054,13 @@ See also: -exempt, +exempt, console%{+m|m}, chanset, chaninfo%{-}, stick, unstic
    specified, your console channel is used.
  
 See also: console, channels%{+m}, status%{-}
+:hub:groups
+###  $bgroups$b [bot]
+   Shows the list of groups and which bots are in them.
+
+   Specify a bot to only show which groups it is in.
+ 
+See also: bots, downbots%{+n}, bottree%{-}
 ::handle
 ###  $bhandle$b <new-handle>
    Changes your handle on the bot. This is the handle (nickname) that the
@@ -1672,7 +1672,7 @@ See also: reload, backup
 [N]  $bmsgburst$b        How many messages to burst at once to server. (Too high will excess flood)
 [N]  $bmsgrate$b         How often (msecs) to dequeue msgs to the server. Only used on
                            non-ratbox servers. Too small a value can result in Excess Flood.
- 
+[L]  $bgroups$b          List of groups that the affected bots are a part of.
 [L]  $brbl-servers$b     Servers to use for RBL checking in channels that are +rbl.
  
 [S]  $brealname$b        The bot's "real name" when connecting. (supports '$n' expansion)

+ 82 - 46
src/botnet.c

@@ -49,6 +49,7 @@
 #include "core_binds.h"
 #include <bdlib/src/String.h>
 #include <bdlib/src/Array.h>
+#include <algorithm>
 
 tand_t			*tandbot = NULL;		/* Keep track of tandem bots on the
 							   botnet */
@@ -522,65 +523,100 @@ void answer_local_whom(int idx, int chan)
   dprintf(idx, "Total users: %d\n", total);
 }
 
+bool sortNodes(const bd::String nodeA, const bd::String nodeB) {
+  const bd::String unknown("(unknown)");
+  if (nodeA == unknown) {
+    return true;
+  } else if (nodeB == unknown) {
+    return false;
+  }
+  // Reverse the domains
+  const bd::Array<bd::String> partsA(nodeA.split("."));
+  const bd::Array<bd::String> partsB(nodeB.split("."));
+  bd::Array<bd::String> reversedPartsA, reversedPartsB;
+  bd::String reversedNodeA, reversedNodeB;
+
+  if (partsA.length()) {
+    for (size_t i = partsA.length() - 1; i > 0; --i) {
+      reversedPartsA << partsA[i - 1];
+    }
+  }
+  if (partsB.length()) {
+    for (size_t i = partsB.length() - 1; i > 0; --i) {
+      reversedPartsB << partsB[i - 1];
+    }
+  }
+  reversedNodeA = reversedPartsA.join(".");
+  reversedNodeB = reversedPartsB.join(".");
+  return reversedNodeA < reversedNodeB;
+}
+
 /* Show z a list of all bots connected
  */
 void
 tell_bots(int idx, int up, const char *nodename)
 {
-  struct userrec *u = NULL;
-  int cnt = 0, tot = 0, total = 1, mtot = 0;
-  char work[151] = "", *node = NULL;
-
-  if (up) {
-    if (nodename)
-      node = (char *) get_user(&USERENTRY_NODENAME, conf.bot->u);    
-    if (!nodename || wild_match(nodename, node)) {
-      strlcat(work, conf.bot->nick, sizeof(work));
-      strlcat(work, " ", sizeof(work));
-      cnt++;
-      tot++;
-      if (nodename)
-        mtot++;
-    }
+  size_t total = 0, maxNodeNameLength = 0;;
+  bd::Array<bd::String> nodes;
+  bd::HashTable<bd::String, bd::Array<bd::String> > nodeBots;
+  bd::Array<bd::String> bots;
+  bd::String group;
+
+  if (nodename && nodename[0] == '%') {
+    group = nodename + 1;
   }
 
-  for (u = userlist; u; u = u->next) {
+  // Gather a list of nodes and bots per node, as well as per domain
+  for (struct userrec* u = userlist; u; u = u->next) {
     if (u->bot) {
-      if (strcasecmp(u->handle, conf.bot->nick)) {
-        bool found = 0;
-        
-        if (findbot(u->handle))
-          found = 1;
-        total++;
-        if (nodename || (!nodename && ((!up && !found) || (up && found)))) {
-          if (nodename)
-            node = (char *) get_user(&USERENTRY_NODENAME, u);
-          if (!nodename || wild_match(nodename, node)) {
-            if (nodename && !found)
-              strlcat(work, "*", sizeof(work));
-            strlcat(work, u->handle, sizeof(work));
-            cnt++;
-            if (nodename)
-              mtot++;
-            if (!nodename || (nodename && found))
-              tot++;
-            if (cnt == 11) {
-              dprintf(idx, "%s bots: %s\n", nodename ? "Matching" : up ? "Up" : "Down", work);
-              work[0] = 0;
-              cnt = 0;
-            } else {
-              strlcat(work, " ", sizeof(work));
-            }
+      // If looking for groups, exclude hubs
+      if (group.length() && bot_hublevel(u) != 999) {
+        continue;
+      }
+      ++total;
+      bd::String botnick(u->handle);
+      const bd::Array<bd::String> botgroups(bd::String(var_get_bot_data(u, "groups", true)).split(","));
+
+      // Include this bot?
+      const bool group_match = group.length() && botgroups.find(group) != botgroups.npos;
+      const char *userNode = (const char*) get_user(&USERENTRY_NODENAME, u);
+      const bd::String node(userNode ? userNode : "(unknown)");
+      const bool node_match = ((nodename && node.length() && wild_match(nodename, node.c_str())) || !nodename);
+      const bool bot_found = findbot(u->handle);
+      const bool up_down_match = (nodename || (!nodename && ((up && bot_found) || (!up && !bot_found))));
+      if (group_match || (group.length() == 0 && node_match && up_down_match)) {
+        if (nodes.find(node) == nodes.npos) {
+          nodes << node;
+          if (node.length() > maxNodeNameLength) {
+            maxNodeNameLength = node.length();
           }
         }
+        if ((group.length() || nodename) && !bot_found) {
+          botnick = '*' + botnick;
+        }
+        nodeBots[node] << botnick;
+        bots << botnick;
       }
     }
   }
-  if (work[0])
-    dprintf(idx, "%s bot%s: %s\n", nodename ? "Matching" : up ? "Up" : "Down", cnt > 1 ? "s" : "", work);
-  if (nodename)
-    dprintf(idx, "(Total Matching: %d/%d)\n", mtot, total);
-  dprintf(idx, "(Total %s: %d/%d)\n", nodename ? "up" : up ? "up" : "down", tot, nodename ? mtot : total);
+
+  if (group.length() == 0 && !nodename) {
+    dumplots(idx, nodename ? "Matching: " : (up ? "Up: " : "Down: "), static_cast<bd::String>(bots.join(" ")).c_str());
+  } else {
+    // Sort by nodes
+    std::sort(nodes.begin(), nodes.end(), sortNodes);
+    for (size_t i = 0; i < nodes.length(); ++i) {
+      const bd::String node(nodes[i]);
+      const bd::Array<bd::String> botsInNode(nodeBots[node]);
+      dumplots(idx, bd::String::printf("%*s: ", int(maxNodeNameLength), node.c_str()).c_str(), static_cast<bd::String>(botsInNode.join(" ")).c_str());
+    }
+  }
+
+  if (nodename || group.length()) {
+    dprintf(idx, "(Total Matching: %zu/%zu)\n", bots.length(), total);
+  } else {
+    dprintf(idx, "(Total %s: %zu/%zu)\n", up ? "up" : "down", bots.length(), total);
+  }
 }
 
 /* Show a simpleton bot tree

+ 5 - 1
src/chan.h

@@ -8,6 +8,9 @@
 #ifndef _EGG_CHAN_H
 #define _EGG_CHAN_H
 
+#include <lib/bdlib/src/Array.h>
+#include <lib/bdlib/src/String.h>
+
 typedef struct memstruct {
   struct memstruct *next;
   struct userrec *user;
@@ -210,6 +213,7 @@ struct chanset_t {
   char pls[21];			/* positive mode changes		*/
   char mns[21];			/* negative mode changes		*/
   char key_prot[121];		/* desired password			*/
+  bd::Array<bd::String> *groups;/* groups that should join */
 /* Chanchar template
  *char temp[121];
  */
@@ -266,7 +270,7 @@ struct chanset_t {
 #define CHAN_STOP_CYCLE     BIT9	/* Some efnetservers have defined NO_CHANOPS_WHEN_SPLIT */
 
 /* prototypes */
-memberlist *ismember(struct chanset_t *, const char *);
+memberlist *ismember(const struct chanset_t *, const char *);
 struct chanset_t *findchan(const char *name);
 struct chanset_t *findchan_by_dname(const char *name);
 

+ 27 - 69
src/chanprog.c

@@ -45,6 +45,7 @@
 #include "userrec.h"
 #include "main.h"
 #include "debug.h"
+#include "set.h"
 #include "dccutil.h"
 #include "botmsg.h"
 #if HAVE_GETRUSAGE
@@ -54,8 +55,10 @@
 #endif
 #endif
 #include <sys/utsname.h>
+#include <bdlib/src/Array.h>
+#include <bdlib/src/String.h>
 
-char *def_chanset = "+enforcebans +dynamicbans +userbans -bitch +cycle -inactive +userexempts -dynamicexempts +userinvites -dynamicinvites -nodesynch -closed -take -voice -private -fastop +meankicks ban-type 3 protect-backup 1";
+char *def_chanset = "+enforcebans +dynamicbans +userbans -bitch +cycle -inactive +userexempts -dynamicexempts +userinvites -dynamicinvites -nodesynch -closed -take -voice -private -fastop +meankicks ban-type 3 protect-backup 1 groups { main }";
 struct chanset_t 	*chanset = NULL;	/* Channel list			*/
 struct chanset_t	*chanset_default = NULL;	/* Default channel list */
 char 			admin[121] = "";	/* Admin info			*/
@@ -92,7 +95,7 @@ void rmspace(char *s)
 
 /* Returns memberfields if the nick is in the member list.
  */
-memberlist *ismember(struct chanset_t *chan, const char *nick)
+memberlist *ismember(const struct chanset_t *chan, const char *nick)
 {
   register memberlist	*x = NULL;
 
@@ -740,69 +743,7 @@ int isowner(char *name)
   }
 }
 
-/* this method is slow, but is only called when sharing and adding/removing chans */
-
-int 
-botshouldjoin(struct userrec *u, struct chanset_t *chan)
-{
-  /* just return 1 for now */
-  return 1;
-/*
-  char *chans = NULL;
-  struct userrec *u = NULL;
- 
-  u = get_user_by_handle(userlist, bot);
-  if (!u)
-    return;
-  chans = get_user(&USERENTRY_CHANS, u);
-*/
-}
-/* future use ?
-void
-chans_addbot(const char *bot, struct chanset_t *chan)
-{
-  char *chans = NULL;
-  struct userrec *u = NULL;
- 
-  u = get_user_by_handle(userlist, bot);
-  if (!u)
-    return;
-  chans = get_user(&USERENTRY_CHANS, u);
-  if (!botshouldjoin(u, chan)) {		
-    size_t size;
-    char *buf = NULL;
-   
-    size = strlen(chans) + strlen(chan->dname) + 2;
-    buf = (char *) my_calloc(1, size);
-    simple_snprintf(buf, size, "%s %s", chans, chan->dname);
-    set_user(&USERENTRY_CHANS, u, buf);
-    free(buf);
-  }
-}
-
-void 
-chans_delbot(const char *bot, struct chanset_t *chan)
-{
-  char *chans = NULL;
-  struct userrec *u = NULL;
- 
-  u = get_user_by_handle(userlist, bot);
-  if (!u)
-    return;
- 
-  if (botshouldjoin(u, chan)) {			
-    char *chans = NULL, *buf = NULL;
-    size_t size;
-
-    chans = get_user(&USERENTRY_CHANS, u);
-    size = strlen(chans) - strlen(chan->dname) + 2;
-
-    
-  }
-}
-*/
-
-bool bot_shouldjoin(struct userrec* u, struct flag_record* fr, struct chanset_t* chan, bool ignore_inactive)
+bool bot_shouldjoin(struct userrec* u, struct flag_record* fr, const struct chanset_t* chan, bool ignore_inactive)
 {
   // If restarting, keep this channel.
   if (restarting && (reset_chans == 2) && (channel_active(chan) || channel_pending(chan))) return 1;
@@ -823,13 +764,30 @@ bool bot_shouldjoin(struct userrec* u, struct flag_record* fr, struct chanset_t*
       return 0;
   }
 #endif
+
+  // Is this bot in the groups that this channel has?
+  const char *botgroups = u == conf.bot->u ? groups : var_get_bot_data(u, "groups", true);
+  bd::Array<bd::String> my_groupsArray(bd::String(botgroups).split(','));
+  bool group_match = 0;
+
+  if (chan->groups && chan->groups->length()) {
+    for (size_t i = 0; i < my_groupsArray.length(); ++i) {
+      if (chan->groups->find(my_groupsArray[i]) != chan->groups->npos) {
+        group_match = 1;
+        break;
+      }
+    }
+  }
+
   // Ignore +inactive during cmd_slowjoin to ensure that +backup bots join
-  return (!glob_kick(*fr) && !chan_kick(*fr) &&
-      ((ignore_inactive || !channel_inactive(chan)) &&
-       (channel_backup(chan) || (!glob_backup(*fr) && !chan_backup(*fr)))));
+  return (!glob_kick(*fr) && !chan_kick(*fr) && // Not being kicked
+      ((ignore_inactive || !channel_inactive(chan)) && // Not inactive
+      ((channel_backup(chan) && (glob_backup(*fr) || chan_backup(*fr) || group_match)) || // Is +backup and I'm a backup bot or my group matches
+       (!channel_backup(chan) && !glob_backup(*fr) && !chan_backup(*fr) && group_match))) // is -backup and I am not a backup bot and my group matches
+      );
 }
 
-bool shouldjoin(struct chanset_t *chan)
+bool shouldjoin(const struct chanset_t *chan)
 {
   struct flag_record fr = { FR_CHAN|FR_GLOBAL|FR_BOT, 0, 0, 0 };
   get_user_flagrec(conf.bot->u, &fr, chan->dname, chan);

+ 2 - 3
src/chanprog.h

@@ -23,9 +23,8 @@ void rmspace(char *s);
 void set_chanlist(const char *host, struct userrec *rec);
 void clear_chanlist(void);
 void clear_chanlist_member(const char *nick);
-int botshouldjoin(struct userrec *u, struct chanset_t *);
-bool bot_shouldjoin(struct userrec* , struct flag_record *, struct chanset_t *, bool = 0);
-bool shouldjoin(struct chanset_t *);
+bool bot_shouldjoin(struct userrec* , struct flag_record *, const struct chanset_t *, bool = 0);
+bool shouldjoin(const struct chanset_t *);
 char *samechans(const char *, const char *);
 void add_myself_to_userlist();
 void add_child_bots();

+ 72 - 12
src/cmds.c

@@ -901,23 +901,74 @@ static void cmd_userlist(int idx, char *par)
   return;
 }
 
+static void cmd_groups(int idx, char *par)
+{
+  struct userrec *u = NULL;
+
+  putlog(LOG_CMDS, "*", "#%s# groups %s", dcc[idx].nick, par);
+  bd::String botnick(newsplit(&par));
+
+  bd::Array<bd::String> globalgroups = bd::String(var_get_gdata("groups")).split(",");
+  bd::Array<bd::String> allgroups;
+  bd::HashTable<bd::String, bd::Array<bd::String> > groupBots;
+  bd::HashTable<bd::String, bd::Array<bd::String> > botGroups;
+  size_t maxGroupLen = 0;
+
+  // Need to loop over every bot and make a list of all groups and bots which are in those groups
+  for (u = userlist; u; u = u->next) {
+    if (u->bot && bot_hublevel(u) == 999) {
+      // Gather all the groups for this bot
+      botGroups[u->handle] = bd::String(var_get_bot_data(u, "groups", true)).split(",");
+      for (size_t i = 0; i < botGroups[u->handle].length(); ++i) {
+        const bd::String group(botGroups[u->handle][i]);
+        if (group.length() > maxGroupLen) {
+          maxGroupLen = group.length();
+        }
+        // Add their groups into the master list
+        if (allgroups.find(group) == allgroups.npos) {
+          allgroups << group;
+        }
+        // Add them to the list for this group
+        groupBots[group] << u->handle;
+
+      }
+    }
+  }
+
+  if (botnick.length()) {
+    dprintf(idx, "%s is in groups: %s\n", botnick.c_str(), static_cast<bd::String>(botGroups[botnick].join(" ")).c_str());
+  } else {
+    // Display all groups and which bots are in them
+    for (size_t i = 0; i < allgroups.length(); ++i) {
+      const bd::String group(allgroups[i]);
+      const bd::Array<bd::String> bots(groupBots[group]);
+      dumplots(idx, bd::String::printf("%-*s: ", int(maxGroupLen), group.c_str()).c_str(), static_cast<bd::String>(bots.join(" ")).c_str());
+    }
+  }
+
+  dprintf(idx, "Total groups: %zu\n", allgroups.length());
+  return;
+}
+
 static void cmd_channels(int idx, char *par) {
   putlog(LOG_CMDS, "*", "#%s# channels %s", dcc[idx].nick, par);
   if (par[0] && (dcc[idx].user->flags & USER_MASTER)) {
-    struct userrec *user = NULL;
-
-    user = get_user_by_handle(userlist, par);
-    if (user && whois_access(dcc[idx].user, user)) {
+    if (par[0] == '%') {
       show_channels(idx, par);
-    } else  {
-      dprintf(idx, "No such user.\n");
+    } else {
+      struct userrec *user = get_user_by_handle(userlist, par);
+      if (user && whois_access(dcc[idx].user, user)) {
+        show_channels(idx, par);
+      } else  {
+        dprintf(idx, "No such user.\n");
+      }
     }
   } else {
       show_channels(idx, NULL);
   }
 
   if ((dcc[idx].user->flags & USER_MASTER) && !(par && par[0]))
-    dprintf(idx, "You can also %schannels <user>\n", (dcc[idx].u.chat->channel >= 0) ? settings.dcc_prefix : "");
+    dprintf(idx, "You can also %schannels <user|%%group>\n", (dcc[idx].u.chat->channel >= 0) ? settings.dcc_prefix : "");
 }
 
 
@@ -1335,7 +1386,7 @@ static void cmd_botcmd(int idx, char *par)
     cmd = newsplit(&par);
 
   if (!botm[0] || !cmd || (cmd && !cmd[0])) {
-    dprintf(idx, "Usage: botcmd <bot> <cmd> [params]\n");
+    dprintf(idx, "Usage: botcmd <bot|*|?|&|%%group> <cmd> [params]\n");
     return;
   }
 
@@ -1347,7 +1398,7 @@ static void cmd_botcmd(int idx, char *par)
     putlog(LOG_CMDS, "*", "#%s# botcmd %s %s ...", dcc[idx].nick, botm, cmd);
 
   // Restrict dangerous mass commands ('botcmd *' (any *) or 'botcmd &')
-  if ((strchr(botm, '*') && !findbot(botm)) || !strcmp(botm, "&")) {
+  if ((strchr(botm, '*') && !findbot(botm)) || !strcmp(botm, "&") || botm[0] == '%') {
     if (!strncasecmp(cmd, "di", 2) || (!strncasecmp(cmd, "res", 3) && strncasecmp(cmd, "reset", 5)) || !strncasecmp(cmd, "sui", 3) || !strncasecmp(cmd, "pl", 2) || !strncasecmp(cmd, "ac", 2) ||
         !strncasecmp(cmd, "j", 1) || (!strncasecmp(cmd, "dump", 4) && (!strncasecmp(par, "privmsg", 7) || !strncasecmp(par, "notice", 6) || !strncasecmp(par, "quit", 4)))) {
       dprintf(idx, "Not a good idea.\n");
@@ -1383,11 +1434,19 @@ static void cmd_botcmd(int idx, char *par)
   }
   
   for (tbot = tandbot; tbot; tbot = tbot->next) {
-    if ((rand_leaf && bot_hublevel(get_user_by_handle(userlist, tbot->bot)) != 999) ||
-        (all_localhub && (bot_hublevel(get_user_by_handle(userlist, tbot->bot)) != 999 || !tbot->localhub)))
+    struct userrec *botu = get_user_by_handle(userlist, tbot->bot);
+    if ((rand_leaf && bot_hublevel(botu) != 999) ||
+        (all_localhub && (bot_hublevel(botu) != 999 || !tbot->localhub)))
       continue;
     cnt++;
-    if ((rleaf != -1 && cnt == rleaf) || (rleaf == -1 && (all_localhub || wild_match(botm, tbot->bot)))) {
+    bool group_match = false;
+    if (botm[0] == '%' && bot_hublevel(botu) == 999) {
+      bd::String botgroups = var_get_bot_data(botu, "groups", true);
+      if (botgroups.split(',').find(botm + 1) != bd::String::npos) {
+        group_match = true;
+      }
+    }
+    if (group_match || (rleaf != -1 && cnt == rleaf) || (rleaf == -1 && (all_localhub || wild_match(botm, tbot->bot)))) {
       if (rleaf != -1)
         putlog(LOG_CMDS, "*", "#%s# botcmd %s %s ...", dcc[idx].nick, tbot->bot, cmd);
       send_remote_simul(idx, tbot->bot, cmd, par ? par : (char *) "");
@@ -4721,6 +4780,7 @@ cmd_t C_dcc[] =
   {"version", 		"o", 	(Function) cmd_version, 	NULL, 0},
   {"netversion", 	"o", 	(Function) cmd_netversion, 	NULL, HUB},
   {"userlist", 		"m", 	(Function) cmd_userlist, 	NULL, 0},
+  {"groups", 		"m", 	(Function) cmd_groups, 		NULL, HUB},
   {"ps", 		"n", 	(Function) cmd_ps, 		NULL, 0},
   {"last", 		"n", 	(Function) cmd_last, 		NULL, 0},
   {"exec", 		"a", 	(Function) cmd_exec, 		NULL, 0},

+ 1 - 1
src/flags.c

@@ -380,7 +380,7 @@ set_user_flagrec(struct userrec *u, struct flag_record *fr, const char *chname)
 /* Always pass the dname (display name) to this function for chname <cybah>
  */
 void
-get_user_flagrec(struct userrec *u, struct flag_record *fr, const char *chname, struct chanset_t* chan)
+get_user_flagrec(struct userrec *u, struct flag_record *fr, const char *chname, const struct chanset_t* chan)
 {
   fr->bot = 0;
   if (!u) {

+ 1 - 1
src/flags.h

@@ -142,7 +142,7 @@ struct flag_record {
 #define chan_doflood(x)				((x).chan & BOT_FLOODBOT)
 
 void init_flags(void);
-void get_user_flagrec(struct userrec *, struct flag_record *, const char *, struct chanset_t* = NULL);
+void get_user_flagrec(struct userrec *, struct flag_record *, const char *, const struct chanset_t* = NULL);
 void set_user_flagrec(struct userrec *, struct flag_record *, const char *);
 void break_down_flags(const char *, struct flag_record *, struct flag_record *);
 int build_flags(char *, struct flag_record *, struct flag_record *);

+ 44 - 27
src/misc.c

@@ -431,45 +431,62 @@ void show_motd(int idx)
 
 void show_channels(int idx, char *handle)
 {
-  struct chanset_t *chan = NULL;
-  struct flag_record fr = { FR_CHAN | FR_GLOBAL, 0, 0, 0 };
   struct userrec *u = NULL;
-  int first = 0, total = 0;
-  size_t l = 0;
-  char format[120] = "";
+  size_t maxChannelLength = 0;
+  bd::Array<bd::String> channelNames;
+  bd::HashTable<bd::String, struct chanset_t*> channels;
+  bd::String group;
 
-  if (handle)
+  if (handle && handle[0] != '%') {
     u = get_user_by_handle(userlist, handle);
-  else
+  } else {
     u = dcc[idx].user;
+    if (handle && handle[0] == '%') {
+      group = handle + 1;
+    }
+  }
 
-  for (chan = chanset;chan ;chan = chan->next) {
+  for (struct chanset_t* chan = chanset; chan; chan = chan->next) {
+    struct flag_record fr = { FR_CHAN | FR_GLOBAL, 0, 0, 0 };
+    const bd::String chname(chan->dname);
+    // If a group was passed, ensure it matches
+    if (group.length() && chan->groups->find(group) == chan->groups->npos) {
+      continue;
+    }
     get_user_flagrec(u, &fr, chan->dname);
-    if (l < strlen(chan->dname)) {
-      l = strlen(chan->dname);
+    if (group.length() || real_chk_op(fr, chan, 0)) {
+      if (maxChannelLength < chname.length()) {
+        maxChannelLength = chname.length();
+      }
+      channelNames << chname;
+      channels[chname] = chan;
     }
-    if (real_chk_op(fr, chan, 0))
-      total++;
   }
 
-  simple_snprintf(format, sizeof(format), "  %%c%%-%zus %%-s%%-s%%-s%%-s%%-s%%-s\n", (l+2));
+  if (channelNames.length()) {
+    char format[120] = "";
+    simple_snprintf(format, sizeof(format), "  %%c%%-%zus %%-s%%-s%%-s%%-s%%-s%%-s\n", (maxChannelLength+2));
+    if (group.length()) {
+      dprintf(idx, "group '%s' is in %zu channel%s:\n", group.c_str(), channelNames.length(), (channelNames.length() > 1) ? "s" : "");
+    } else {
+      dprintf(idx, "%s %s access to %zu channel%s:\n", handle ? u->handle : "You", handle ? "has" : "have", channelNames.length(), (channelNames.length() > 1) ? "s" : "");
+    }
 
-  for (chan = chanset;chan;chan = chan->next) {
-    get_user_flagrec(u, &fr, chan->dname);
-    if (real_chk_op(fr, chan, 0)) {
-        if (!first) { 
-          dprintf(idx, "%s %s access to %d channel%s:\n", handle ? u->handle : "You", handle ? "has" : "have", total, (total > 1) ? "s" : "");
-          
-          first = 1;
-        }
-        dprintf(idx, format, !conf.bot->hub && me_op(chan) ? '@' : ' ', chan->dname, !shouldjoin(chan) ? "(inactive) " : "", 
-           channel_privchan(chan) ? "(private)  " : "", chan->manop ? "(no manop) " : "", 
-           channel_bitch(chan) && !channel_botbitch(chan) ? "(bitch)    " : channel_botbitch(chan) ? "(botbitch) " : "",
-           channel_closed(chan) ?  "(closed) " : "", channel_backup(chan) ? "(backup)" : "");
+    for (size_t i = 0; i < channelNames.length(); ++i) {
+      const bd::String chname(channelNames[i]);
+      const struct chanset_t* chan = channels[chname];
+      dprintf(idx, format, !conf.bot->hub && me_op(chan) ? '@' : ' ', chan->dname, ((conf.bot->hub && channel_inactive(chan)) || (!conf.bot->hub && !shouldjoin(chan))) ? "(inactive) " : "",
+          channel_privchan(chan) ? "(private)  " : "", chan->manop ? "(no manop) " : "", 
+          channel_bitch(chan) && !channel_botbitch(chan) ? "(bitch)    " : channel_botbitch(chan) ? "(botbitch) " : "",
+          channel_closed(chan) ?  "(closed) " : "", channel_backup(chan) ? "(backup)" : "");
+    }
+  } else {
+    if (group.length()) {
+      dprintf(idx, "No channels found for group '%s'\n", group.c_str());
+    } else {
+      dprintf(idx, "%s %s not have access to any channels.\n", handle ? u->handle : "You", handle ? "does" : "do");
     }
   }
-  if (!first)
-    dprintf(idx, "%s %s not have access to any channels.\n", handle ? u->handle : "You", handle ? "does" : "do");
 }
 
 /* Create a string with random letters and digits

+ 20 - 7
src/mod/channels.mod/chanmisc.c

@@ -306,7 +306,7 @@ int SplitList(char *resultBuf, const char *list, int *argcPtr, const char ***arg
  */
 int channel_modify(char *result, struct chanset_t *chan, int items, char **item, bool cmd)
 {
-  bool error = 0;
+  bool error = 0, changed_groups = false;
   int old_status = chan->status,
       old_mode_mns_prot = chan->mode_mns_prot,
       old_mode_pls_prot = chan->mode_pls_prot;
@@ -336,6 +336,20 @@ int channel_modify(char *result, struct chanset_t *chan, int items, char **item,
       }
       strlcpy(s, item[i], sizeof(s));
       set_mode_protect(chan, s);
+    } else if (!strcmp(item[i], "groups")) {
+      i++;
+      if (i >= items) {
+	if (result)
+	  strlcpy(result, "channel groups needs argument", RESULT_LEN);
+	return ERROR;
+      }
+      // Get string into right format
+      bd::String changroups(item[i]);
+      // Replace commas with spaces to be in proper format
+      changroups.sub(",", " ");
+      changroups.trim();
+      *(chan->groups) = changroups.split(" ");
+      changed_groups = true;
     } else if (!strcmp(item[i], "topic")) {
       char *p = NULL;
 
@@ -814,12 +828,9 @@ int channel_modify(char *result, struct chanset_t *chan, int items, char **item,
     chan->status |= CHAN_BITCH;		// to avoid bots still mass opping from +take from not using cookies
 
   if (!conf.bot->hub && (chan != chanset_default)) {
-    if ((old_status ^ chan->status) & (CHAN_INACTIVE | CHAN_BACKUP)) {
-      if (!shouldjoin(chan) && (chan->ircnet_status & (CHAN_ACTIVE | CHAN_PEND))) {
-        putlog(LOG_DEBUG, "*", "In %s, but I shouldn't be, parting...", chan->dname);
-        dprintf(DP_SERVER, "PART %s\n", chan->name[0] ? chan->name : chan->dname);
-      } else if (shouldjoin(chan))
-        join_chan(chan);
+    // Check if groups changed or +/-backup set
+    if (!restarting && !loading && (changed_groups || ((old_status ^ chan->status) & (CHAN_INACTIVE | CHAN_BACKUP)))) {
+      check_shouldjoin(chan);
     }
     if (me_op(chan)) {
       if ((old_status ^ chan->status) & (CHAN_ENFORCEBANS|CHAN_NOUSERBANS|CHAN_DYNAMICBANS|CHAN_NOUSEREXEMPTS|CHAN_NOUSERINVITES|CHAN_DYNAMICEXEMPTS|CHAN_DYNAMICINVITES)) {
@@ -995,6 +1006,8 @@ int channel_add(char *result, const char *newname, char *options, bool isdefault
 /* Chanint template
  *  chan->temp = 0;
  */
+    chan->groups = new bd::Array<bd::String>;
+    *(chan->groups) << "main";
     chan->protect_backup = 1;
     chan->knock_flags = 0;
     chan->flood_lock_time = 120;

+ 3 - 0
src/mod/channels.mod/channels.c

@@ -721,6 +721,9 @@ void remove_channel(struct chanset_t *chan)
      free(chan->key);
    if (chan->rmkey)
      free(chan->rmkey);
+   if (chan->groups) {
+     delete(chan->groups);
+   }
    free(chan);
 }
 

+ 1 - 46
src/mod/channels.mod/cmdschan.c

@@ -1008,29 +1008,6 @@ static void cmd_pls_chan(int idx, char *par)
   pls_chan(idx, par, NULL);
 }
 
-static void cmd_botjoin(int idx, char *par)
-{
-  char *bot = NULL;
-  struct userrec *botu = NULL;
-
-  putlog(LOG_CMDS, "*", "#%s# botjoin %s", dcc[idx].nick, par);
-
-  if (!par[0]) {
-    dprintf(idx, "Usage: botjoin <bot> [%s]<channel> [options]\n", CHANMETA);
-    return;
-  }
-  bot = newsplit(&par);
-  botu = get_user_by_handle(userlist, bot);
-  if (botu && botu->bot) {
-    pls_chan(idx, par, bot);
-  } else {
-    dprintf(idx, "Error: '%s' is not a bot.\n", bot);
-  }
-
-  dprintf(idx, "!!!! Warning: botjoin is an unfinished feature which will pretty much make all bots join the channel.\n");
-  dprintf(idx, "!!!! Warning: You should probably -chan the channel now to avoid problems/confusion later.\n");
-}
-
 static void mns_chan(int idx, char *par, char *bot)
 {
   char *chname = NULL, buf2[1024] = "";
@@ -1084,27 +1061,6 @@ static void cmd_mns_chan(int idx, char *par)
   mns_chan(idx, par, NULL);
 }
 
-static void cmd_botpart(int idx, char *par)
-{
-  char *bot = NULL;
-  struct userrec *botu = NULL;
-
-  putlog(LOG_CMDS, "*", "#%s# botpart %s", dcc[idx].nick, par);
-  
-  if (!par[0]) {
-    dprintf(idx, "Usage: botpart <bot> [%s]<channel> [options]\n", CHANMETA);
-    return;
-  }
-
-  bot = newsplit(&par);
-  botu = get_user_by_handle(userlist, bot);
-  if (botu && botu->bot) {
-    mns_chan(idx, par, bot);
-  } else {
-    dprintf(idx, "Error: '%s' is not a bot.\n", bot);
-  }
-}
-
 /* thanks Excelsior */
 #define FLAG_COLS 4
 static void show_flag(int idx, char *work, int *cnt, const char *name, unsigned int state, size_t worksiz)
@@ -1228,6 +1184,7 @@ static void cmd_chaninfo(int idx, char *par)
 /* FIXME: SHOW_CHAR() here */
     get_mode_protect(chan, work, sizeof(work));
     dprintf(idx, "Protect modes (chanmode): %s\n", work[0] ? work : "None");
+    dprintf(idx, "Groups: %s\n", chan->groups && chan->groups->length() ? static_cast<bd::String>(chan->groups->join(" ")).c_str() : "None");
 //    dprintf(idx, "Protect topic (topic)   : %s\n", chan->topic[0] ? chan->topic : "");
 /* Chanchar template
  *  dprintf(idx, "String temp: %s\n", chan->temp[0] ? chan->temp : "NULL");
@@ -1419,8 +1376,6 @@ static cmd_t C_dcc_channels[] =
   {"-exempt",	"o|o",	(Function) cmd_mns_exempt,	NULL, AUTH},
   {"-invite",	"o|o",	(Function) cmd_mns_invite,	NULL, AUTH},
   {"bans",	"o|o",	(Function) cmd_bans,		NULL, 0},
-  {"botjoin",	"n",	(Function) cmd_botjoin,		NULL, 0},
-  {"botpart",	"n",	(Function) cmd_botpart,		NULL, 0},
   {"exempts",	"o|o",	(Function) cmd_exempts,		NULL, 0},
   {"invites",	"o|o",	(Function) cmd_invites,		NULL, 0},
   {"chaninfo",	"m|m",	(Function) cmd_chaninfo,	NULL, 0},

+ 3 - 10
src/mod/channels.mod/userchan.c

@@ -701,9 +701,6 @@ static void write_chan(bd::Stream& stream, int idx, struct chanset_t* chan)
   putlog(LOG_DEBUG, "*", "writing channel %s to userfile..", chan->dname);
 
   bool force_inactive = 0;
-  /* if a bot should explicitly NOT join, just set it +inactive ... */
-  if (idx >= 0 && !botshouldjoin(dcc[idx].user, chan))
-    force_inactive = 1;
 
   stream << bd::String::printf("+ channel add %s { ", chan->dname);
   if (chan != chanset_default)
@@ -717,7 +714,7 @@ bd::String channel_to_string(struct chanset_t* chan, bool force_inactive) {
 
   get_mode_protect(chan, w, sizeof(w));
   return bd::String::printf("\
-chanmode { %s } bad-cookie %d manop %d mdop %d mop %d limit %d ban-type %d \
+chanmode { %s } groups { %s } bad-cookie %d manop %d mdop %d mop %d limit %d ban-type %d \
 flood-chan %d:%d flood-ctcp %d:%d flood-join %d:%d \
 flood-kick %d:%d flood-deop %d:%d flood-nick %d:%d flood-mjoin %d:%d \
 closed-ban %d closed-invite %d closed-private %d ban-time %d \
@@ -732,6 +729,7 @@ flood-exempt %d flood-lock-time %d knock %d \
  *      temp,
  * also include temp %s in dprintf.
  */
+        chan->groups && chan->groups->length() ? static_cast<bd::String>(chan->groups->join(" ")).c_str() : "",
 	chan->bad_cookie,
 	chan->manop,
 	chan->mdop,
@@ -825,12 +823,7 @@ void write_chans_compat(bd::Stream& stream, int idx)
     putlog(LOG_DEBUG, "*", "writing channel %s to userfile..", chan->dname);
     get_mode_protect(chan, w, sizeof(w));
 
-    /* if a bot should explicitly NOT join, just set it +inactive ... */
-    if (idx >= 0 && !botshouldjoin(dcc[idx].user, chan))
-      inactive = '+';
-    /* ... otherwise give the bot the *actual* setting */
-    else
-      inactive = PLSMNS(channel_inactive(chan));
+    inactive = PLSMNS(channel_inactive(chan));
 
     stream << bd::String::printf("\
 + channel add %s { chanmode { %s } addedby %s addedts %li \

+ 15 - 8
src/mod/irc.mod/irc.c

@@ -1279,7 +1279,7 @@ killmember(struct chanset_t *chan, char *nick)
 /* Check if I am a chanop. Returns boolean 1 or 0.
  */
 bool
-me_op(struct chanset_t *chan)
+me_op(const struct chanset_t *chan)
 {
   memberlist *mx = ismember(chan, botname);
 
@@ -1294,7 +1294,7 @@ me_op(struct chanset_t *chan)
 /* Check whether I'm voice. Returns boolean 1 or 0.
  */
 bool
-me_voice(struct chanset_t *chan)
+me_voice(const struct chanset_t *chan)
 {
   memberlist *mx = ismember(chan, botname);
 
@@ -1553,13 +1553,21 @@ raise_limit(struct chanset_t *chan)
 
 }
 
-static void
-check_expired_chanstuff(struct chanset_t *chan)
+void check_shouldjoin(struct chanset_t* chan)
 {
   if ((channel_active(chan) || channel_pending(chan)) && !shouldjoin(chan)) {
     sdprintf("Active/Pending in %s but I shouldn't be there, parting...", chan->dname);
-    dprintf(DP_MODE, "PART %s\n", chan->name[0] ? chan->name : chan->dname);
-  } else if (channel_active(chan)) {
+    dprintf(DP_SERVER, "PART %s\n", chan->name[0] ? chan->name : chan->dname);
+  } else if (shouldjoin(chan)) {
+    join_chan(chan);
+  }
+}
+
+static void
+check_expired_chanstuff(struct chanset_t *chan)
+{
+  check_shouldjoin(chan);
+  if (channel_active(chan) && shouldjoin(chan)) {
     masklist *b = NULL, *e = NULL;
     memberlist *m = NULL, *n = NULL;
     char s[UHOSTLEN] = "";
@@ -1670,8 +1678,7 @@ check_expired_chanstuff(struct chanset_t *chan)
     // Update minutely
     chan->channel.splitmembers = splitmembers;
     check_lonely_channel(chan);
-  } else if (shouldjoin(chan))
-    join_chan(chan);
+  }
 }
 
 void

+ 3 - 2
src/mod/irc.mod/irc.h

@@ -117,8 +117,8 @@ void notice_invite(struct chanset_t *, char *, char *, char *, bool);
 void real_add_mode(struct chanset_t *, const char, const char, const char *, bool);
 #define add_mode(chan, pls, mode, nick) real_add_mode(chan, pls, mode, nick, 0)
 #define add_cookie(chan, nick) real_add_mode(chan, '+', 'o', nick, 1)
-bool me_op(struct chanset_t *);
-bool me_voice(struct chanset_t *);
+bool me_op(const struct chanset_t *);
+bool me_voice(const struct chanset_t *);
 
 void check_this_ban(struct chanset_t *, char *, bool);
 void check_this_exempt(struct chanset_t *, char *, bool);
@@ -145,6 +145,7 @@ void irc_report(int, int);
 void flush_modes();
 void reset_chan_info(struct chanset_t *);
 char *getnick(const char *, struct chanset_t *);
+void check_shouldjoin(struct chanset_t* chan);
 
 extern int		max_bans, max_exempts, max_invites, max_modes;
 extern bool		use_354, include_lk;

+ 27 - 3
src/set.c

@@ -14,6 +14,7 @@
 #include "misc.h"
 #include "net.h"
 #include "src/mod/server.mod/server.h"
+#include "src/mod/irc.mod/irc.h"
 #include "src/mod/channels.mod/channels.h"
 #include "src/mod/ctcp.mod/ctcp.h"
 #include "users.h"
@@ -30,6 +31,7 @@ static bool parsing_botset = 0;
 char altchars[50] = "";
 char alias[1024] = "";
 char rbl_servers[1024] = "";
+char groups[1024] = "";
 bool auth_chan;
 char auth_key[51] = "";
 char auth_prefix[2] = "";
@@ -94,6 +96,7 @@ static variable_t vars[] = {
  VAR("flood-ctcp",	&flood_ctcp,		VAR_RATE|VAR_NOLHUB,				0, 0, "3:60"),
  VAR("flood-msg",	&flood_msg,		VAR_RATE|VAR_NOLHUB,				0, 0, "5:60"),
  VAR("fork-interval",	&fork_interval,		VAR_INT,					10, 0, "0"),
+ VAR("groups",		groups,			VAR_STRING|VAR_LIST|VAR_NOLHUB,			0, 0, "main"),
  VAR("hijack",		&hijack,		VAR_INT|VAR_DETECTED|VAR_PERM,			0, 4, "die"),
  VAR("homechan",	homechan,		VAR_WORD|VAR_NOLOC|VAR_HIDE,			0, 0, NULL),
  VAR("ident-botnick",   &ident_botnick,		VAR_INT|VAR_BOOL|VAR_NOLHUB,			0, 1, "0"),
@@ -410,6 +413,15 @@ sdprintf("var (mem): %s -> %s", var->name, datain ? datain : "(NULL)");
     var_set_mem(servers, servers->ldata ? servers->ldata : servers->gdata ? servers->gdata : NULL);
   }
 
+  // Check if should part/join channels based on groups changing
+  if (!conf.bot->hub && !strcmp(var->name, "groups")) {
+    if (server_online && !restarting && !loading && !reset_chans) {
+      for (struct chanset_t* chan = chanset; chan; chan = chan->next) {
+        check_shouldjoin(chan);
+      }
+    }
+  }
+
   if (datap)
     free(datap);
 
@@ -516,6 +528,11 @@ static inline variable_t *var_get_var_by_name(const char *name)
   return (variable_t*) bsearch(&key, &vars, lengthof(vars) - 1, sizeof(variable_t), comp_variable_t);
 }
 
+const char *var_get_gdata(const char *name) {
+  variable_t* var = var_get_var_by_name(name);
+  return var && var->gdata ? var->gdata : NULL;
+}
+
 void var_set(variable_t *var, const char *target, const char *datain)
 {
   /* Don't set locally if the variable doesn't permit it. */
@@ -686,7 +703,7 @@ void var_userfile_share_line(char *line, int idx, bool share)
   set_noshare = 0;
 }
 
-static const char *var_get_bot_data(struct userrec *u, const char *name)
+const char *var_get_bot_data(struct userrec *u, const char *name, bool useDefault)
 {
   if (!u)
     return NULL;
@@ -697,7 +714,14 @@ static const char *var_get_bot_data(struct userrec *u, const char *name)
   while (xk && strcmp(xk->key, name))
     xk = xk->next;
 
-  return xk ? xk->data : NULL;
+  if (xk) {
+    return xk->data;
+  }
+  if (useDefault) {
+    variable_t *var = var_get_var_by_name(name);
+    return var->gdata ? var->gdata : var->def;
+  }
+  return NULL;
 }
 
 
@@ -796,7 +820,7 @@ static int var_add_list(const char *botnick, variable_t *var, const char *elemen
   char *data = NULL, *olddata = NULL, *botdata = NULL;
 
   if (botnick) {                          //fetch data from bot's USERENTRY_SET
-    botdata = (char *) var_get_bot_data(get_user_by_handle(userlist, (char *) botnick), var->name);
+    botdata = (char *) var_get_bot_data(get_user_by_handle(userlist, (char *) botnick), var->name, true);
     olddata = botdata ? botdata : NULL;
   } else                                  //use global, no bot specified
     olddata = var->gdata ? var->gdata : NULL;

+ 3 - 1
src/set.h

@@ -67,7 +67,7 @@ typedef struct rate_b {
  interval_t time;
 } rate_t;
 
-extern char		auth_key[], auth_prefix[2], motd[], alias[], rbl_servers[1024],
+extern char		auth_key[], auth_prefix[2], motd[], alias[], rbl_servers[1024], groups[1024],
 			msgident[], msginvite[], msgop[], msgpass[], msgrelease[],
                         homechan[], altchars[];
 extern bool		dccauth, auth_obscure, manop_warn, auth_chan, oidentd, ident_botnick, irc_autoaway, link_cleartext, use_deaf, use_callerid;
@@ -86,6 +86,8 @@ void var_parse_my_botset();
 void init_vars();
 void var_set_by_name(const char *, const char *, const char *);
 void var_set_userentry(const char *, const char *, const char *);
+const char *var_get_bot_data(struct userrec *u, const char *name, bool useDefault = false);
+const char *var_get_gdata(const char *name);
 int cmd_set_real(const char *, int idx, char *);
 const char *var_get_str_by_name(const char *);
 

+ 0 - 1
src/tandem.h

@@ -14,7 +14,6 @@ typedef struct tand_t_struct {
   time_t buildts;
   int localhub;
   struct userrec* u;
-  char *not_chans;
   char commit[10];
   char bot[HANDLEN + 1];
   char version[151];

+ 20 - 21
src/userent.c

@@ -299,23 +299,21 @@ static bool set_unpack(struct userrec *u, struct user_entry *e)
   head = curr = e->u.list;
   e->u.extra = NULL;
 
-  if (conf.bot->hub || conf.bot->localhub || !strcasecmp(conf.bot->nick, u->handle)) {
-    struct xtra_key *t = NULL;
-    char *key = NULL, *data = NULL;
-
-    while (curr) {
-      t = (struct xtra_key *) my_calloc(1, sizeof(struct xtra_key));
-      data = curr->extra;
-      key = newsplit(&data);
-      if (data[0]) {
-        t->key = strdup(key);
-        t->data = strdup(data);
-        list_insert((struct xtra_key **) (&e->u.extra), t);
-      } else
-        free(t);
-      curr = curr->next;
-    }
-  } 
+  struct xtra_key *t = NULL;
+  char *key = NULL, *data = NULL;
+
+  while (curr) {
+    t = (struct xtra_key *) my_calloc(1, sizeof(struct xtra_key));
+    data = curr->extra;
+    key = newsplit(&data);
+    if (data[0]) {
+      t->key = strdup(key);
+      t->data = strdup(data);
+      list_insert((struct xtra_key **) (&e->u.extra), t);
+    } else
+      free(t);
+    curr = curr->next;
+  }
 
   list_type_kill(head);
   return 1;
@@ -365,12 +363,13 @@ static bool set_gotshare(struct userrec *u, struct user_entry *e, char *buf, int
 static void set_write_userfile(bd::Stream& stream, const struct userrec *u, const struct user_entry *e, int idx)
 {
   int localhub = nextbot(u->handle);
-  /* only write if saving local, or if sending to hub, or if sending to same user as entry, or the localhub in the chain */
-  if (idx == -1 || dcc[idx].hub || dcc[idx].user == u || (localhub != -1 && idx == localhub)) {
-    struct xtra_key *x = (struct xtra_key *) e->u.extra;
+  struct xtra_key *x = (struct xtra_key *) e->u.extra;
 
-    for (; x; x = x->next)
+  for (; x; x = x->next) {
+    /* only write if saving local, or if sending to hub, or if sending to same user as entry, or the localhub in the chain, or sending 'groups' */
+    if (idx == -1 || dcc[idx].hub || dcc[idx].user == u || (localhub != -1 && idx == localhub) || !strcmp(x->key, "groups")) {
       stream << bd::String::printf("--%s %s %s\n", e->type->name, x->key, x->data ? x->data : "");
+    }
   }
 }