From 13c62c85775b97fa30765bec57bb4a9e2f68e1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sat, 11 May 2019 14:37:22 +0300 Subject: [PATCH 1/9] Use the CC0 file --- CC0 | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ UNLICENSE | 24 ----------- 2 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 CC0 delete mode 100644 UNLICENSE diff --git a/CC0 b/CC0 new file mode 100644 index 0000000..670154e --- /dev/null +++ b/CC0 @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +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. + +For more information, please see + diff --git a/UNLICENSE b/UNLICENSE deleted file mode 100644 index 69843e4..0000000 --- a/UNLICENSE +++ /dev/null @@ -1,24 +0,0 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to [http://unlicense.org] From 635cab74408064e71699136c929833b000ab8453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sat, 11 May 2019 15:32:27 +0300 Subject: [PATCH 2/9] Don't allow sending to the closed server socket if we are reconnecting --- ircbot.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ircbot.py b/ircbot.py index 5af87fb..a5ab9dd 100644 --- a/ircbot.py +++ b/ircbot.py @@ -183,7 +183,10 @@ class ServerThread(threading.Thread): time.sleep(wait) with self.server_socket_write_lock: - self.server_socket.sendall(line + b'\r\n') + if self.server_socket is not None: + self.server_socket.sendall(line + b'\r\n') + else: + return # Don't log PINGs or PONGs if not (len(line) >= 5 and (line[:5] == b'PING ' or line[:5] == b'PONG ')): @@ -351,6 +354,7 @@ class ServerThread(threading.Thread): # Tell the server we're quiting self.send_line_raw(b'QUIT :%s exiting normally' % self.server.username.encode('utf-8')) + self.server_socket.close() break @@ -358,7 +362,9 @@ class ServerThread(threading.Thread): else: # Tell server we're reconnecting self.send_line_raw(b'QUIT :Reconnecting') - self.server_socket.close() + with self.server_socket_write_lock: + self.server_socket.close() + self.server_socket = None except (BrokenPipeError, TimeoutError) as err: # Connection broke, log it and try to reconnect From f6fe53385dda40ba530825398d7dc1a58540cc7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sun, 2 May 2021 00:40:22 +0300 Subject: [PATCH 3/9] Add built-in authentication to o3-base --- ircbot.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/ircbot.py b/ircbot.py index a5ab9dd..848e1a9 100644 --- a/ircbot.py +++ b/ircbot.py @@ -142,11 +142,12 @@ class API: self.serverthread_object.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, message)) -# ServerThread(server, control_channel, cron_control_channel, logging_channel) +# ServerThread(server, auth, control_channel, cron_control_channel, logging_channel) # Creates a new server main loop thread class ServerThread(threading.Thread): - def __init__(self, server, control_channel, cron_control_channel, logging_channel): + def __init__(self, server, auth, control_channel, cron_control_channel, logging_channel): self.server = server + self.auth = auth self.control_channel = control_channel self.cron_control_channel = cron_control_channel self.logging_channel = logging_channel @@ -330,10 +331,15 @@ class ServerThread(threading.Thread): try: # Run initialization - self.send_line_raw(b'USER %s a a :%s' % (self.server.username.encode('utf-8'), self.server.realname.encode('utf-8'))) - # Set up nick + # Use server-password based authentication if it's set up + user, password = self.auth + if user is not None: + self.send_line_raw(b'PASS %s:%s' % (user.encode(), password.encode())) + + # Set up nick and username self.api.nick(self.server.nick.encode('utf-8')) + self.send_line_raw(b'USER %s a a :%s' % (self.server.username.encode('utf-8'), self.server.realname.encode('utf-8'))) # Run the on_connect hook, to allow further setup botcmd.on_connect(irc = self.api) @@ -377,12 +383,12 @@ class ServerThread(threading.Thread): # Tell cron we're quiting cron.quit(cron_control_channel) -# spawn_serverthread(server, cron_control_channel, logging_channel) → control_channel +# spawn_serverthread(server, auth, cron_control_channel, logging_channel) → control_channel # Creates a ServerThread for given server and returns the channel for controlling it -def spawn_serverthread(server, cron_control_channel, logging_channel): +def spawn_serverthread(server, auth, cron_control_channel, logging_channel): thread_control_socket, spawner_control_socket = socket.socketpair() control_channel = channel.Channel() - ServerThread(server, control_channel, cron_control_channel, logging_channel).start() + ServerThread(server, auth, control_channel, cron_control_channel, logging_channel).start() return control_channel # spawn_loggerthread() → logging_channel, dead_notify_channel @@ -406,18 +412,24 @@ def read_config(): realname = config['server']['realname'] channels = config['server']['channels'].split() + user = None + password = None + if 'auth' in config: + user = config['auth']['user'] + password = config['auth']['password'] + server = Server(host = host, port = port, nick = nick, username = username, realname = realname, channels = channels) - return config, server + return config, server, (user, password) if __name__ == '__main__': - config, server = read_config() + config, server, auth = read_config() botcmd.initialize(config = config) cron_control_channel = cron.start() logging_channel, dead_notify_channel = spawn_loggerthread() - control_channel = spawn_serverthread(server, cron_control_channel, logging_channel) + control_channel = spawn_serverthread(server, auth, cron_control_channel, logging_channel) while True: message = dead_notify_channel.recv(blocking = False) From de139b14b87a6ac756c73d1f1a820d1183c43566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sun, 12 Dec 2021 16:37:35 +0200 Subject: [PATCH 4/9] Bye Freenode --- bot.conf.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.conf.example b/bot.conf.example index accc678..7bd40a1 100644 --- a/bot.conf.example +++ b/bot.conf.example @@ -1,5 +1,5 @@ [server] -host = irc.freenode.net +host = irc.libera.chat port = 6667 nick = o3-base username = o3-base From ed314735f9da6f8bea545d137f419c9b758a5f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sun, 12 Dec 2021 16:37:58 +0200 Subject: [PATCH 5/9] Disable interactive console if stdin is not a tty --- ircbot.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ircbot.py b/ircbot.py index 848e1a9..0fc25f9 100644 --- a/ircbot.py +++ b/ircbot.py @@ -2,6 +2,7 @@ import configparser import select import socket +import sys import threading import time from collections import namedtuple @@ -16,7 +17,8 @@ import line_handling Server = namedtuple('Server', ['host', 'port', 'nick', 'username', 'realname', 'channels']) class LoggerThread(threading.Thread): - def __init__(self, logging_channel, dead_notify_channel): + def __init__(self, interactive_console, logging_channel, dead_notify_channel): + self.interactive_console = interactive_console self.logging_channel = logging_channel self.dead_notify_channel = dead_notify_channel @@ -29,11 +31,11 @@ class LoggerThread(threading.Thread): # Lines that were sent between server and client if message_type == logmessage_types.sent: assert len(message_data) == 1 - print('>' + message_data[0]) + if self.interactive_console: print('>' + message_data[0]) elif message_type == logmessage_types.received: assert len(message_data) == 1 - print('<' + message_data[0]) + if self.interactive_console: print('<' + message_data[0]) # Messages that are from internal components elif message_type == logmessage_types.internal: @@ -391,12 +393,12 @@ def spawn_serverthread(server, auth, cron_control_channel, logging_channel): ServerThread(server, auth, control_channel, cron_control_channel, logging_channel).start() return control_channel -# spawn_loggerthread() → logging_channel, dead_notify_channel +# spawn_loggerthread(interactive_console) → logging_channel, dead_notify_channel # Spawn logger thread and returns the channel it logs and the channel it uses to notify about quiting -def spawn_loggerthread(): +def spawn_loggerthread(interactive_console): logging_channel = channel.Channel() dead_notify_channel = channel.Channel() - LoggerThread(logging_channel, dead_notify_channel).start() + LoggerThread(interactive_console, logging_channel, dead_notify_channel).start() return logging_channel, dead_notify_channel # read_config() → config, server @@ -423,15 +425,17 @@ def read_config(): return config, server, (user, password) if __name__ == '__main__': + interactive_console = sys.stdin.isatty() + config, server, auth = read_config() botcmd.initialize(config = config) cron_control_channel = cron.start() - logging_channel, dead_notify_channel = spawn_loggerthread() + logging_channel, dead_notify_channel = spawn_loggerthread(interactive_console) control_channel = spawn_serverthread(server, auth, cron_control_channel, logging_channel) - while True: + while interactive_console: message = dead_notify_channel.recv(blocking = False) if message is not None: if message[0] == controlmessage_types.quit: From 424e6c7b67e34b278cec113fcb285d490775cf8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sun, 12 Dec 2021 16:45:52 +0200 Subject: [PATCH 6/9] Log connection attempts to server --- constants.py | 2 +- ircbot.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/constants.py b/constants.py index 402a494..7b624cc 100644 --- a/constants.py +++ b/constants.py @@ -4,7 +4,7 @@ class logmessage_types(enum.Enum): sent, received, internal, status = range(4) class internal_submessage_types(enum.Enum): - quit, error = range(2) + quit, error, server = range(3) class controlmessage_types(enum.Enum): quit, reconnect, send_line, ping, ping_timeout = range(5) diff --git a/ircbot.py b/ircbot.py index 0fc25f9..e964149 100644 --- a/ircbot.py +++ b/ircbot.py @@ -50,6 +50,11 @@ class LoggerThread(threading.Thread): assert len(message_data) == 2 print('--- Error', message_data[1]) + elif message_data[0] == internal_submessage_types.server: + assert len(message_data) == 2 + assert len(message_data[1]) == 2 + print(f'--- Connecting to server {message_data[1][0]}:{message_data[1][1]}') + else: print('--- ???', message_data) @@ -295,6 +300,7 @@ class ServerThread(threading.Thread): while True: # Connect to given server address = (self.server.host, self.server.port) + self.logging_channel.send((logmessage_types.internal, internal_submessage_types.server, address)) try: self.server_socket = socket.create_connection(address) except (ConnectionRefusedError, socket.gaierror): From deae8043e11ed2f333437586f8757fffd61b736a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sun, 12 Dec 2021 17:01:54 +0200 Subject: [PATCH 7/9] Log to stderr if run non-interactively --- ircbot.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ircbot.py b/ircbot.py index e964149..a28b8d6 100644 --- a/ircbot.py +++ b/ircbot.py @@ -41,31 +41,31 @@ class LoggerThread(threading.Thread): elif message_type == logmessage_types.internal: if message_data[0] == internal_submessage_types.quit: assert len(message_data) == 1 - print('--- Quit') + print('--- Quit', file=sys.stderr) self.dead_notify_channel.send((controlmessage_types.quit,)) break elif message_data[0] == internal_submessage_types.error: assert len(message_data) == 2 - print('--- Error', message_data[1]) + print('--- Error', message_data[1], file=sys.stderr) elif message_data[0] == internal_submessage_types.server: assert len(message_data) == 2 assert len(message_data[1]) == 2 - print(f'--- Connecting to server {message_data[1][0]}:{message_data[1][1]}') + print(f'--- Connecting to server {message_data[1][0]}:{message_data[1][1]}', file=sys.stderr) else: - print('--- ???', message_data) + print('--- ???', message_data, file=sys.stderr) # Messages about status from the bot code elif message_type == logmessage_types.status: assert len(message_data) == 2 - print('*', end='') - print(*message_data[0], **message_data[1]) + print('*', end='', file=sys.stderr) + print(*message_data[0], **message_data[1], file=sys.stderr) else: - print('???', message_type, message_data) + print('???', message_type, message_data, file=sys.stderr) # API(serverthread_object) # Create a new API object corresponding to given ServerThread object From 2c6d35347cf6c82c26c176a5519a6f1c88c8f647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sun, 12 Dec 2021 17:05:31 +0200 Subject: [PATCH 8/9] Revert "Log to stderr if run non-interactively" This reverts commit deae8043e11ed2f333437586f8757fffd61b736a. --- ircbot.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ircbot.py b/ircbot.py index a28b8d6..e964149 100644 --- a/ircbot.py +++ b/ircbot.py @@ -41,31 +41,31 @@ class LoggerThread(threading.Thread): elif message_type == logmessage_types.internal: if message_data[0] == internal_submessage_types.quit: assert len(message_data) == 1 - print('--- Quit', file=sys.stderr) + print('--- Quit') self.dead_notify_channel.send((controlmessage_types.quit,)) break elif message_data[0] == internal_submessage_types.error: assert len(message_data) == 2 - print('--- Error', message_data[1], file=sys.stderr) + print('--- Error', message_data[1]) elif message_data[0] == internal_submessage_types.server: assert len(message_data) == 2 assert len(message_data[1]) == 2 - print(f'--- Connecting to server {message_data[1][0]}:{message_data[1][1]}', file=sys.stderr) + print(f'--- Connecting to server {message_data[1][0]}:{message_data[1][1]}') else: - print('--- ???', message_data, file=sys.stderr) + print('--- ???', message_data) # Messages about status from the bot code elif message_type == logmessage_types.status: assert len(message_data) == 2 - print('*', end='', file=sys.stderr) - print(*message_data[0], **message_data[1], file=sys.stderr) + print('*', end='') + print(*message_data[0], **message_data[1]) else: - print('???', message_type, message_data, file=sys.stderr) + print('???', message_type, message_data) # API(serverthread_object) # Create a new API object corresponding to given ServerThread object From 9f5f74c45635090d7a195ef515f3c47b44b06a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sun, 12 Dec 2021 17:07:00 +0200 Subject: [PATCH 9/9] Flush log output after each line in case output is buffered --- ircbot.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ircbot.py b/ircbot.py index e964149..7ec5f50 100644 --- a/ircbot.py +++ b/ircbot.py @@ -42,6 +42,7 @@ class LoggerThread(threading.Thread): if message_data[0] == internal_submessage_types.quit: assert len(message_data) == 1 print('--- Quit') + sys.stdout.flush() self.dead_notify_channel.send((controlmessage_types.quit,)) break @@ -49,23 +50,28 @@ class LoggerThread(threading.Thread): elif message_data[0] == internal_submessage_types.error: assert len(message_data) == 2 print('--- Error', message_data[1]) + sys.stdout.flush() elif message_data[0] == internal_submessage_types.server: assert len(message_data) == 2 assert len(message_data[1]) == 2 print(f'--- Connecting to server {message_data[1][0]}:{message_data[1][1]}') + sys.stdout.flush() else: print('--- ???', message_data) + sys.stdout.flush() # Messages about status from the bot code elif message_type == logmessage_types.status: assert len(message_data) == 2 print('*', end='') print(*message_data[0], **message_data[1]) + sys.stdout.flush() else: print('???', message_type, message_data) + sys.stdout.flush() # API(serverthread_object) # Create a new API object corresponding to given ServerThread object