]> git.netwichtig.de Git - user/henk/code/inspircd.git/blobdiff - src/modules/m_filter.cpp
Update copyright headers.
[user/henk/code/inspircd.git] / src / modules / m_filter.cpp
index d32a11f21ce58050e232663885cd07a11cf33168..c666f6ad265d855c31fde130a9fb78559387411f 100644 (file)
@@ -1,10 +1,21 @@
 /*
  * InspIRCd -- Internet Relay Chat Daemon
  *
- *   Copyright (C) 2009 Daniel De Graaf <danieldg@inspircd.org>
- *   Copyright (C) 2004, 2008 Craig Edwards <craigedwards@brainbox.cc>
+ *   Copyright (C) 2019 Matt Schatz <genius3000@g3k.solutions>
+ *   Copyright (C) 2019 Filippo Cortigiani <simos@simosnap.org>
+ *   Copyright (C) 2018-2019 linuxdaemon <linuxdaemon.irc@gmail.com>
+ *   Copyright (C) 2018 Michael Hazell <michaelhazell@hotmail.com>
+ *   Copyright (C) 2017 B00mX0r <b00mx0r@aureus.pw>
+ *   Copyright (C) 2012-2014, 2016 Attila Molnar <attilamolnar@hush.com>
+ *   Copyright (C) 2012-2013, 2017-2021 Sadie Powell <sadie@witchery.services>
+ *   Copyright (C) 2012, 2018-2019 Robby <robby@chatbelgie.be>
+ *   Copyright (C) 2011 Adam <Adam@anope.org>
+ *   Copyright (C) 2009-2010 Daniel De Graaf <danieldg@inspircd.org>
+ *   Copyright (C) 2009 Robin Burchell <robin+git@viroteck.net>
+ *   Copyright (C) 2009 Matt Smith <dz@inspircd.org>
+ *   Copyright (C) 2009 John Brooks <special@inspircd.org>
  *   Copyright (C) 2007 Dennis Friis <peavey@inspircd.org>
- *   Copyright (C) 2007 Robin Burchell <robin+git@viroteck.net>
+ *   Copyright (C) 2006-2010 Craig Edwards <brain@inspircd.org>
  *
  * This file is part of InspIRCd.  InspIRCd is free software: you can
  * redistribute it and/or modify it under the terms of the GNU General Public
@@ -26,6 +37,9 @@
 #include "modules/server.h"
 #include "modules/shun.h"
 #include "modules/stats.h"
+#include "modules/account.h"
+
+#include <fstream>
 
 enum FilterFlags
 {
@@ -39,6 +53,7 @@ enum FilterAction
 {
        FA_GLINE,
        FA_ZLINE,
+       FA_WARN,
        FA_BLOCK,
        FA_SILENT,
        FA_KILL,
@@ -53,7 +68,7 @@ class FilterResult
        std::string freeform;
        std::string reason;
        FilterAction action;
-       long duration;
+       unsigned long duration;
        bool from_config;
 
        bool flag_no_opers;
@@ -62,8 +77,9 @@ class FilterResult
        bool flag_privmsg;
        bool flag_notice;
        bool flag_strip_color;
+       bool flag_no_registered;
 
-       FilterResult(dynamic_reference<RegexFactory>& RegexEngine, const std::string& free, const std::string& rea, FilterAction act, long gt, const std::string& fla, bool cfg)
+       FilterResult(dynamic_reference<RegexFactory>& RegexEngine, const std::string& free, const std::string& rea, FilterAction act, unsigned long gt, const std::string& fla, bool cfg)
                : freeform(free)
                , reason(rea)
                , action(act)
@@ -79,7 +95,7 @@ class FilterResult
        char FillFlags(const std::string &fl)
        {
                flag_no_opers = flag_part_message = flag_quit_message = flag_privmsg =
-                       flag_notice = flag_strip_color = false;
+                       flag_notice = flag_strip_color = flag_no_registered = false;
 
                for (std::string::const_iterator n = fl.begin(); n != fl.end(); ++n)
                {
@@ -103,6 +119,9 @@ class FilterResult
                                case 'c':
                                        flag_strip_color = true;
                                break;
+                               case 'r':
+                                       flag_no_registered = true;
+                               break;
                                case '*':
                                        flag_no_opers = flag_part_message = flag_quit_message =
                                                flag_privmsg = flag_notice = flag_strip_color = true;
@@ -115,7 +134,7 @@ class FilterResult
                return 0;
        }
 
-       std::string GetFlags()
+       std::string GetFlags() const
        {
                std::string flags;
                if (flag_no_opers)
@@ -129,12 +148,15 @@ class FilterResult
                if (flag_notice)
                        flags.push_back('n');
 
-               /* Order is important here, 'c' must be the last char in the string as it is unsupported
-                * on < 2.0.10, and the logic in FillFlags() stops parsing when it ecounters an unknown
-                * character.
+               /* Order is important here, as the logic in FillFlags() stops parsing when it encounters
+                * an unknown character. So the following characters must be last in the string.
+                * 'c' is unsupported on < 2.0.10
+                * 'r' is unsupported on < 3.2.0
                 */
                if (flag_strip_color)
                        flags.push_back('c');
