From c495b5d9cf8bed4f07c0b77a1f9e98dcc1f62068 Mon Sep 17 00:00:00 2001 From: Peter Powell Date: Thu, 6 Sep 2018 10:09:09 +0100 Subject: [PATCH] Implement support for IRCv3 client-to-client tags. --- docs/conf/modules.conf.example | 6 + include/modules/ctctags.h | 135 ++++++++++ src/coremods/core_serialize_rfc.cpp | 2 +- src/modules/m_delayjoin.cpp | 18 +- src/modules/m_ircv3_ctctags.cpp | 347 ++++++++++++++++++++++++++ src/modules/m_ircv3_echomessage.cpp | 47 +++- src/modules/m_spanningtree/compat.cpp | 5 + 7 files changed, 555 insertions(+), 5 deletions(-) create mode 100644 include/modules/ctctags.h create mode 100644 src/modules/m_ircv3_ctctags.cpp diff --git a/docs/conf/modules.conf.example b/docs/conf/modules.conf.example index 2fa3b5042..3f7e5a9f0 100644 --- a/docs/conf/modules.conf.example +++ b/docs/conf/modules.conf.example @@ -1112,6 +1112,12 @@ # extension will get the chghost message and won't see host cycling. # +#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# IRCv3 client-to-client tags module: Provides the message-tags IRCv3 +# extension which allows clients to add extra data to their messages. +# This is used to support new IRCv3 features such as replies and ids. +# + #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # IRCv3 echo-message module: Provides the echo-message IRCv3 # extension which allows capable clients to get an acknowledgement when diff --git a/include/modules/ctctags.h b/include/modules/ctctags.h new file mode 100644 index 000000000..d8798de54 --- /dev/null +++ b/include/modules/ctctags.h @@ -0,0 +1,135 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2019 Peter Powell + * + * 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 + * License as published by the Free Software Foundation, version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#pragma once + +#include "event.h" + +namespace CTCTags +{ + class EventListener; + class TagMessage; + class TagMessageDetails; +} + +class CTCTags::TagMessage : public ClientProtocol::Message +{ + public: + TagMessage(User* source, const Channel* targetchan, const ClientProtocol::TagMap& Tags) + : ClientProtocol::Message("TAGMSG", source) + { + PushParamRef(targetchan->name); + AddTags(Tags); + SetSideEffect(true); + } + + TagMessage(User* source, const User* targetuser, const ClientProtocol::TagMap& Tags) + : ClientProtocol::Message("TAGMSG", source) + { + if (targetuser->registered & REG_NICK) + PushParamRef(targetuser->nick); + else + PushParam("*"); + AddTags(Tags); + SetSideEffect(true); + } + + TagMessage(User* source, const char* targetstr, const ClientProtocol::TagMap& Tags) + : ClientProtocol::Message("TAGMSG", source) + { + PushParam(targetstr); + AddTags(Tags); + SetSideEffect(true); + } +}; + +class CTCTags::TagMessageDetails +{ + public: + /** Whether to echo the tags at all. */ + bool echo; + + /* Whether to send the original tags back to clients with echo-message support. */ + bool echo_original; + + /** The users who are exempted from receiving this message. */ + CUList exemptions; + + /** IRCv3 message tags sent to the server by the user. */ + const ClientProtocol::TagMap tags_in; + + /** IRCv3 message tags sent out to users who get this message. */ + ClientProtocol::TagMap tags_out; + + TagMessageDetails(const ClientProtocol::TagMap& tags) + : echo(true) + , echo_original(false) + , tags_in(tags) + { + } +}; + +class CTCTags::EventListener + : public Events::ModuleEventListener +{ + protected: + EventListener(Module* mod, unsigned int eventprio = DefaultPriority) + : ModuleEventListener(mod, "event/tagmsg", eventprio) + { + } + + public: + /** Called before a user sends a tag message to a channel, a user, or a server glob mask. + * @param user The user sending the message. + * @param target The target of the message. This can either be a channel, a user, or a server + * glob mask. + * @param details Details about the message such as the message tags or whether to echo. See the + * TagMessageDetails class for more information. + * @return MOD_RES_ALLOW to explicitly allow the message, MOD_RES_DENY to explicitly deny the + * message, or MOD_RES_PASSTHRU to let another module handle the event. + */ + virtual ModResult OnUserPreTagMessage(User* user, const MessageTarget& target, TagMessageDetails& details) { return MOD_RES_PASSTHRU; } + + /** Called immediately after a user sends a tag message to a channel, a user, or a server glob mask. + * @param user The user sending the message. + * @param target The target of the message. This can either be a channel, a user, or a server + * glob mask. + * @param details Details about the message such as the message tags or whether to echo. See the + * TagMessageDetails class for more information. + */ + virtual void OnUserPostTagMessage(User* user, const MessageTarget& target, const TagMessageDetails& details) { } + + /** Called immediately before a user sends a tag message to a channel, a user, or a server glob mask. + * @param user The user sending the message. + * @param target The target of the message. This can either be a channel, a user, or a server + * glob mask. + * @param details Details about the message such as the message tags or whether to echo. See the + * TagMessageDetails class for more information. + */ + virtual void OnUserTagMessage(User* user, const MessageTarget& target, const TagMessageDetails& details) { } + + /** Called when a tag message sent by a user to a channel, a user, or a server glob mask is blocked. + * @param user The user sending the message. + * @param target The target of the message. This can either be a channel, a user, or a server + * glob mask. + * @param details Details about the message such as the message tags or whether to echo. See the + * TagMessageDetails class for more information. + */ + virtual void OnUserTagMessageBlocked(User* user, const MessageTarget& target, const TagMessageDetails& details) { } +}; diff --git a/src/coremods/core_serialize_rfc.cpp b/src/coremods/core_serialize_rfc.cpp index 6b693bfb9..b8d075ab6 100644 --- a/src/coremods/core_serialize_rfc.cpp +++ b/src/coremods/core_serialize_rfc.cpp @@ -32,7 +32,7 @@ class RFCSerializer : public ClientProtocol::Serializer static const std::string::size_type MAX_CLIENT_MESSAGE_TAG_LENGTH = 4095; /** The maximum size of server-originated message tags in an outgoing message including the `@`. */ - static const std::string::size_type MAX_SERVER_MESSAGE_TAG_LENGTH = 511; + static const std::string::size_type MAX_SERVER_MESSAGE_TAG_LENGTH = 4095; static void SerializeTags(const ClientProtocol::TagMap& tags, const ClientProtocol::TagSelection& tagwl, std::string& line); diff --git a/src/modules/m_delayjoin.cpp b/src/modules/m_delayjoin.cpp index 8b06f060a..469f33439 100644 --- a/src/modules/m_delayjoin.cpp +++ b/src/modules/m_delayjoin.cpp @@ -21,6 +21,7 @@ #include "inspircd.h" +#include "modules/ctctags.h" class DelayJoinMode : public ModeHandler { @@ -72,7 +73,9 @@ class JoinHook : public ClientProtocol::EventHook } -class ModuleDelayJoin : public Module +class ModuleDelayJoin + : public Module + , public CTCTags::EventListener { public: LocalIntExt unjoined; @@ -80,7 +83,8 @@ class ModuleDelayJoin : public Module DelayJoinMode djm; ModuleDelayJoin() - : unjoined("delayjoin", ExtensionItem::EXT_MEMBERSHIP, this) + : CTCTags::EventListener(this) + , unjoined("delayjoin", ExtensionItem::EXT_MEMBERSHIP, this) , joinhook(this, unjoined) , djm(this, unjoined) { @@ -94,6 +98,7 @@ class ModuleDelayJoin : public Module void OnUserKick(User* source, Membership*, const std::string &reason, CUList&) CXX11_OVERRIDE; void OnBuildNeighborList(User* source, IncludeChanList& include, std::map& exception) CXX11_OVERRIDE; void OnUserMessage(User* user, const MessageTarget& target, const MessageDetails& details) CXX11_OVERRIDE; + void OnUserTagMessage(User* user, const MessageTarget& target, const CTCTags::TagMessageDetails& details) CXX11_OVERRIDE; ModResult OnRawMode(User* user, Channel* channel, ModeHandler* mh, const std::string& param, bool adding) CXX11_OVERRIDE; }; @@ -176,6 +181,15 @@ void ModuleDelayJoin::OnBuildNeighborList(User* source, IncludeChanList& include } } +void ModuleDelayJoin::OnUserTagMessage(User* user, const MessageTarget& target, const CTCTags::TagMessageDetails& details) +{ + if (target.type != MessageTarget::TYPE_CHANNEL) + return; + + Channel* channel = target.Get(); + djm.RevealUser(user, channel); +} + void ModuleDelayJoin::OnUserMessage(User* user, const MessageTarget& target, const MessageDetails& details) { if (target.type != MessageTarget::TYPE_CHANNEL) diff --git a/src/modules/m_ircv3_ctctags.cpp b/src/modules/m_ircv3_ctctags.cpp new file mode 100644 index 000000000..8684642c6 --- /dev/null +++ b/src/modules/m_ircv3_ctctags.cpp @@ -0,0 +1,347 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2019 Peter Powell + * Copyright (C) 2016 Attila Molnar + * + * 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 + * License as published by the Free Software Foundation, version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#include "inspircd.h" +#include "modules/cap.h" +#include "modules/ctctags.h" + +class CommandTagMsg : public Command +{ + private: + Cap::Capability& cap; + ChanModeReference moderatedmode; + ChanModeReference noextmsgmode; + Events::ModuleEventProvider tagevprov; + ClientProtocol::EventProvider msgevprov; + + bool FirePreEvents(User* source, MessageTarget& msgtarget, CTCTags::TagMessageDetails& msgdetails) + { + // Inform modules that a TAGMSG wants to be sent. + ModResult modres; + FIRST_MOD_RESULT_CUSTOM(tagevprov, CTCTags::EventListener, OnUserPreTagMessage, modres, (source, msgtarget, msgdetails)); + if (modres == MOD_RES_DENY) + { + // Inform modules that a module blocked the TAGMSG. + FOREACH_MOD_CUSTOM(tagevprov, CTCTags::EventListener, OnUserTagMessageBlocked, (source, msgtarget, msgdetails)); + return false; + } + + // Inform modules that a TAGMSG is about to be sent. + FOREACH_MOD_CUSTOM(tagevprov, CTCTags::EventListener, OnUserTagMessage, (source, msgtarget, msgdetails)); + return true; + } + + CmdResult FirePostEvent(User* source, const MessageTarget& msgtarget, const CTCTags::TagMessageDetails& msgdetails) + { + // If the source is local then update its idle time. + LocalUser* lsource = IS_LOCAL(source); + if (lsource) + lsource->idle_lastmsg = ServerInstance->Time(); + + // Inform modules that a TAGMSG was sent. + FOREACH_MOD_CUSTOM(tagevprov, CTCTags::EventListener, OnUserPostTagMessage, (source, msgtarget, msgdetails)); + return CMD_SUCCESS; + } + + CmdResult HandleChannelTarget(User* source, const Params& parameters, const char* target, PrefixMode* pm) + { + Channel* chan = ServerInstance->FindChan(target); + if (!chan) + { + // The target channel does not exist. + source->WriteNumeric(Numerics::NoSuchChannel(parameters[0])); + return CMD_FAILURE; + } + + if (IS_LOCAL(source)) + { + if (chan->IsModeSet(noextmsgmode) && !chan->HasUser(source)) + { + // The noextmsg mode is set and the source is not in the channel. + source->WriteNumeric(ERR_CANNOTSENDTOCHAN, chan->name, "Cannot send to channel (no external messages)"); + return CMD_FAILURE; + } + + bool no_chan_priv = chan->GetPrefixValue(source) < VOICE_VALUE; + if (no_chan_priv && chan->IsModeSet(moderatedmode)) + { + // The moderated mode is set and the source has no status rank. + source->WriteNumeric(ERR_CANNOTSENDTOCHAN, chan->name, "Cannot send to channel (+m)"); + return CMD_FAILURE; + } + + if (no_chan_priv && ServerInstance->Config->RestrictBannedUsers != ServerConfig::BUT_NORMAL && chan->IsBanned(source)) + { + // The source is banned in the channel and restrictbannedusers is enabled. + if (ServerInstance->Config->RestrictBannedUsers == ServerConfig::BUT_RESTRICT_NOTIFY) + source->WriteNumeric(ERR_CANNOTSENDTOCHAN, chan->name, "Cannot send to channel (you're banned)"); + return CMD_FAILURE; + } + } + + // Fire the pre-message events. + MessageTarget msgtarget(chan, pm ? pm->GetPrefix() : 0); + CTCTags::TagMessageDetails msgdetails(parameters.GetTags()); + if (!FirePreEvents(source, msgtarget, msgdetails)) + return CMD_FAILURE; + + unsigned int minrank = pm ? pm->GetPrefixRank() : 0; + CTCTags::TagMessage message(source, chan, parameters.GetTags()); + const Channel::MemberMap& userlist = chan->GetUsers(); + for (Channel::MemberMap::const_iterator iter = userlist.begin(); iter != userlist.end(); ++iter) + { + LocalUser* luser = IS_LOCAL(iter->first); + + // Don't send to remote users or the user who is the source. + if (!luser || luser == source) + continue; + + // Don't send to unprivileged or exempt users. + if (iter->second->getRank() < minrank || msgdetails.exemptions.count(luser)) + continue; + + // Send to users if they have the capability. + if (cap.get(luser)) + luser->Send(msgevprov, message); + } + return FirePostEvent(source, msgtarget, msgdetails); + } + + CmdResult HandleServerTarget(User* source, const Params& parameters) + { + // If the source isn't allowed to mass message users then reject + // the attempt to mass-message users. + if (!source->HasPrivPermission("users/mass-message")) + return CMD_FAILURE; + + // Extract the server glob match from the target parameter. + std::string servername(parameters[0], 1); + + // Fire the pre-message events. + MessageTarget msgtarget(&servername); + CTCTags::TagMessageDetails msgdetails(parameters.GetTags()); + if (!FirePreEvents(source, msgtarget, msgdetails)) + return CMD_FAILURE; + + // If the current server name matches the server name glob then send + // the message out to the local users. + if (InspIRCd::Match(ServerInstance->Config->ServerName, servername)) + { + CTCTags::TagMessage message(source, "$*", parameters.GetTags()); + const UserManager::LocalList& list = ServerInstance->Users.GetLocalUsers(); + for (UserManager::LocalList::const_iterator iter = list.begin(); iter != list.end(); ++iter) + { + LocalUser* luser = IS_LOCAL(*iter); + + // Don't send to unregistered users or the user who is the source. + if (luser->registered != REG_ALL || luser == source) + continue; + + // Don't send to exempt users. + if (msgdetails.exemptions.count(luser)) + continue; + + // Send to users if they have the capability. + if (cap.get(luser)) + luser->Send(msgevprov, message); + } + } + + // Fire the post-message event. + return FirePostEvent(source, msgtarget, msgdetails); + } + + CmdResult HandleUserTarget(User* source, const Params& parameters) + { + User* target; + if (IS_LOCAL(source)) + { + // Local sources can specify either a nick or a nick@server mask as the target. + const char* targetserver = strchr(parameters[0].c_str(), '@'); + if (targetserver) + { + // The target is a user on a specific server (e.g. jto@tolsun.oulu.fi). + target = ServerInstance->FindNickOnly(parameters[0].substr(0, targetserver - parameters[0].c_str())); + if (target && strcasecmp(target->server->GetName().c_str(), targetserver + 1)) + target = NULL; + } + else + { + // If the source is a local user then we only look up the target by nick. + target = ServerInstance->FindNickOnly(parameters[0]); + } + } + else + { + // Remote users can only specify a nick or UUID as the target. + target = ServerInstance->FindNick(parameters[0]); + } + + if (!target || target->registered != REG_ALL) + { + // The target user does not exist or is not fully registered. + source->WriteNumeric(Numerics::NoSuchNick(parameters[0])); + return CMD_FAILURE; + } + + // Fire the pre-message events. + MessageTarget msgtarget(target); + CTCTags::TagMessageDetails msgdetails(parameters.GetTags()); + if (!FirePreEvents(source, msgtarget, msgdetails)) + return CMD_FAILURE; + + LocalUser* const localtarget = IS_LOCAL(target); + if (localtarget && cap.get(localtarget)) + { + // Send to the target if they have the capability and are a local user. + CTCTags::TagMessage message(source, localtarget, parameters.GetTags()); + localtarget->Send(msgevprov, message); + } + + // Fire the post-message event. + return FirePostEvent(source, msgtarget, msgdetails); + } + + public: + CommandTagMsg(Module* Creator, Cap::Capability& Cap) + : Command(Creator, "TAGMSG", 1) + , cap(Cap) + , moderatedmode(Creator, "moderated") + , noextmsgmode(Creator, "noextmsg") + , tagevprov(Creator, "event/tagmsg") + , msgevprov(Creator, "TAGMSG") + { + allow_empty_last_param = false; + } + + CmdResult Handle(User* user, const Params& parameters) CXX11_OVERRIDE + { + if (CommandParser::LoopCall(user, this, parameters, 0)) + return CMD_SUCCESS; + + // Check that the source has the message tags capability. + if (IS_LOCAL(user) && !cap.get(user)) + return CMD_FAILURE; + + // The target is a server glob. + if (parameters[0][0] == '$') + return HandleServerTarget(user, parameters); + + // If the message begins with a status character then look it up. + const char* target = parameters[0].c_str(); + PrefixMode* pmh = ServerInstance->Modes->FindPrefix(target[0]); + if (pmh) + target++; + + // The target is a channel name. + if (*target == '#') + return HandleChannelTarget(user, parameters, target, pmh); + + // The target is a nickname. + return HandleUserTarget(user, parameters); + } + + RouteDescriptor GetRouting(User* user, const Params& parameters) CXX11_OVERRIDE + { + return ROUTE_MESSAGE(parameters[0]); + } +}; + +class C2CTags : public ClientProtocol::MessageTagProvider +{ + private: + Cap::Capability& cap; + + public: + C2CTags(Module* Creator, Cap::Capability& Cap) + : ClientProtocol::MessageTagProvider(Creator) + , cap(Cap) + { + } + + ModResult OnProcessTag(User* user, const std::string& tagname, std::string& tagvalue) CXX11_OVERRIDE + { + // A client-only tag is prefixed with a plus sign (+) and otherwise conforms + // to the format specified in IRCv3.2 tags. + if (tagname[0] != '+') + return MOD_RES_PASSTHRU; + + // If the user is local then we check whether they have the message-tags cap + // enabled. If not then we reject all client-only tags originating from them. + LocalUser* lu = IS_LOCAL(user); + if (lu && !cap.get(lu)) + return MOD_RES_DENY; + + // Remote users have their client-only tags checked by their local server. + return MOD_RES_ALLOW; + } + + bool ShouldSendTag(LocalUser* user, const ClientProtocol::MessageTagData& tagdata) CXX11_OVERRIDE + { + return cap.get(user); + } +}; + +class ModuleIRCv3CTCTags + : public Module + , public CTCTags::EventListener +{ + private: + Cap::Capability cap; + CommandTagMsg cmd; + C2CTags c2ctags; + + ModResult CopyClientTags(const ClientProtocol::TagMap& tags_in, ClientProtocol::TagMap& tags_out) + { + for (ClientProtocol::TagMap::const_iterator i = tags_in.begin(); i != tags_in.end(); ++i) + { + const ClientProtocol::MessageTagData& tagdata = i->second; + if (tagdata.tagprov == &c2ctags) + tags_out.insert(*i); + } + return MOD_RES_PASSTHRU; + } + + public: + ModuleIRCv3CTCTags() + : CTCTags::EventListener(this) + , cap(this, "message-tags") + , cmd(this, cap) + , c2ctags(this, cap) + { + } + + ModResult OnUserPreMessage(User* user, const MessageTarget& target, MessageDetails& details) CXX11_OVERRIDE + { + return CopyClientTags(details.tags_in, details.tags_out); + } + + ModResult OnUserPreTagMessage(User* user, const MessageTarget& target, CTCTags::TagMessageDetails& details) CXX11_OVERRIDE + { + return CopyClientTags(details.tags_in, details.tags_out); + } + + Version GetVersion() CXX11_OVERRIDE + { + return Version("Provides the DRAFT message-tags IRCv3 extension", VF_VENDOR | VF_COMMON); + } +}; + +MODULE_INIT(ModuleIRCv3CTCTags) diff --git a/src/modules/m_ircv3_echomessage.cpp b/src/modules/m_ircv3_echomessage.cpp index b407aece4..3ec534e91 100644 --- a/src/modules/m_ircv3_echomessage.cpp +++ b/src/modules/m_ircv3_echomessage.cpp @@ -20,14 +20,21 @@ #include "inspircd.h" #include "modules/cap.h" +#include "modules/ctctags.h" -class ModuleIRCv3EchoMessage : public Module +class ModuleIRCv3EchoMessage + : public Module + , public CTCTags::EventListener { + private: Cap::Capability cap; + ClientProtocol::EventProvider tagmsgprov; public: ModuleIRCv3EchoMessage() - : cap(this, "echo-message") + : CTCTags::EventListener(this) + , cap(this, "echo-message") + , tagmsgprov(this, "TAGMSG") { } @@ -64,6 +71,35 @@ class ModuleIRCv3EchoMessage : public Module } } + void OnUserPostTagMessage(User* user, const MessageTarget& target, const CTCTags::TagMessageDetails& details) CXX11_OVERRIDE + { + if (!cap.get(user) || !details.echo) + return; + + // Caps are only set on local users + LocalUser* const localuser = static_cast(user); + + const ClientProtocol::TagMap& tags = details.echo_original ? details.tags_in : details.tags_out; + if (target.type == MessageTarget::TYPE_USER) + { + User* destuser = target.Get(); + CTCTags::TagMessage message(user, destuser, tags); + localuser->Send(tagmsgprov, message); + } + else if (target.type == MessageTarget::TYPE_CHANNEL) + { + Channel* chan = target.Get(); + CTCTags::TagMessage message(user, chan, tags); + localuser->Send(tagmsgprov, message); + } + else + { + const std::string* servername = target.Get(); + CTCTags::TagMessage message(user, servername->c_str(), tags); + localuser->Send(tagmsgprov, message); + } + } + void OnUserMessageBlocked(User* user, const MessageTarget& target, const MessageDetails& details) CXX11_OVERRIDE { // Prevent spammers from knowing that their spam was blocked. @@ -71,6 +107,13 @@ class ModuleIRCv3EchoMessage : public Module OnUserPostMessage(user, target, details); } + void OnUserTagMessageBlocked(User* user, const MessageTarget& target, const CTCTags::TagMessageDetails& details) CXX11_OVERRIDE + { + // Prevent spammers from knowing that their spam was blocked. + if (details.echo_original) + OnUserPostTagMessage(user, target, details); + } + Version GetVersion() CXX11_OVERRIDE { return Version("Provides the echo-message IRCv3 extension", VF_VENDOR); diff --git a/src/modules/m_spanningtree/compat.cpp b/src/modules/m_spanningtree/compat.cpp index 17bc7cbc6..17b44f896 100644 --- a/src/modules/m_spanningtree/compat.cpp +++ b/src/modules/m_spanningtree/compat.cpp @@ -309,6 +309,11 @@ void TreeSocket::WriteLine(const std::string& original_line) push.append(line, 26, std::string::npos); push.swap(line); } + else if (command == "TAGMSG") + { + // Drop IRCv3 tag messages as v2 has no message tag support. + return; + } } WriteLineNoCompat(line); return; -- 2.39.2