The source code for User:B-bot uses the DotNetWikiBot framework. For the sake of brevity, I have omitted the things like my test functions or routine utility functions. (If a BAG member needs the whole thing, please let me know and perhaps I can email you the entire solution.)

Source code for individual tasks

edit

Each task has its own class and they are all derived from a common class that has some shared functions that do things like checking for {{bots}} tags, unescaping strings (which oddly, though I tried to find a built-in method, there didn't seem to be one), managing connections and delay times, and other miscellanea.

The source code for tasks of this bot can be found at these pages:

Bot edit permitted

edit

This is the code used by any bot I create to check to see if a bot is permitted to edit a user page to leave a notification. It handles some possibilities that the examples at {{bots}} do not.

/// <summary>
/// This function will remove commented uses of the {{nobots}} or {{bots}} template so that they are 
/// not considered in the BotEditPermitted() function.
/// </summary>
/// <param name="PageText"></param>
/// <param name="CommentStart"></param>
/// <param name="CommentEnd"></param>
/// <returns></returns>
private static String RemoveComment(String PageText, String CommentStart, String CommentEnd)
{
    // Remove anything that is inside of <nowiki> ... <nowiki> or <!-- ... -->
    for (int nowiki = PageText.IndexOf(CommentStart, StringComparison.CurrentCultureIgnoreCase); 0 <= nowiki; nowiki = PageText.IndexOf(CommentStart, StringComparison.CurrentCultureIgnoreCase))
    {
        int endnowiki = PageText.IndexOf(CommentEnd, nowiki, StringComparison.CurrentCultureIgnoreCase);

        if (0 <= endnowiki)
        {
            String NewText = "";
            if (0 < nowiki)
            {
                NewText = PageText.Substring(0, nowiki);
            }

            if (endnowiki + CommentEnd.Length < PageText.Length)
            {
                NewText += PageText.Substring(endnowiki + CommentEnd.Length);
            }

            PageText = NewText;
        }
        else
        {
            // If the comment doesn't end, then we're done
            return PageText;
        }
    }

    return PageText;
}

/// <summary>
/// This function attempts to parse the page text for the {{bots}} and {{nobots}} templates.
/// 
/// {{nobots}} will be checked for allow=BotUserName or optin=BotNotificationType
/// {{nobots}} can also be superseded by a {{bots|allow=BotUserName}} or {{bots|optin=BotNotificationType}}
/// 
/// {{bots|allow=Bot1,Bot2}} is interpreted as disallowing everything but Bot1 and Bot2 unless another {{bots}} statement is found
/// </summary>
/// <param name="PageText">The page text to be parsed.</param>
/// <param name="BotUserName">This bot's username, this function will check for it in "allow" or "deny".  IMPORTANT: </param>
/// <param name="BotNotificationType">The type of notification this bot is sending, e.g. nosource, nolicense, orfud.</param>
/// <returns>Returns true if this bot is permitted to edit the page and false if it is not.</returns>
public static bool BotEditPermitted(string PageText, string BotUserName, string BotNotificationType)
{
    try
    {
        // Remove comments from consideration
        PageText = RemoveComment(PageText, "<nowiki>", "</nowiki>");
        PageText = RemoveComment(PageText, "<code>", "</code>");
        PageText = RemoveComment(PageText, "<pre>", "</pre>");
        PageText = RemoveComment(PageText, "<!--", "-->");

        // Find all of the bots or nobots tags
        MatchCollection matches = Regex.Matches(PageText, @"\{\{\s*(nobots|bots|deceased wikipedian)[^\}\{]*\}\}", RegexOptions.IgnoreCase);

        if (null != matches && matches.Count > 0)
        {
            List<string> commands = new List<string>();

            foreach (Match m in matches)
            {
                if (0 < m.Length)
                {
                    // This is an individual bots tag to process
                    commands.Add(PageText.Substring(m.Index, m.Length).Trim());
                }
            }
                
            // Rule #1: if any command denies this bot by name, then disallow the edit
            if (!String.IsNullOrWhiteSpace(BotUserName))
            {
                foreach (String str in commands)
                {
                    if (Regex.IsMatch(str, @"\{\{\s*(nobots|bots).*deny\s*\=\s*([^\|]*\,|)\s*" + Regex.Escape(BotUserName) + @"\s*(\,[^\|]*|)(\|.*|)\s*\}\}", RegexOptions.IgnoreCase))
                    {
                        return false;
                    }
                }
            }

            // Rule #2 - if any command opts out of this notification type by name, then disallow the edit
            if (!String.IsNullOrWhiteSpace(BotNotificationType))
            {
                foreach (String str in commands)
                {
                    if (Regex.IsMatch(str, @"\{\{\s*(nobots|bots).*optout\s*\=\s*([^\|]*\,|)\s*" + Regex.Escape(BotNotificationType) + @"\s*(\,[^\|]*|)(\|.*|)\s*\}\}", RegexOptions.IgnoreCase))
                    {
                        return false;
                    }
                }
            }

            // Rule #3 - if any command allows this bot by name, then allow the edit
            if (!String.IsNullOrWhiteSpace(BotUserName))
            {
                foreach (String str in commands)
                {
                    if (Regex.IsMatch(str, @"\{\{\s*(nobots|bots).*allow\s*\=\s*([^\|]*\,|)\s*" + Regex.Escape(BotUserName) + @"\s*(\,[^\|]*|)(\|.*|)\s*\}\}", RegexOptions.IgnoreCase))
                    {
                        return true;
                    }
                }
            }

            // Rule #4 - if any command opts in to this notification type by name, then allow the edit
            if (!String.IsNullOrWhiteSpace(BotNotificationType))
            {
                foreach (String str in commands)
                {
                    if (Regex.IsMatch(str, @"\{\{\s*(nobots|bots).*optin\s*\=\s*([^\|]*\,|)\s*" + Regex.Escape(BotNotificationType) + @"\s*(\,[^\|]*|)(\|.*|)\s*\}\}", RegexOptions.IgnoreCase))
                    {
                        return true;
                    }
                }
            }

            // Rule #5 - if we have a {{bots|allow=all}} or {{nobots|allow=all}}, then allow the edit (does anyone do this?)
            foreach (String str in commands)
            {
                if (Regex.IsMatch(str, @"\{\{\s*(nobots|bots).*allow\s*\=\s*([^\|]*\,|)\s*all\s*(\,[^\|]*|)(\|.*|)\s*\}\}", RegexOptions.IgnoreCase))
                {
                    return true;
                }
            }

            // Rule #6 - if we have a {{(no)bots|allow=none}} or {{(no)bots|deny=all}}, then disallow the edit
            foreach (String str in commands)
            {
                if (Regex.IsMatch(str, @"\{\{\s*(nobots|bots).*(allow\s*\=\s*([^\|]*\,|)\s*none|deny\s*\=\s*([^\|]*\,|)\s*all)\s*(\,[^\|]*|)(\|.*|)\s*\}\}", RegexOptions.IgnoreCase))
                {
                    return false;
                }
            }

            // Rule #7 - if we have a {{nobots}} of any sort or a {{deceased wikipedian}} and we haven't found one of the exemptions above, then disallow the edit
            foreach (String str in commands)
            {
                if (Regex.IsMatch(str, @"\{\{\s*(nobots|deceased wikipedian).*\}\}", RegexOptions.IgnoreCase))
                {
                    return false;
                }
            }

            // Rule #8 - if we have a {{bots|allow=anything}} and we are not mentioned by name, then disallow the edit
            foreach (String str in commands)
            {
                if (Regex.IsMatch(str, @"\{\{\s*(nobots|bots).*allow\s*\=\s*.*\}\}", RegexOptions.IgnoreCase))
                {
                    return false;
                }
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }

    return true;
}