+               if (flag_no_registered)
+                       flags.push_back('r');
 
                if (flags.empty())
                        flags.push_back('-');
@@ -154,7 +176,7 @@ class CommandFilter : public Command
                : Command(f, "FILTER", 1, 5)
        {
                flags_needed = 'o';
-               this->syntax = "<filter-definition> <action> <flags> [<duration>] :<reason>";
+               this->syntax = "<pattern> [<action> <flags> [<duration>] :<reason>]";
        }
        CmdResult Handle(User* user, const Params& parameters) CXX11_OVERRIDE;
 
@@ -164,12 +186,19 @@ class CommandFilter : public Command
        }
 };
 
-class ModuleFilter : public Module, public ServerEventListener, public Stats::EventListener
+class ModuleFilter
+       : public Module
+       , public ServerProtocol::SyncEventListener
+       , public Stats::EventListener
+       , public Timer
 {
        typedef insp::flat_set<std::string, irc::insensitive_swo> ExemptTargetSet;
 
        bool initing;
        bool notifyuser;
+       bool warnonselfmsg;
+       bool dirty;
+       std::string filterconf;
        RegexFactory* factory;
        void FreeFilters();
 
@@ -191,8 +220,8 @@ class ModuleFilter : public Module, public ServerEventListener, public Stats::Ev
        CullResult cull() CXX11_OVERRIDE;
        ModResult OnUserPreMessage(User* user, const MessageTarget& target, MessageDetails& details) CXX11_OVERRIDE;
        FilterResult* FilterMatch(User* user, const std::string &text, int flags);
-       bool DeleteFilter(const std::string &freeform);
-       std::pair<bool, std::string> AddFilter(const std::string &freeform, FilterAction type, const std::string &reason, long duration, const std::string &flags);
+       bool DeleteFilter(const std::string& freeform, std::string& reason);
+       std::pair<bool, std::string> AddFilter(const std::string& freeform, FilterAction type, const std::string& reason, unsigned long duration, const std::string& flags, bool config = false);
        void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE;
        Version GetVersion() CXX11_OVERRIDE;
        std::string EncodeFilter(FilterResult* filter);
@@ -202,6 +231,7 @@ class ModuleFilter : public Module, public ServerEventListener, public Stats::Ev
        ModResult OnStats(Stats::Context& stats) CXX11_OVERRIDE;
        ModResult OnPreCommand(std::string& command, CommandBase::Params& parameters, LocalUser* user, bool validated) CXX11_OVERRIDE;
        void OnUnloadModule(Module* mod) CXX11_OVERRIDE;
+       bool Tick(time_t) CXX11_OVERRIDE;
        bool AppliesToMe(User* user, FilterResult* filter, int flags);
        void ReadFilters();
        static bool StringToFilterAction(const std::string& str, FilterAction& fa);
@@ -213,16 +243,19 @@ CmdResult CommandFilter::Handle(User* user, const Params& parameters)
        if (parameters.size() == 1)
        {
                /* Deleting a filter */
-               Module *me = creator;
-               if (static_cast<ModuleFilter *>(me)->DeleteFilter(parameters[0]))
+               Module* me = creator;
+               std::string reason;
+
+               if (static_cast<ModuleFilter*>(me)->DeleteFilter(parameters[0], reason))
                {
-                       user->WriteNotice("*** Removed filter '" + parameters[0] + "'");
-                       ServerInstance->SNO->WriteToSnoMask(IS_LOCAL(user) ? 'f' : 'F', "FILTER: "+user->nick+" removed filter '"+parameters[0]+"'");
+                       user->WriteNotice("*** Removed filter '" + parameters[0] + "': " + reason);
+                       ServerInstance->SNO->WriteToSnoMask(IS_LOCAL(user) ? 'f' : 'F', "%s removed filter '%s': %s",
+                               user->nick.c_str(), parameters[0].c_str(), reason.c_str());
                        return CMD_SUCCESS;
                }
                else
                {
-                       user->WriteNotice("*** Filter '" + parameters[0] + "' not found in list, try /stats s.");
+                       user->WriteNotice("*** Filter '" + parameters[0] + "' not found on the list.");
                        return CMD_FAILURE;
                }
        }
@@ -235,14 +268,14 @@ CmdResult CommandFilter::Handle(User* user, const Params& parameters)
                        FilterAction type;
                        const std::string& flags = parameters[2];
                        unsigned int reasonindex;
-                       long duration = 0;
+                       unsigned long duration = 0;
 
                        if (!ModuleFilter::StringToFilterAction(parameters[1], type))
                        {
                                if (ServerInstance->XLines->GetFactory("SHUN"))
-                                       user->WriteNotice("*** Invalid filter type '" + parameters[1] + "'. Supported types are 'gline', 'zline', 'none', 'block', 'silent', 'kill', and 'shun'.");
+                                       user->WriteNotice("*** Invalid filter type '" + parameters[1] + "'. Supported types are 'gline', 'zline', 'none', 'warn', 'block', 'silent', 'kill', and 'shun'.");
                                else
-                                       user->WriteNotice("*** Invalid filter type '" + parameters[1] + "'. Supported types are 'gline', 'zline', 'none', 'block', 'silent', and 'kill'.");
+                                       user->WriteNotice("*** Invalid filter type '" + parameters[1] + "'. Supported types are 'gline', 'zline', 'none', 'warn', 'block', 'silent', and 'kill'.");
                                return CMD_FAILURE;
                        }
 
@@ -250,7 +283,11 @@ CmdResult CommandFilter::Handle(User* user, const Params& parameters)
                        {
                                if (parameters.size() >= 5)
                                {
-                                       duration = InspIRCd::Duration(parameters[3]);
+                                       if (!InspIRCd::Duration(parameters[3], duration))
+                                       {
+                                               user->WriteNotice("*** Invalid duration for filter");
+                                               return CMD_FAILURE;
+                                       }
                                        reasonindex = 4;
                                }
                                else
@@ -264,15 +301,19 @@ CmdResult CommandFilter::Handle(User* user, const Params& parameters)
                                reasonindex = 3;
                        }
 
-                       Module *me = creator;
-                       std::pair<bool, std::string> result = static_cast<ModuleFilter *>(me)->AddFilter(freeform, type, parameters[reasonindex], duration, flags);
+                       Moduleme = creator;
+                       std::pair<bool, std::string> result = static_cast<ModuleFilter*>(me)->AddFilter(freeform, type, parameters[reasonindex], duration, flags);
                        if (result.first)
                        {
-                               user->WriteNotice("*** Added filter '" + freeform + "', type '" + parameters[1] + "'" +
-                                       (duration ? ", duration " +  parameters[3] : "") + ", flags '" + flags + "', reason: '" +
-                                       parameters[reasonindex] + "'");
+                               const std::string message = InspIRCd::Format("'%s', type '%s'%s, flags '%s', reason: %s",
+                                       freeform.c_str(), parameters[1].c_str(),
+                                       (duration ? InspIRCd::Format(", duration '%s'",
+                                               InspIRCd::DurationString(duration).c_str()).c_str()
+                                       : ""), flags.c_str(), parameters[reasonindex].c_str());
 
-                               ServerInstance->SNO->WriteToSnoMask(IS_LOCAL(user) ? 'f' : 'F', "FILTER: "+user->nick+" added filter '"+freeform+"', type '"+parameters[1]+"', "+(duration ? "duration "+parameters[3]+", " : "")+"flags '"+flags+"', reason: "+parameters[reasonindex]);
+                               user->WriteNotice("*** Added filter " + message);
+                               ServerInstance->SNO->WriteToSnoMask(IS_LOCAL(user) ? 'f' : 'F',
+                                       "%s added filter %s", user->nick.c_str(), message.c_str());
 
                                return CMD_SUCCESS;
                        }
@@ -293,8 +334,12 @@ CmdResult CommandFilter::Handle(User* user, const Params& parameters)
 
 bool ModuleFilter::AppliesToMe(User* user, FilterResult* filter, int iflags)
 {
+       const AccountExtItem* accountext = GetAccountExtItem();
+
        if ((filter->flag_no_opers) && user->IsOper())
                return false;
+       if ((filter->flag_no_registered) && accountext && accountext->get(user))
+               return false;
        if ((iflags & FLAG_PRIVMSG) && (!filter->flag_privmsg))
                return false;
        if ((iflags & FLAG_NOTICE) && (!filter->flag_notice))
@@ -307,9 +352,11 @@ bool ModuleFilter::AppliesToMe(User* user, FilterResult* filter, int iflags)
 }
 
 ModuleFilter::ModuleFilter()
-       : ServerEventListener(this)
+       : ServerProtocol::SyncEventListener(this)
        , Stats::EventListener(this)
+       , Timer(0, true)
        , initing(true)
+       , dirty(false)
        , filtcommand(this)
        , RegexEngine(this, "regex")
 {
@@ -332,6 +379,7 @@ void ModuleFilter::FreeFilters()
                delete i->regex;
 
        filters.clear();
+       dirty = true;
 }
 
 ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtarget, MessageDetails& details)
