FlightGear next
mpirc.cxx
Go to the documentation of this file.
1
2//
3// mpirc.cxx
4//
5// started November 2020
6// Authors: Michael Filhol, Henning Stahlke
7//
8// This program is free software; you can redistribute it and/or
9// modify it under the terms of the GNU General Public License as
10// published by the Free Software Foundation; either version 2 of the
11// License, or (at your option) any later version.
12//
13// This program is distributed in the hope that it will be useful, but
14// WITHOUT ANY WARRANTY; without even the implied warranty of
15// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16// General Public License for more details.
17//
18// You should have received a copy of the GNU General Public License
19// along with this program; if not, write to the Free Software
20// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21//
22// $Id$
23//
25
26#include "mpirc.hxx"
27#include <Main/fg_props.hxx>
28
29const std::string IRC_TEST_CHANNEL{"#mptest"}; // for development
30const std::string IRC_MSG_TERMINATOR {"\r\n"};
31// https://www.alien.net.au/irc/irc2numerics.html
32const std::string IRC_RPL_WELCOME {"001"};
33const std::string IRC_RPL_YOURID {"042"};
34const std::string IRC_RPL_MOTD {"372"};
35const std::string IRC_RPL_MOTDSTART {"375"};
36const std::string IRC_RPL_ENDOFMOTD {"376"};
37const std::string IRC_ERR_NOSUCHNICK {"401"};
38
39IRCConnection::IRCConnection(const std::string &nickname, const std::string &servername, const std::string &port) : SGSocket(servername, port, "tcp"),
40 _nickname(nickname)
41{
42}
43
47
48// setup properties to reflect the status of this IRC connection in the prop tree
49void IRCConnection::setupProperties(std::string path)
50{
51 if (path.back() != '/') path.push_back('/');
52 if (!_pReadyFlag) _pReadyFlag = fgGetNode(path + "irc-ready", true);
53 _pReadyFlag->setBoolValue(_logged_in);
54
55 if (!_pMessageCountIn) _pMessageCountIn = fgGetNode(path + "msg-count-in", true);
56 if (!_pMessageCountOut) _pMessageCountOut = fgGetNode(path + "msg-count-out", true);
57 if (!_pIRCReturnCode) _pIRCReturnCode = fgGetNode(path + "last-return-code", true);
58}
59
60
61bool IRCConnection::login(const std::string &nickname)
62{
63 if (!_connected && !connect()) {
64 return false;
65 }
66 if (!nickname.empty()) {
67 _nickname = nickname;
68 } else {
69 SG_LOG(SG_NETWORK, SG_WARN, "IRC login requires nickname argument.");
70 return false;
71 }
72
73 std::string lines("NICK ");
74 lines += _nickname;
75 lines += IRC_MSG_TERMINATOR;
76 lines += "USER ";
77 lines += _nickname; //IRC <user>
78 lines += " 0 * :"; //IRC <mode> <unused>
79 lines += _nickname; //IRC <realname>
80 lines += IRC_MSG_TERMINATOR;
81 return writestring(lines.c_str());
82}
83
84// login with nickname given to constructor
86{
87 return login(_nickname);
88}
89
90// the polite way to leave
92{
93 if (!_connected) return;
94 writestring("QUIT goodbye\r\n");
95 disconnect();
96}
97
98bool IRCConnection::sendPrivmsg(const std::string &recipient, const std::string &textline)
99{
100 if (!_logged_in) {
101 SG_LOG(SG_NETWORK, SG_WARN, "IRC 'privmsg' command unvailable. Login first!");
102 return false;
103 }
104 std::string line("PRIVMSG ");
105 line += recipient;
106 line += " :";
107 line += textline;
108 line += IRC_MSG_TERMINATOR;
109 if (writestring(line.c_str())) {
110 if (_pMessageCountOut) _pMessageCountOut->setIntValue(_pMessageCountOut->getIntValue() + 1);
111 if (_pIRCReturnCode) _pIRCReturnCode->setStringValue("");
112 return true;
113 } else {
114 SG_LOG(SG_NETWORK, SG_WARN, "IRC send privmsg failed.");
115 return false;
116 }
117}
118
119// join an IRC channel
120bool IRCConnection::join(const std::string &channel)
121{
122 if (!_logged_in) {
123 SG_LOG(SG_NETWORK, SG_WARN, "IRC 'join' command unvailable. Login first!");
124 return false;
125 }
126 std::string lines("JOIN ");
127 lines += channel;
128 lines += IRC_MSG_TERMINATOR;
129 return writestring(lines.c_str());
130}
131
132// leave an IRC channel
133bool IRCConnection::part(const std::string &channel)
134{
135 if (!_logged_in) {
136 SG_LOG(SG_NETWORK, SG_WARN, "IRC 'part' command unvailable. Login first!");
137 return false;
138 }
139 std::string lines("PART ");
140 lines += channel;
141 lines += IRC_MSG_TERMINATOR;
142 return writestring(lines.c_str());
143}
144
145/*
146 Call update() regularly to maintain connection (ping/pong) and process messages.
147 For information only:
148 The ping timeout appears to depend on the server settings and can be in the order
149 of minutes. However, for smooth message processing the update frequency should be
150 at least a few times per second and calling this at frame rate should not hurt.
151*/
153{
154 if (_connected && readline(_read_buffer, sizeof(_read_buffer) - 1) > 0) {
155 std::string line(_read_buffer); // TODO: buffer size check required?
156 parseReceivedLine(line);
157 }
158}
159
161// private methods
163
164// open a connection to IRC server
165bool IRCConnection::connect()
166{
167 if (_connected) {
168 return true;
169 }
170
171 _connected = open(SG_IO_OUT);
172 if (_connected) {
173 nonblock();
174 } else {
175 disconnect();
176 SG_LOG(SG_NETWORK, SG_WARN, "IRCConnection::connect error");
177 }
178 return _connected;
179}
180
181void IRCConnection::disconnect()
182{
183 _logged_in = false;
184 if (_pReadyFlag) _pReadyFlag->setBoolValue(_logged_in);
185
186 if (_connected) {
187 _connected = false;
188 close();
189 SG_LOG(SG_NETWORK, SG_INFO, "IRCConnection::disconnect");
190 }
191}
192
193void IRCConnection::pong(const std::string &recipient)
194{
195 if (!_connected) return;
196 std::string line("PONG ");
197 line += recipient;
198 line += IRC_MSG_TERMINATOR;
199 writestring(line.c_str());
200}
201
202bool IRCConnection::parseReceivedLine(std::string line)
203{
204 /*
205 https://tools.ietf.org/html/rfc2812#section-3.7.2
206 2.3.1 Message format in Augmented BNF
207
208 The protocol messages must be extracted from the contiguous stream of
209 octets. The current solution is to designate two characters, CR and
210 LF, as message separators. Empty messages are silently ignored,
211 which permits use of the sequence CR-LF between messages without
212 extra problems.
213
214 The extracted message is parsed into the components <prefix>,
215 <command> and list of parameters (<params>).
216
217 The Augmented BNF representation for this is:
218
219 message = [ ":" prefix SPACE ] command [ params ] crlf
220 prefix = servername / ( nickname [ [ "!" user ] "@" host ] )
221 command = 1*letter / 3digit
222 params = *14( SPACE middle ) [ SPACE ":" trailing ]
223 =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]
224
225 nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
226 ; any octet except NUL, CR, LF, " " and ":"
227 middle = nospcrlfcl *( ":" / nospcrlfcl )
228 trailing = *( ":" / " " / nospcrlfcl )
229
230 SPACE = %x20 ; space character
231 crlf = %x0D %x0A ; "carriage return" "linefeed"
232
233 */
234
235 // removes trailing '\r\n'
236 // TODO: for length(IRC_MSG_TERMINATOR) do line.pop_back();
237 line.pop_back();
238 line.pop_back();
239
240 std::string prefix;
241 std::string command;
242 std::string params;
243
244 std::size_t pos = line.find(" ", 1);
245 //prefix
246 if (line.at(0) == ':') {
247 prefix = line.substr(1, pos - 1); // remove leading ":"
248 std::size_t end = line.find(" ", pos + 1);
249 command = line.substr(pos + 1, end - pos - 1);
250 pos = end;
251 } else {
252 command = line.substr(0, pos);
253 }
254 params = line.substr(pos + 1);
255
256 // uncomment next line for debug output
257 //cout << "[prefix]" << prefix << "[cmd]" << command << "[params]" << params << "[end]" << endl;
258
259 // receiving a message
260 if (command == "PRIVMSG") {
261 if (_pMessageCountIn) _pMessageCountIn->setIntValue(_pMessageCountIn->getIntValue() + 1);
262 std::string recipient = params.substr(0, params.find(" :"));
263 // direct private message
264 if (recipient == _nickname) {
265 struct IRCMessage rcv;
266 rcv.sender = prefix.substr(0, prefix.find("!"));
267 rcv.textline = params.substr(params.find(":") + 1);
268 _incoming_private_messages.push_back(rcv);
269 } else {
270 // Most likely from an IRC channel if we joined any. In this case
271 // recipient equals channel name (e.g. "#mptest"). IRC channel
272 // support could be implemented here in future.
273 SG_LOG(SG_NETWORK, SG_DEV_WARN, "Ignoring PRIVMSG to '" + recipient + "' (should be '" + _nickname + "')");
274 }
275 } else if (command == "PING") {
276 // server pings us
277 std::string server = params.substr(0, params.find(" "));
278 pong(server);
279 } else if (command == "JOIN") {
280 // server acks our join request
281 std::string channel = params.substr(0, params.find(" "));
282 SG_LOG(SG_NETWORK, SG_DEV_WARN, "Joined IRC channel " + channel); //DEBUG
283 } else if (command == IRC_RPL_WELCOME) {
284 // after welcome we are logged in and allowed to send commands/messages to the IRC
285 _logged_in = true;
286 if (_pReadyFlag) _pReadyFlag->setBoolValue(1);
287
288 //joining channel might help while development, maybe removed later
289 //join(IRC_TEST_CHANNEL);
290 }
291 else if (command == IRC_RPL_MOTD) {
292 }
293 else if (command == IRC_RPL_MOTDSTART) {
294 }
295 else if (command == IRC_RPL_ENDOFMOTD) {
296 }
297 else if (command == IRC_ERR_NOSUCHNICK) {
298 // server return code if we send to invalid nickname
299 if (_pIRCReturnCode) _pIRCReturnCode->setStringValue(IRC_ERR_NOSUCHNICK);
300 }
301 else if (command == "ERROR") {
302 if (_pIRCReturnCode) _pIRCReturnCode->setStringValue(params);
303 disconnect();
304 }
305 // unexpected IRC message
306 else {
307 //SG_LOG(SG_NETWORK, SG_MANDATORY_INFO, "Unhandled IRC message "); //DEBUG
308 //cout << "[prefix]" << prefix << "[cmd]" << command << "[params]" << params << "[end]" << endl;
309
310 // TODO: anything sensitive here that we should handle?
311 // e.g. IRC user has disconnected and username == {current-cpdlc-authority}
312 return false;
313 }
314 return true;
315}
316
318{
319 struct IRCMessage entry {
320 "", ""
321 };
322 if (!_incoming_private_messages.empty()) {
323 entry = _incoming_private_messages.front();
324 _incoming_private_messages.pop_front();
325 }
326 return entry;
327}
bool sendPrivmsg(const std::string &recipient, const std::string &textline)
Definition mpirc.cxx:98
bool login()
Definition mpirc.cxx:85
bool part(const std::string &channel)
Definition mpirc.cxx:133
bool join(const std::string &channel)
Definition mpirc.cxx:120
IRCConnection(const std::string &nickname, const std::string &servername, const std::string &port=IRC_DEFAULT_PORT)
Definition mpirc.cxx:39
void update()
Definition mpirc.cxx:152
void quit()
Definition mpirc.cxx:91
void setupProperties(std::string path)
Definition mpirc.cxx:49
IRCMessage getMessage()
Definition mpirc.cxx:317
SGCommandMgr::command_t command
const std::string IRC_RPL_YOURID
Definition mpirc.cxx:33
const std::string IRC_RPL_WELCOME
Definition mpirc.cxx:32
const std::string IRC_MSG_TERMINATOR
Definition mpirc.cxx:30
const std::string IRC_RPL_MOTDSTART
Definition mpirc.cxx:35
const std::string IRC_RPL_ENDOFMOTD
Definition mpirc.cxx:36
const std::string IRC_RPL_MOTD
Definition mpirc.cxx:34
const std::string IRC_ERR_NOSUCHNICK
Definition mpirc.cxx:37
const std::string IRC_TEST_CHANNEL
Definition mpirc.cxx:29
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27