User:B-bot/source/Expired OTRS pending tagger

This task will tag files for deletion if they have been tagged with {{OTRS pending}} longer than {{OTRS backlog}} days..

    /// <summary>
    /// This class will search for images that have been tagged as {{OTRS pending}} for more than {{OTRS backlog}}
    /// days.  It will tag the images with {{subst:npd}} and notify the uploader with {{subst:Di-no permission-notice-final}}.
    /// </summary>
    public class ExpiredOtrsPendingTagger : BBotBase
    {
        /// <summary>
        /// Name of category to patrol
        /// </summary>
        const String csOtrsPendingByDateCategoryName = "Category:Items pending OTRS confirmation of permission by date";

        /// <summary>
        /// Maximum numberof 
        /// </summary>
        public int MaximumTagsPerRun { get; set; }

        /// <summary>
        /// Constructor
        /// </summary>
        public ExpiredOtrsPendingTagger()
        {
            MaximumTagsPerRun = Properties.Settings.Default.MaximumExpiredOtrsPendingTagsPerRun;
        }

        /// <summary>
        /// Gets the name for this job
        /// </summary>
        /// <returns></returns>
        public override string GetJobName()
        {
            return "Expired OTRS pending tagger";
        }

        /// <summary>
        /// This function will return the text of the last version of this page that either an OTRS user or an admin edited.
        /// </summary>
        /// <param name="PageName"></param>
        private String GetLastVersionByOtrsUser(Site site, String PageName, ref DateTime dtmModified)
        {
            System.Threading.Thread.Sleep(1000 * Properties.Settings.Default.CheckStopDelaySeconds);

            PageList pl = new PageList(site);
            pl.FillFromPageHistory(PageName, 20);

            // Loop through the pages and find the last one an OTRS user or admin edited
            foreach (Page p in pl)
            {
                if (IsUserOTRS(site, p.lastUser) || IsUserAdmin(site, p.lastUser))
                {
                    p.LoadTextOnly();
                    dtmModified = p.timestamp;
                    System.Threading.Thread.Sleep(1000 * Properties.Settings.Default.CheckStopDelaySeconds);
                    return p.text;
                }
            }

            return "";
        }

        /// <summary>
        /// Searches the text of a page to find the reqested tag
        /// </summary>
        /// <param name="strPageText">Revision text</param>
        /// <param name="strRegex"></param>
        /// <returns></returns>
        /// <remarks></remarks>
        public String GetTag(String strPageText, String strRegex)
        {
            Match match = Regex.Match(strPageText, strRegex, RegexOptions.IgnoreCase);
            if (null != match && 0 < match.Length)
            {
                return strPageText.Substring(match.Index, match.Length);
            }

            return "";
        }

        /// <summary>
        /// The master function to perform the job
        /// </summary>
        public void PerformTask()
        {
            // Always allow at least 30 days before deleting anything
            const int ciMinimumBacklogDays = 30;

            // On top of the backlog, allow 7 days so that we're not going to 
            const int ciGracePeriod = 7;

            // Connect to Wikipedia
            Site site = TryToConnect("https://en.wikipedia.org", Properties.Settings.Default.BotUserName, Properties.Settings.Default.BotPassword);

            // Use a separate connection for our less-important API calls
            Site site2 = TryToConnect("https://en.wikipedia.org", Properties.Settings.Default.BotUserName, Properties.Settings.Default.BotPassword);

            // Log the start
            if (UserspaceTest)
            {
                LogToEventLog(ref site, MessageType.Start, "B-Bot \"Expired [[template:OTRS pending|OTRS pending]] tagger\" process now commencing <font color='red'>'''IN TEST MODE'''</font>.", null);
            }
            else
            {
                LogToEventLog(ref site, MessageType.Start, "B-Bot \"Expired [[template:OTRS pending|OTRS pending]] tagger\" process now commencing.", null);
            }

            DateTime dtmBacklog = DateTime.Now.AddYears(-5);
            String strBacklog = GetLastVersionByOtrsUser(site2, "Template:OTRS backlog", ref dtmBacklog);

            SleepApiDelay();

            Page pgUserspaceTest = new Page(site, Properties.Settings.Default.UserspaceTestPage);

            if (UserspaceTest)
            {
                if (CallEditPage(site, pgUserspaceTest.title, "", "Initial header"))
                {
                    pgUserspaceTest.text = "Now beginning expired OTRS pending tagger task on " + DateTime.Now.ToString() + " (local time) ...\r\n\r\n";
                    pgUserspaceTest.text += "{| class=\"wikitable sortable\"\r\n|-\r\n! Page !! Timestamp !! Proposed edit\r\n";
                    pgUserspaceTest.Save();
                }
            }

            if (String.IsNullOrWhiteSpace(strBacklog))
            {
                LogToEventLog(ref site, MessageType.Error, "{{tl|OTRS backlog}} could not be read - no revision by an admin or OTRS user could be found.  Aborting job.", null);
                return;
            }

            strBacklog = RemoveComment(strBacklog, "<noinclude>", "</noinclude>");

            int intBacklog = 0;

            // Now, hopefully, what we have here is a number
            try
            {
                intBacklog = System.Convert.ToInt32(strBacklog);
            }
            catch { }

            if (0 >= intBacklog)
            {
                LogToEventLog(ref site, MessageType.Error, "{{tl|OTRS backlog}} could not be read as a positive integer.  Please ensure that content outside of noinclude blocks is nothing but a number.", null);
                return;
            }

            // Truncate the time
            dtmBacklog = new DateTime(dtmBacklog.Year, dtmBacklog.Month, dtmBacklog.Day);

            String strBacklogMessage = "The [[Template:OTRS backlog|backlog]] is " + intBacklog.ToString() + " as of " + dtmBacklog.ToShortDateString() + ".";

            // So now, we have a number of days.  Subtract this from the date that the template was updated
            dtmBacklog = dtmBacklog.AddDays(-1 * intBacklog);
            strBacklogMessage += "  (In other words, tickets prior to " + dtmBacklog.ToShortDateString() + " should have been processed.)";

            // Now, provide a seven-day grace period
            dtmBacklog = dtmBacklog.AddDays(-1 * ciGracePeriod);

            // Require a minimum of 30 days
            if (dtmBacklog.AddDays(ciMinimumBacklogDays) > DateTime.Now)
            {
                dtmBacklog = DateTime.Now.AddDays(-1 * ciMinimumBacklogDays);

                strBacklogMessage += "  However, we have a minimum time of " + ciMinimumBacklogDays.ToString() + " days before tagging and thus will process only images tagged prior to " + dtmBacklog.ToShortDateString() + ".";
            }
            else
            {
                intBacklog = (int)((DateTime.Now - dtmBacklog).TotalDays);
                strBacklogMessage += "  Including a grace period of " + ciGracePeriod.ToString() + " days to allow time to submit the ticket, we will process images tagged prior to " + dtmBacklog.ToShortDateString() +
                                     ", or, " + intBacklog.ToString() + " days.";
            }

            // Log our backlog
            LogToEventLog(ref site, MessageType.Informational, strBacklogMessage, null);

            // Grab the list of pages in the category
            PageList pl = new PageList(site);
            pl.FillAllFromCategory(csOtrsPendingByDateCategoryName);

            SleepApiDelay();

            int intTagsLeft = 0;

            // Loop through the list
            foreach (Page page in pl)
            {
                if (Abort)
                {
                    break;
                }

                // Only process files
                if (6 != page.GetNamespace())
                {
                    continue;
                }

                // Load the page
                page.Load();

                SleepApiDelay();

                // Ignore any pages already tagged with a deletion tag
                bool blnTaggedForDeletion = false;
                List<String> listCategories = page.GetAllCategories();
                SleepApiDelay();
                foreach (String strCat in listCategories)
                {
                    if (strCat == "Category:Candidates for speedy deletion" ||
                        strCat.StartsWith("Category:Wikipedia files missing permission") ||
                        strCat == "Category:All non-free media" ||
                        strCat == "Category:Items with OTRS permission confirmed")
                    {
                        blnTaggedForDeletion = true;
                        break;
                    }
                }
                if (blnTaggedForDeletion)
                {
                    continue;
                }

                // We need to determine the date.  If the OTRS pending tag has a date, use that.  
                // Otherwise, use the date of the last page revision.
                String strOtrsPendingTag = GetTag(page.text, @"\{\{\s*otrs(\s|-|)pending([^\{^\}]*|)\}\}");

                if (String.IsNullOrWhiteSpace(strOtrsPendingTag))
                {
                    LogToEventLog(ref site2, MessageType.Error, "Error: could not find OTRS pending tag on [[:" + page.title + "]]", null);
                    continue;
                }

                // Split on the pipes
                String[] arrParameters = strOtrsPendingTag.Split('|');

                int intMonth = 0;
                int intDay = 0;
                int intYear = 0;
                DateTime? dtmDate = null;

                // Loop through our parameters
                foreach (String param in arrParameters)
                {
                    try
                    {
                        String[] arrNameValue = param.Split('=');

                        if (2 != arrNameValue.Length)
                        {
                            continue;
                        }

                        arrNameValue[1] = arrNameValue[1].Replace("}}", "").Trim();

                        // Look for month, day, year, and date
                        if (arrNameValue[0].Trim().ToLower() == "month")
                        {
                            intMonth = System.Convert.ToInt32(arrNameValue[1]);
                        }
                        else if (arrNameValue[0].Trim().ToLower() == "day")
                        {
                            intDay = System.Convert.ToInt32(arrNameValue[1]);
                        }
                        else if (arrNameValue[0].Trim().ToLower() == "year")
                        {
                            intYear = System.Convert.ToInt32(arrNameValue[1]);
                        }
                        else if (arrNameValue[0].Trim().ToLower() == "date")
                        {
                            try
                            {
                                dtmDate = DateTime.Parse(arrNameValue[1]);
                            }
                            catch 
                            {
                                try
                                {
                                    dtmDate = DateTime.ParseExact(arrNameValue[1], "hh:mm, dd MMMM yyyy (UTC)", System.Globalization.CultureInfo.InvariantCulture);
                                }
                                catch { }
                            }
                        }
                    }
                    catch{}
                }

                // Okay, now we should have a date maybe?
                if (!dtmDate.HasValue)
                {
                    if (0 < intMonth && 12 >= intMonth && 0 < intDay && 31 >= intDay && 2015 <= intYear)
                    {
                        try
                        {
                            dtmDate = new DateTime(intYear, intMonth, intDay);
                        }
                        catch{}
                    }
                }

                // If we get here, then we are just going to use the page revision date
                if (!dtmDate.HasValue)
                {
                    dtmDate = page.timestamp;
                }

                // Has our page expired?
                if (dtmBacklog > dtmDate)
                {
                    // Add the npd tag
                    if (CallEditPage(site, page.title, page.text, "{{subst:npd|source={{NoOTRS60|days={{subst:OTRS backlog}}}}}}\r\n" + page.text))
                    {
                        page.text = "{{subst:npd|source={{NoOTRS60|days={{subst:OTRS backlog}}}}}}\r\n" + page.text;

                        if (UserspaceTest)
                        {
                            pgUserspaceTest.text += "|-\r\n| [[:" + page.title + "]] || ~~~~~ || <pre>" + page.text.Substring(0, Math.Min(300, page.text.Length)) + "</pre>\r\n";
                            pgUserspaceTest.Save(Properties.Settings.Default.NpdTagComment, false);
                        }
                        else
                        {
                            page.Save(page.text, Properties.Settings.Default.NpdTagComment, false);
                        }
                    }

                    if (Abort) { LogToEventLog(ref site, MessageType.Error, "I was ordered to abort.", null); break; }

                    intTagsLeft++;

                    // Sleep for our editing delay
                    SleepTaggingDelay();

                    // Determine the first contributor
                    PageList history = TryToFillFromPageHistory(ref site2, page.title);

                    if (0 < history.Count())
                    {
                        String strNotifyUser = history[history.Count() - 1].lastUser;

                        if (!String.IsNullOrWhiteSpace(strNotifyUser))
                        {
                            try
                            {
                                // Retrieve this user's talk page
                                Page pgUserTalkPage = new Page(site, "User talk:" + strNotifyUser);
                                pgUserTalkPage.Load();

                                SleepApiDelay();

                                // If it is a redirect, resolve it.
                                if (pgUserTalkPage.IsRedirect())
                                {
                                    pgUserTalkPage.ResolveRedirect();
                                }

                                // Can we notify this user?
                                if (BotEditPermitted(pgUserTalkPage.text, Properties.Settings.Default.BotUserName, "npd"))
                                {
                                    if (CallEditPage(site, pgUserTalkPage.title, pgUserTalkPage.text, pgUserTalkPage.text +
                                                    "\r\n{{subst:di-no permission-notice-final|" + page.title + "}} --~~~~"))
                                    {
                                        pgUserTalkPage.text += "\r\n{{subst:di-no permission-notice-final|" + page.title + "}} --~~~~";

                                        if (UserspaceTest)
                                        {
                                            pgUserspaceTest.text += "|-\r\n| [[:" + pgUserTalkPage.title + "]] || ~~~~~ || <pre>" +
                                                pgUserTalkPage.text.Substring(pgUserTalkPage.text.Length - Math.Min(300, pgUserTalkPage.text.Length)) + "</pre>\r\n";
                                            pgUserspaceTest.Save(String.Format(Properties.Settings.Default.NpdWarningTagComment, page.title), false);
                                        }
                                        else
                                        {
                                            pgUserTalkPage.Save(String.Format(Properties.Settings.Default.NpdWarningTagComment, page.title), false);
                                        }
                                    }

                                }
                            }
                            catch (Exception ex)
                            {
                                LogToEventLog(ref site, MessageType.Error, "Failed to notify [[:" + "User talk:" + strNotifyUser + "]] that the OTRS pending tag on [[:" +
                                                            page.title + "]] has expired.", ex);
                            }
                        }

                    }

                    if (Abort) { LogToEventLog(ref site, MessageType.Error, "I was ordered to abort.", null); break; }

                    // Sleep for our editing delay
                    System.Threading.Thread.Sleep(1000 * Properties.Settings.Default.TaggingEditDelaySeconds);
                }

                if (intTagsLeft >= MaximumTagsPerRun)
                {
                    break;
                }

                SleepApiDelay();
            }

            if (UserspaceTest)
            {
                if (CallEditPage(site, pgUserspaceTest.title, "", "footer"))
                {
                    pgUserspaceTest.text += "|}\r\n";
                    pgUserspaceTest.Save();
                }
            }

            // All done
            LogToEventLog(ref site, MessageType.Finish, "B-Bot Expired [[template:OTRS pending|OTRS pending]] tagger process done.  Processed " + intTagsLeft.ToString() + " pages.", null);
        }
    }