@@ -345,34 +393,53 @@ ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtar
        FilterResult* f = this->FilterMatch(user, details.text, flags);
        if (f)
        {
-               std::string target;
-               if (msgtarget.type == MessageTarget::TYPE_USER)
+               bool is_selfmsg = false;
+               switch (msgtarget.type)
                {
-                       User* t = msgtarget.Get<User>();
-                       // Check if the target nick is exempted, if yes, ignore this message
-                       if (exemptednicks.count(t->nick))
+                       case MessageTarget::TYPE_USER:
+                       {
+                               User* t = msgtarget.Get<User>();
+                               // Check if the target nick is exempted, if yes, ignore this message
+                               if (exemptednicks.count(t->nick))
+                                       return MOD_RES_PASSTHRU;
+
+                               if (user == t)
+                                       is_selfmsg = true;
+                               break;
+                       }
+                       case MessageTarget::TYPE_CHANNEL:
+                       {
+                               Channel* t = msgtarget.Get<Channel>();
+                               if (exemptedchans.count(t->name))
+                                       return MOD_RES_PASSTHRU;
+                               break;
+                       }
+                       case MessageTarget::TYPE_SERVER:
                                return MOD_RES_PASSTHRU;
+               }
 
-                       target = t->nick;
+               if (is_selfmsg && warnonselfmsg)
+               {
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("WARNING: %s's self message matched %s (%s)",
+                               user->nick.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       return MOD_RES_PASSTHRU;
                }
-               else if (msgtarget.type == MessageTarget::TYPE_CHANNEL)
+               else if (f->action == FA_WARN)
                {
-                       Channel* t = msgtarget.Get<Channel>();
-                       if (exemptedchans.count(t->name))
-                               return MOD_RES_PASSTHRU;
-
-                       target = t->name;
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("WARNING: %s's message to %s matched %s (%s)",
+                               user->nick.c_str(), msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       return MOD_RES_PASSTHRU;
                }
-               if (f->action == FA_BLOCK)
+               else if (f->action == FA_BLOCK)
                {
                        ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s had their message to %s filtered as it matched %s (%s)",
-                               user->nick.c_str(), target.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                               user->nick.c_str(), msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
                        if (notifyuser)
                        {
                                if (msgtarget.type == MessageTarget::TYPE_CHANNEL)
-                                       user->WriteNumeric(ERR_CANNOTSENDTOCHAN, target, InspIRCd::Format("Message to channel blocked and opers notified (%s)", f->reason.c_str()));
+                                       user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<Channel>(), InspIRCd::Format("Your message to this channel was blocked: %s.", f->reason.c_str())));
                                else
-                                       user->WriteNotice("Your message to "+target+" was blocked and opers notified: "+f->reason);
+                                       user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<User>(), InspIRCd::Format("Your message to this user was blocked: %s.", f->reason.c_str())));
                        }
                        else
                                details.echo_original = true;
