From 9bc0ac40f784e773b69acc34a16e37e8da342de4 Mon Sep 17 00:00:00 2001 From: Les De Ridder Date: Thu, 2 May 2019 02:10:44 +0200 Subject: [PATCH] Initial commit --- .gitignore | 15 ++ COPYING | 121 ++++++++++++++ dub.sdl | 6 + dub.selections.json | 6 + source/app.d | 390 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 538 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 dub.sdl create mode 100644 dub.selections.json create mode 100644 source/app.d diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce49585 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.dub +docs.json +__dummy.html +docs/ +ircbot.so +ircbot.dylib +ircbot.dll +ircbot.a +ircbot.lib +ircbot +ircbot-test-* +*.exe +*.o +*.obj +*.lst diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/dub.sdl b/dub.sdl new file mode 100644 index 0000000..972af0b --- /dev/null +++ b/dub.sdl @@ -0,0 +1,6 @@ +name "ircbot" +description "meme irc bot" +authors "lesderid" +copyright "No rights reserved." +license "CC0" +dependency "openssl" version="~>2.0.0" diff --git a/dub.selections.json b/dub.selections.json new file mode 100644 index 0000000..7d061d1 --- /dev/null +++ b/dub.selections.json @@ -0,0 +1,6 @@ +{ + "fileVersion": 1, + "versions": { + "openssl": "2.0.0+1.1.0h" + } +} diff --git a/source/app.d b/source/app.d new file mode 100644 index 0000000..6326dd1 --- /dev/null +++ b/source/app.d @@ -0,0 +1,390 @@ +/+ +Written in 2019 by Les De Ridder + +To the extent possible under law, the author has dedicated all copyright +and related and neighboring rights to this software to the public domain +worldwide. This software is distributed without any warranty. ++/ + +import std.stdio; +import std.regex; +import std.parallelism; +import std.algorithm; +import std.range; +import std.string; +import std.typecons; +import core.thread; +import std.process; +import std.datetime.stopwatch; +import std.conv : to; +import core.sys.posix.signal : SIGKILL; +import core.sys.posix.poll; + +import std.socket; +import deimos.openssl.ssl; + +extern(C): int SSL_has_pending(const(SSL)* s); + +enum Server = "irc.soupwhale.com"; +enum Port = 6697; +enum TLS = true; +enum Username = "ircbot"; +enum RealName = "ircbot"; +enum Nickname = "ircbot"; +enum Channels = ["#soupwhale"]; + +enum Shell = "bash"; +enum ProcessTimeout = 60.seconds; + +enum SleepDuration = 100.msecs; + +void main() +{ + static assert(TLS, "Non-TLS client not implemented"); + + auto ircClient = new IRCClient!TLSClient(); + + ircClient.connect(Server, Port); + ircClient.write(Message(null, "USER", [Username, "0", "*", RealName], true)); + ircClient.write(Message(null, "NICK", [Nickname])); + + auto processRunning = false; + ProcessPipes pipes; + StopWatch stopWatch; + + while (true) + { + if (processRunning) + { + if (pipes.stdout.eof) + { + processRunning = false; + } + else + { + auto stdoutFD = pollfd(pipes.stdout.fileno, POLLIN); + while (poll(&stdoutFD, 1, 0) > 0 && !pipes.stdout.eof) + { + auto line = pipes.stdout.readln; + if (line.length > 0) + { + foreach (channel; Channels) + { + ircClient.write(Message(null, "PRIVMSG", [channel, line], true)); + } + } + } + pollfd stderrFD = { pipes.stderr.fileno, POLLIN }; + while (poll(&stderrFD, 1, 0) > 0 && !pipes.stderr.eof) + { + auto line = pipes.stderr.readln; + if (line.length > 0) + { + foreach (channel; Channels) + { + ircClient.write(Message(null, "PRIVMSG", [channel, "stderr: " ~ line], true)); + } + } + } + } + + //if (stopWatch.peek() > ProcessTimeout) + //{ + // kill(pipes.pid, SIGKILL); + // processRunning = false; + + // foreach (channel; Channels) + // { + // ircClient.write(Message(null, "PRIVMSG", [channel, "Process timed out"])); + // } + //} + } + + auto maybeMessages = ircClient.readMessages(); + + if (maybeMessages.isNull) + { + Thread.sleep(SleepDuration); + continue; + } + + foreach (message; maybeMessages.get) + { + writeln("message={", message.toString, "}"); + switch (message.command) + { + case "PING": + ircClient.write(Message(null, "PONG", message.parameters)); + break; + case "376": + writeln("376 376 376"); + ircClient.join(Channels); + break; + case "PRIVMSG": + auto source = message.parameters[0]; + auto line = message.parameters[1]; + + if (Channels.canFind(source)) + { + if (line.startsWith("$") && !processRunning) + { + auto command = line.drop(1); + + pipes = pipeProcess([Shell, "-c", command], Redirect.all, null); + //stopWatch = StopWatch(AutoStart.yes); + processRunning = true; + } + else if (processRunning) + { + if (!pipes.stdin.isOpen) + { + processRunning = false; + continue; + } + + if (line == "KILL") + { + kill(pipes.pid, SIGKILL); + } + else if (line == "^D") + { + pipes.stdin.close(); + } + else + { + pipes.stdin.writeln(line); + pipes.stdin.flush(); + } + } + } + break; + default: + break; + } + } + } +} + +//TODO: Write a compatible non-TLS Client class +class IRCClient(Client) +{ + private Client _client; + + this() + { + _client = new Client(); + } + + void connect(string address, ushort port) + { + _client.connect(address, port); + } + + void disconnect() + { + _client.disconnect(); + } + + Nullable!(Message[]) readMessages(bool blocking = false) + { + string readString = null; + + do { + readString = _client.readTo!"\r\n"; + } while (blocking && readString is null); + + return readString is null ? Nullable!(Message[])() : (readString.splitLines.map!(l => Message.fromString(l)).array).nullable; + } + + void write(Message message) + { + write(message.toString()); + } + + void write(string message) + { + _client.write(message ~ "\r\n"); + } + + void join(string[] channels) + { + foreach (channel; channels) + { + write(Message(null, "JOIN", [channel])); + } + } +} + +class TLSClient +{ + private bool _connected; + @property bool connected() { return _connected; } + + private SSL_CTX* _sslContext; + private SSL* _ssl; + + private Socket _socket; + + const ReadBufferSize = 65536; + private char[ReadBufferSize] _readBuffer; + + this() + { + _sslContext = SSL_CTX_new(TLSv1_2_client_method()); + + assert(_sslContext !is null); + } + + ~this() + { + if(connected) + { + disconnect(); + } + + SSL_CTX_free(_sslContext); + } + + void connect(string address, ushort port) + { + if(connected) + { + disconnect(); + } + + _socket = new TcpSocket(); + _socket.connect(new InternetAddress(address, port)); + + _ssl = SSL_new(_sslContext); + + SSL_set_fd(_ssl, _socket.handle()); + assert(SSL_connect(_ssl) != -1); + + _socket.blocking = false; + + _connected = true; + } + + void disconnect() + { + SSL_free(_ssl); + + _socket.close(); + + _connected = false; + } + + string readTo(string delimiter, bool includeDelimiter = false)() + { + auto totalBytesRead = 0; + auto bytesRead = 0; + + while(totalBytesRead == 0 || (totalBytesRead >= delimiter.length && _readBuffer[totalBytesRead - delimiter.length .. totalBytesRead] != delimiter)) + { + bytesRead = SSL_read(_ssl, cast(void*) _readBuffer + totalBytesRead, ReadBufferSize - totalBytesRead); + + if (bytesRead <= 0) return null; + + totalBytesRead += bytesRead; + } + + return _readBuffer[0 .. (includeDelimiter ? totalBytesRead : totalBytesRead - delimiter.length)].idup; + } + + void write(string message) + { + auto bytesWritten = SSL_write(_ssl, cast(const char*) message, cast(int) message.length); + + if (bytesWritten <= 0) + { + stderr.writeln("uh-oh"); + + disconnect(); + } + } +} + +struct Message +{ + string prefix; + string command; + string[] parameters; + bool prefixedParameter; + + //NOTE: The RFCs don't state what this is exactly, but common implementations use the byte count of the message parameters + ulong bytes; + + static Message fromString(string line) + { + string prefix = null; + if(line.startsWith(':')) + { + line = line[1 .. $]; + prefix = line[0 .. line.indexOf(' ')]; + line = line[prefix.length + 1 .. $]; + } + + //stop early when no space character can be found (message without parameters) + if(!line.canFind(' ')) + { + return Message(prefix, line, [], false); + } + + auto command = line[0 .. line.indexOf(' ')]; + line = line[command.length + 1 .. $]; + auto bytes = line.length; + string[] params = []; + bool prefixedParam; + while(true) + { + if(line.startsWith(':')) + { + params ~= line[1 .. $]; + prefixedParam = true; + break; + } + else if(line.canFind(' ')) + { + auto param = line[0 .. line.indexOf(' ')]; + line = line[param.length + 1 .. $]; + params ~= param; + } + else + { + params ~= line; + break; + } + } + + return Message(prefix, command, params, prefixedParam, bytes); + } + + string toString() + { + auto message = ""; + if(prefix != null) + { + message = ":" ~ prefix ~ " "; + } + + if(parameters.length == 0) + { + return message ~ command; + } + + message ~= command ~ " "; + if(parameters.length > 1) + { + message ~= parameters[0 .. $-1].join(' ') ~ " "; + } + + if(parameters[$-1].canFind(' ') || prefixedParameter) + { + message ~= ":"; + } + message ~= parameters[$-1]; + + return message; + } +} +