@@ -382,9 +449,9 @@ ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtar
                        if (notifyuser)
                        {
                                if (msgtarget.type == MessageTarget::TYPE_CHANNEL)
-                                       user->WriteNumeric(ERR_CANNOTSENDTOCHAN, target, InspIRCd::Format("Message to channel blocked (%s)", f->reason.c_str()));
+                                       user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<Channel>(), InspIRCd::Format("Your message to this channel was blocked: %s.", f->reason.c_str())));
                                else
-                                       user->WriteNotice("Your message to "+target+" was blocked: "+f->reason);
+                                       user->WriteNumeric(Numerics::CannotSendTo(msgtarget.Get<User>(), InspIRCd::Format("Your message to this user was blocked: %s.", f->reason.c_str())));
                        }
                        else
                                details.echo_original = true;
@@ -392,14 +459,16 @@ ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtar
                else if (f->action == FA_KILL)
                {
                        ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s was killed because their message to %s matched %s (%s)",
-                               user->nick.c_str(), target.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                               user->nick.c_str(), msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
                        ServerInstance->Users->QuitUser(user, "Filtered: " + f->reason);
                }
                else if (f->action == FA_SHUN && (ServerInstance->XLines->GetFactory("SHUN")))
                {
                        Shun* sh = new Shun(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
-                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s was shunned because their message to %s matched %s (%s)",
-                               user->nick.c_str(), target.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was shunned for %s (expires on %s) because their message to %s matched %s (%s)",
+                               user->nick.c_str(), sh->Displayable().c_str(), InspIRCd::DurationString(f->duration).c_str(),
+                               InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                               msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
                        if (ServerInstance->XLines->AddLine(sh, NULL))
                        {
                                ServerInstance->XLines->ApplyLines();
@@ -410,8 +479,10 @@ ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtar
                else if (f->action == FA_GLINE)
                {
                        GLine* gl = new GLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), "*", user->GetIPString());
-                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s was glined because their message to %s matched %s (%s)",
-                               user->nick.c_str(), target.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was G-lined for %s (expires on %s) because their message to %s matched %s (%s)",
+                               user->nick.c_str(), gl->Displayable().c_str(), InspIRCd::DurationString(f->duration).c_str(),
+                               InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                               msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
                        if (ServerInstance->XLines->AddLine(gl,NULL))
                        {
                                ServerInstance->XLines->ApplyLines();
@@ -422,8 +493,10 @@ ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtar
                else if (f->action == FA_ZLINE)
                {
                        ZLine* zl = new ZLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
-                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s was zlined because their message to %s matched %s (%s)",
-                               user->nick.c_str(), target.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                       ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was Z-lined for %s (expires on %s) because their message to %s matched %s (%s)",
+                               user->nick.c_str(), zl->Displayable().c_str(), InspIRCd::DurationString(f->duration).c_str(),
+                               InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                               msgtarget.GetName().c_str(), f->freeform.c_str(), f->reason.c_str()));
                        if (ServerInstance->XLines->AddLine(zl,NULL))
                        {
                                ServerInstance->XLines->ApplyLines();
@@ -432,7 +505,7 @@ ModResult ModuleFilter::OnUserPreMessage(User* user, const MessageTarget& msgtar
                                delete zl;
                }
 
-               ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, user->nick + " had their message filtered, target was " + target + ": " + f->reason + " Action: " + ModuleFilter::FilterActionToString(f->action));
+               ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, user->nick + " had their message filtered, target was " + msgtarget.GetName() + ": " + f->reason + " Action: " + ModuleFilter::FilterActionToString(f->action));
                return MOD_RES_DENY;
        }
        return MOD_RES_PASSTHRU;
@@ -478,10 +551,10 @@ ModResult ModuleFilter::OnPreCommand(std::string& command, CommandBase::Params&
                /* We cant block a part or quit, so instead we change the reason to 'Reason filtered' */
                parameters[parting ? 1 : 0] = "Reason filtered";
 
-               /* We're blocking, OR theyre quitting and its a KILL action
+               /* We're warning or blocking, OR they're quitting and its a KILL action
                 * (we cant kill someone whos already quitting, so filter them anyway)
                 */
-               if ((f->action == FA_BLOCK) || (((!parting) && (f->action == FA_KILL))) || (f->action == FA_SILENT))
+               if ((f->action == FA_WARN) || (f->action == FA_BLOCK) || (((!parting) && (f->action == FA_KILL))) || (f->action == FA_SILENT))
                {
                        return MOD_RES_PASSTHRU;
                }
@@ -495,10 +568,13 @@ ModResult ModuleFilter::OnPreCommand(std::string& command, CommandBase::Params&
                        }
                        if (f->action == FA_GLINE)
                        {
-                               /* Note: We gline *@IP so that if their host doesnt resolve the gline still applies. */
+                               /* Note: We G-line *@IP so that if their host doesn't resolve the G-line still applies. */
                                GLine* gl = new GLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), "*", user->GetIPString());
-                               ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s was glined because their %s message matched %s (%s)",
-                                       user->nick.c_str(), command.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                               ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was G-lined for %s (expires on %s) because their %s message matched %s (%s)",
+                                       user->nick.c_str(), gl->Displayable().c_str(),
+                                       InspIRCd::DurationString(f->duration).c_str(),
+                                       InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                                       command.c_str(), f->freeform.c_str(), f->reason.c_str()));
 
                                if (ServerInstance->XLines->AddLine(gl,NULL))
                                {
@@ -510,8 +586,11 @@ ModResult ModuleFilter::OnPreCommand(std::string& command, CommandBase::Params&
                        if (f->action == FA_ZLINE)
                        {
                                ZLine* zl = new ZLine(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
-                               ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s was zlined because their %s message matched %s (%s)",
-                                       user->nick.c_str(), command.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                               ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was Z-lined for %s (expires on %s) because their %s message matched %s (%s)",
+                                       user->nick.c_str(), zl->Displayable().c_str(),
+                                       InspIRCd::DurationString(f->duration).c_str(),
+                                       InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                                       command.c_str(), f->freeform.c_str(), f->reason.c_str()));
 
                                if (ServerInstance->XLines->AddLine(zl,NULL))
                                {
@@ -524,8 +603,12 @@ ModResult ModuleFilter::OnPreCommand(std::string& command, CommandBase::Params&
                        {
                                /* Note: We shun *!*@IP so that if their host doesnt resolve the shun still applies. */
                                Shun* sh = new Shun(ServerInstance->Time(), f->duration, ServerInstance->Config->ServerName.c_str(), f->reason.c_str(), user->GetIPString());
-                               ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s was shunned because their %s message matched %s (%s)",
-                                       user->nick.c_str(), command.c_str(), f->freeform.c_str(), f->reason.c_str()));
+                               ServerInstance->SNO->WriteGlobalSno('f', InspIRCd::Format("%s (%s) was shunned for %s (expires on %s) because their %s message matched %s (%s)",
+                                       user->nick.c_str(), sh->Displayable().c_str(),
+                                       InspIRCd::DurationString(f->duration).c_str(),
+                                       InspIRCd::TimeString(ServerInstance->Time() + f->duration).c_str(),
+                                       command.c_str(), f->freeform.c_str(), f->reason.c_str()));
+
                                if (ServerInstance->XLines->AddLine(sh, NULL))
                                {
                                        ServerInstance->XLines->ApplyLines();
@@ -550,7 +633,7 @@ void ModuleFilter::ReadConfig(ConfigStatus& status)
                ConfigTag* tag = i->second;
 
                // If "target" is not found, try the old "channel" key to keep compatibility with 2.0 configs
-               const std::string target = tag->getString("target", tag->getString("channel"));
+               const std::string target = tag->getString("target", tag->getString("channel"), 1);
                if (!target.empty())
                {
                        if (target[0] == '#')
@@ -563,6 +646,11 @@ void ModuleFilter::ReadConfig(ConfigStatus& status)
        ConfigTag* tag = ServerInstance->Config->ConfValue("filteropts");
        std::string newrxengine = tag->getString("engine");
        notifyuser = tag->getBool("notifyuser", true);
+       warnonselfmsg = tag->getBool("warnonselfmsg");
+       filterconf = tag->getString("filename");
+       if (!filterconf.empty())
+               filterconf = ServerInstance->Config->Paths.PrependConfig(filterconf);
+       SetInterval(tag->getDuration("saveperiod", 5));
 
        factory = RegexEngine ? (RegexEngine.operator->()) : NULL;
 
@@ -595,7 +683,7 @@ void ModuleFilter::ReadConfig(ConfigStatus& status)
 
 Version ModuleFilter::GetVersion()
 {
-       return Version("Text (spam) filtering", VF_VENDOR | VF_COMMON, RegexEngine ? RegexEngine->name : "");
+       return Version("Adds the /FILTER command which allows server operators to define regex matches for inappropriate phrases that are not allowed to be used in channel messages, private messages, part messages, or quit messages.", VF_VENDOR | VF_COMMON, RegexEngine ? RegexEngine->name : "");
 }
 
 std::string ModuleFilter::EncodeFilter(FilterResult* filter)
@@ -630,7 +718,7 @@ FilterResult ModuleFilter::DecodeFilter(const std::string &data)
 
        std::string duration;
        tokens.GetMiddle(duration);
-       res.duration = ConvToInt(duration);
+       res.duration = ConvToNum<unsigned long>(duration);
 
        tokens.GetTrailing(res.reason);
 
@@ -695,21 +783,23 @@ FilterResult* ModuleFilter::FilterMatch(User* user, const std::string &text, int
        return NULL;
 }
 
-bool ModuleFilter::DeleteFilter(const std::string &freeform)
+bool ModuleFilter::DeleteFilter(const std::string& freeform, std::string& reason)
 {
        for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
        {
                if (i->freeform == freeform)
                {
+                       reason.assign(i->reason);
                        delete i->regex;
                        filters.erase(i);
+                       dirty = true;
                        return true;
                }
        }
        return false;
 }
 
-std::pair<bool, std::string> ModuleFilter::AddFilter(const std::string &freeform, FilterAction type, const std::string &reason, long duration, const std::string &flgs)
+std::pair<bool, std::string> ModuleFilter::AddFilter(const std::string& freeform, FilterAction type, const std::string& reason, unsigned long duration, const std::string& flgs, bool config)
 {
        for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
        {
@@ -721,7 +811,8 @@ std::pair<bool, std::string> ModuleFilter::AddFilter(const std::string &freeform
 
        try
        {
-               filters.push_back(FilterResult(RegexEngine, freeform, reason, type, duration, flgs, false));
+               filters.push_back(FilterResult(RegexEngine, freeform, reason, type, duration, flgs, config));
+               dirty = true;
        }
        catch (ModuleException &e)
        {
@@ -737,6 +828,8 @@ bool ModuleFilter::StringToFilterAction(const std::string& str, FilterAction& fa
                fa = FA_GLINE;
        else if (stdalgo::string::equalsci(str, "zline"))
                fa = FA_ZLINE;
+       else if (stdalgo::string::equalsci(str, "warn"))
+               fa = FA_WARN;
        else if (stdalgo::string::equalsci(str, "block"))
                fa = FA_BLOCK;
        else if (stdalgo::string::equalsci(str, "silent"))
@@ -759,6 +852,7 @@ std::string ModuleFilter::FilterActionToString(FilterAction fa)
        {
                case FA_GLINE:  return "gline";
                case FA_ZLINE:  return "zline";
+               case FA_WARN:   return "warn";
                case FA_BLOCK:  return "block";
                case FA_SILENT: return "silent";
                case FA_KILL:   return "kill";
@@ -769,11 +863,13 @@ std::string ModuleFilter::FilterActionToString(FilterAction fa)
 
 void ModuleFilter::ReadFilters()
 {
+       insp::flat_set<std::string> removedfilters;
+
        for (std::vector<FilterResult>::iterator filter = filters.begin(); filter != filters.end(); )
        {
                if (filter->from_config)
                {
-                       ServerInstance->SNO->WriteGlobalSno('f', "FILTER: removing filter '" + filter->freeform + "' due to config rehash.");
+                       removedfilters.insert(filter->freeform);
                        delete filter->regex;
                        filter = filters.erase(filter);
                        continue;
@@ -798,15 +894,17 @@ void ModuleFilter::ReadFilters()
                if (!StringToFilterAction(action, fa))
                        fa = FA_NONE;
 
-               try
-               {
-                       filters.push_back(FilterResult(RegexEngine, pattern, reason, fa, duration, flgs, true));
-                       ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Regular expression %s loaded.", pattern.c_str());
-               }
-               catch (ModuleException &e)
-               {
-                       ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Error in regular expression '%s': %s", pattern.c_str(), e.GetReason().c_str());
-               }
+               std::pair<bool, std::string> result = static_cast<ModuleFilter*>(this)->AddFilter(pattern, fa, reason, duration, flgs, !i->second->getBool("generated"));
+               if (result.first)
+                       removedfilters.erase(pattern);
+               else
+                       ServerInstance->Logs->Log(MODNAME, LOG_DEFAULT, "Filter '%s' could not be added: %s", pattern.c_str(), result.second.c_str());
+       }
+
+       if (!removedfilters.empty())
+       {
+               for (insp::flat_set<std::string>::const_iterator it = removedfilters.begin(); it != removedfilters.end(); ++it)
+                       ServerInstance->SNO->WriteGlobalSno('f', "Removing filter '" + *(it) + "' due to config rehash.");
        }
 }
 
@@ -816,7 +914,7 @@ ModResult ModuleFilter::OnStats(Stats::Context& stats)
        {
                for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); i++)
                {
-                       stats.AddRow(223, RegexEngine.GetProvider()+":"+i->freeform+" "+i->GetFlags()+" "+FilterActionToString(i->action)+" "+ConvToStr(i->duration)+" :"+i->reason);
+                       stats.AddRow(223, RegexEngine.GetProvider(), i->freeform, i->GetFlags(), FilterActionToString(i->action), i->duration, i->reason);
                }
                for (ExemptTargetSet::const_iterator i = exemptedchans.begin(); i != exemptedchans.end(); ++i)
                {
@@ -844,4 +942,72 @@ void ModuleFilter::OnUnloadModule(Module* mod)
        }
 }
 
+bool ModuleFilter::Tick(time_t)
+{
+               if (!dirty) // No need to write.
+                       return true;
+
+               if (filterconf.empty()) // Nothing to write to.
+               {
+                       dirty = false;
+                       return true;
+               }
+
+               const std::string newfilterconf = filterconf + ".tmp";
+               std::ofstream stream(newfilterconf.c_str());
+               if (!stream.is_open()) // Filesystem probably not writable.
+               {
+                       ServerInstance->SNO->WriteToSnoMask('f', "Unable to save filters to \"%s\": %s (%d)",
+                                       newfilterconf.c_str(), strerror(errno), errno);
+                       return true;
+               }
+
+               stream
+                       << "# This file was automatically generated by the " << INSPIRCD_VERSION << " filter module on " << InspIRCd::TimeString(ServerInstance->Time()) << "." << std::endl
+                       << "# Any changes to this file will be automatically overwritten." << std::endl
+                       << "# If you want to convert this to a normal config file you *MUST* remove the generated=\"yes\" keys!" << std::endl
+                       << std::endl
+                       << "<config format=\"xml\">" << std::endl;
+
+               for (std::vector<FilterResult>::iterator i = filters.begin(); i != filters.end(); ++i)
+               {
+                       // # <keyword reason="You qwertied!" action="block" flags="pn">
+                       const FilterResult& filter = (*i);
+                       if (filter.from_config)
+                               continue;
+
+                       stream << "<keyword generated=\"yes"
+                       << "\" pattern=\"" << ServerConfig::Escape(filter.freeform)
+                       << "\" reason=\"" << ServerConfig::Escape(filter.reason)
+                       << "\" action=\"" << FilterActionToString(filter.action)
+                       << "\" flags=\"" << filter.GetFlags();
+                       if (filter.duration)
+                               stream << "\" duration=\"" << InspIRCd::DurationString(filter.duration);
+                       stream << "\">" << std::endl;
+               }
+
+               if (stream.fail()) // Filesystem probably not writable.
+               {
+                       ServerInstance->SNO->WriteToSnoMask('f', "Unable to save filters to \"%s\": %s (%d)",
+                               newfilterconf.c_str(), strerror(errno), errno);
+                       return true;
+               }
+               stream.close();
+
+#ifdef _WIN32
+               remove(filterconf.c_str());
+#endif
+
+               // Use rename to move temporary to new db - this is guaranteed not to fuck up, even in case of a crash.
+               if (rename(newfilterconf.c_str(), filterconf.c_str()) < 0)
+               {
+                       ServerInstance->SNO->WriteToSnoMask('f', "Unable to replace old filter config \"%s\" with \"%s\": %s (%d)",
+                               filterconf.c_str(), newfilterconf.c_str(), strerror(errno), errno);
+                       return true;
+               }
+
+               dirty = false;
+               return true;
+}
+
 MODULE_INIT(ModuleFilter)