ircd/script: Extend meetbot functionality.

This commit is contained in:
Luther Blissett
2022-09-24 20:20:51 +02:00
parent 6fc5b43d54
commit b2da1c8964
6 changed files with 203 additions and 246 deletions

View File

@@ -1,19 +1,18 @@
# IRC Private Channel Bot
IRC Bots
========
*meeting_bot_secret.py* is an upgraded version of the *meeting_bot.py* (for IRC). This version allows for multiple channels, including private ones.
## `meetbot`
**Note:**
`meetbot.py` is a bot used to keep topics for IRC channels, that can
be discussed on meetings. Multiple channels can be configured and this
is done through `meetbot_cfg.py`.
* `{user}` needs to be exchanged with your *username*.
* Every channel runs a bot on a different thread - be careful how many bots you deploy!
* Never add secrets to the repo config file - always copy to a local dir!
**Notes:**
* Never add secrets to the public repo config!
### Setup
**Setup**
* Donwload *meeting_bot_secret.py* and *meeting_bot_secret_config.json*
* Copy meeting_bot_secret_config.json to `/home/{user}/.config/darkfi`
* Open the config and set up all the channels and the values of name and secret (if not private, secret must be {null})
* Change `{user}` to your *username* in *meeting_bot_secret.py* in the path of `load_config()` function
* Navigate terminal to the folder where is *meeting_bot_secret.py*
* Run the bot: `$ python meeting_bot_secret.py`
* Download `meetbot.py` and `meetbot_cfg.py`
* Edit `meetbot_cfg.py` for your needs.
* Navigate terminal to the folder where `meetbot.py` is.
* Run the bot: `$ python meetbot.py`

167
bin/ircd/script/meetbot.py Executable file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
import asyncio
import logging
from base58 import b58decode
from nacl.public import PrivateKey, Box
from meetbot_cfg import config
# Initialized channels from the configuration
CHANS = {}
async def channel_listen(host, port, nick, chan):
global CHANS
logging.info(f"Connecting to {host}:{port}")
reader, writer = await asyncio.open_connection(host, port)
logging.info(f"{host}:{port} Send CAP msg")
cap_msg = "CAP REQ : no-history\r\n"
writer.write(cap_msg.encode("utf-8"))
logging.info(f"{host}:{port} Send NICK msg")
nick_msg = f"NICK {nick}\r\n"
writer.write(nick_msg.encode("utf-8"))
logging.info(f"{host}:{port} Send CAP END msg")
cap_end_msg = "CAP END\r\n"
writer.write(cap_end_msg.encode("utf-8"))
logging.info(f"{host}:{port} Send JOIN msg for {chan}")
join_msg = f"JOIN {chan}\r\n"
writer.write(join_msg.encode("utf-8"))
logging.info(f"{host}:{port} Listening to channel: {chan}")
while True:
msg = await reader.read(1024)
msg = msg.decode("utf8")
if not msg:
continue
command = msg.split(" ")[1]
if command == "PRIVMSG":
msg_title = msg.split(" ")[3][1:].rstrip()
if not msg_title:
logging.info("Got empty PRIVMSG, ignoring")
continue
if msg_title == "!start":
topics = CHANS[chan]["topics"]
reply = f"PRIVMSG {chan} :Meeting started\r\n"
writer.write(reply.encode("utf-8"))
await writer.drain()
reply = f"PRIVMSG {chan} :Topics:\r\n"
writer.write(reply.encode("utf-8"))
await writer.drain()
for i, topic in enumerate(topics):
reply = f"PRIVMSG {chan} :1. {topic}\r\n"
writer.write(reply.encode("utf-8"))
await writer.drain()
if len(topics) > 0:
cur_topic = topics.pop(0)
reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n"
else:
reply = f"PRIVMSG {chan} :No further topics\r\n"
CHANS[chan]["topics"] = topics
writer.write(reply.encode("utf-8"))
await writer.drain()
continue
if msg_title == "!end":
reply = f"PRIVMSG {chan} :Meeting ended\r\n"
writer.write(reply.encode("utf-8"))
await writer.drain()
continue
if msg_title == "!topic":
topic = msg.split(" ", 4)
if len(topic) != 5:
continue
topic = topic[4].rstrip()
if topic == "":
continue
topics = CHANS[chan]["topics"]
topics.append(topic)
CHANS[chan]["topics"] = topics
reply = f"PRIVMSG {chan} :Added topic: {topic}\r\n"
writer.write(reply.encode("utf-8"))
await writer.drain()
continue
if msg_title == "!list":
topics = CHANS[chan]["topics"]
if len(topics) == 0:
reply = f"PRIVMSG {chan} :No set topics\r\n"
else:
reply = f"PRIVMSG {chan} :Topics:\r\n"
writer.write(reply.encode("utf-8"))
await writer.drain()
for i, topic in enumerate(topics):
reply = f"PRIVMSG {chan} :1. {topic}\r\n"
writer.write(reply.encode("utf-8"))
await writer.drain()
continue
if msg_title == "!next":
topics = CHANS[chan]["topics"]
if len(topics) == 0:
reply = f"PRIVMSG {chan} :No further topics\r\n"
else:
cur_topic = topics.pop(0)
CHANS[chan]["topics"] = topics
reply = f"PRIVMSG {chan} :Current topic: {cur_topic}\r\n"
writer.write(reply.encode("utf-8"))
await writer.drain()
continue
return
async def main():
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO, datefmt="%H:%M:%S")
for i in config["channels"]:
name = i["name"]
logging.info(f"Found config for channel {name}")
# TODO: This will be useful when ircd has a CAP that tells it to
# give **all** messages to the connected client, no matter if ircd
# itself has a configured secret or not.
# This way the ircd itself doesn't have to keep channel secrets, but
# they can rather only be held by this bot. In turn this means the bot
# can be deployed with any ircd.
if i["secret"]:
logging.info(f"Instantiating NaCl box for {name}")
sk = b58decode(i["secret"].encode("utf-8"))
sk = PrivateKey(sk)
pk = sk.public_key
box = Box(sk, pk)
else:
box = None
CHANS[name] = {}
CHANS[name]["box"] = box
CHANS[name]["topics"] = []
coroutines = []
for i in CHANS.keys():
task = asyncio.create_task(
channel_listen(config["host"], config["port"], config["nick"], i))
coroutines.append(task)
await asyncio.gather(*coroutines)
asyncio.run(main())

View File

@@ -0,0 +1,23 @@
config = {
# IRC server host
"host": "127.0.0.1",
# IRC server port
"port": 6667,
# IRC nickname
"nick": "meetbot",
"channels": [
{
"name": "#foo",
"secret": None,
},
{
"name": "#secret_channel",
# TODO: This is useless right now, but it would be nice
# to add a CAP in ircd to give all incoming PRIVMSG to be
# able to check them.
"secret": "HNEKcUmwsspdaL9b8sFn45b8Rf3bzv1LdYS1JVNvkPGL",
},
],
}

View File

@@ -1,91 +0,0 @@
import asyncio
async def start():
host = "127.0.0.1"
port = 6667
channel = "#dev"
nickname = "meeting_bot"
print(f"Start a connection {host}:{port}")
reader, writer = await asyncio.open_connection(host, port)
print("Send CAP msg")
cap_msg = f"CAP REQ : no-history \r\n"
writer.write(cap_msg.encode('utf8'))
print("Send NICK msg")
nick_msg = f"NICK {nickname} \r\n"
writer.write(nick_msg.encode('utf8'))
print("Send CAP END msg")
cap_end_msg = f"CAP END \r\n"
writer.write(cap_end_msg.encode('utf8'))
print(f"Send JOIN msg: {channel}")
join_msg = f"JOIN {channel} \r\n"
writer.write(join_msg.encode('utf8'))
topics = []
print("Start...")
while True:
msg = await reader.read(1024)
msg = msg.decode('utf8').strip()
if not msg:
continue
command = msg.split(" ")[1]
if command == "PRIVMSG":
msg_title = msg.split(" ")[3][1:]
if not msg_title:
continue
reply = None
if msg_title == "!start":
reply = f"PRIVMSG {channel} :meeting started \r\n"
msg_title = "!list"
if msg_title == "!end":
reply = f"PRIVMSG {channel} :meeting end \r\n"
topics = []
if msg_title == "!topic":
topic = msg.split(" ", 4)
if len(topic) != 5:
continue
topic = topic[4]
topics.append(topic)
reply = f"PRIVMSG {channel} :add topic: {topic} \r\n"
if msg_title == "!list":
rep = f"PRIVMSG {channel} :topics: \r\n"
writer.write(rep.encode('utf8'))
for i, topic in enumerate(topics, 1):
rep = f"PRIVMSG {channel} :{i}-{topic} \r\n"
writer.write(rep.encode('utf8'))
await writer.drain()
if msg_title == "!next":
if len(topics) == 0:
reply = f"PRIVMSG {channel} :no topics \r\n"
else:
tp = topics.pop(0)
reply = f"PRIVMSG {channel} :current topic: {tp} \r\n"
if reply != None:
writer.write(reply.encode('utf8'))
await writer.drain()
if command == "QUIT":
break
writer.close()
asyncio.run(start())

View File

@@ -1,121 +0,0 @@
#TODO:
# Expand home path
import logging
import threading
import time
import json
import asyncio
def load_config():
with open('/home/{user}/.config/darkfi/meeting_bot_config.json', 'r') as config:
data = json.load(config)
logging.info(f"Config loaded: {data}")
return data
def thread_run(host, port, nickname, channel):
asyncio.run(channel_listen(host, port, nickname, channel))
async def channel_listen(host, port, nickname, channel):
logging.info(f"Starting listening to channel: {channel['name']}")
logging.info(f"Start a connection {host}:{port}")
reader, writer = await asyncio.open_connection(host, port)
logging.info("Send CAP msg")
cap_msg = f"CAP REQ : no-history \r\n"
writer.write(cap_msg.encode('utf8'))
logging.info("Send NICK msg")
nick_msg = f"NICK {nickname} \r\n"
writer.write(nick_msg.encode('utf8'))
logging.info("Send CAP END msg")
cap_end_msg = f"CAP END \r\n"
writer.write(cap_end_msg.encode('utf8'))
logging.info(f"Send JOIN msg: {channel['name']}")
join_msg = f"JOIN {channel['name']} \r\n"
writer.write(join_msg.encode('utf8'))
topics = []
logging.info("Start...")
while True:
msg = await reader.read(1024)
msg = msg.decode('utf8').strip()
if not msg:
continue
command = msg.split(" ")[1]
if command == "PRIVMSG":
msg_title = msg.split(" ")[3][1:]
if not msg_title:
continue
reply = None
if msg_title == "!start":
reply = f"PRIVMSG {channel['name']} :meeting started \r\n"
msg_title = "!list"
if msg_title == "!end":
reply = f"PRIVMSG {channel['name']} :meeting end \r\n"
topics = []
if msg_title == "!topic":
topic = msg.split(" ", 4)
if len(topic) != 5:
continue
topic = topic[4]
topics.append(topic)
reply = f"PRIVMSG {channel['name']} :add topic: {topic} \r\n"
if msg_title == "!list":
rep = f"PRIVMSG {channel['name']} :topics: \r\n"
writer.write(rep.encode('utf8'))
for i, topic in enumerate(topics, 1):
rep = f"PRIVMSG {channel['name']} :{i}-{topic} \r\n"
writer.write(rep.encode('utf8'))
await writer.drain()
if msg_title == "!next":
if len(topics) == 0:
reply = f"PRIVMSG {channel['name']} :no topics \r\n"
else:
tp = topics.pop(0)
reply = f"PRIVMSG {channel['name']} :current topic: {tp} \r\n"
if reply != None:
writer.write(reply.encode('utf8'))
await writer.drain()
if command == "QUIT":
break
writer.close()
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S")
data = load_config()
threads = list()
for index in range(len(data['channels'])):
logging.info(f"Main : create and start thread {index}.")
x = threading.Thread(target=thread_run, args=(data['host'], data['port'], data['nickname'], data['channels'][index],))
threads.append(x)
x.start()
for index, thread in enumerate(threads):
logging.info(f"Main : before joining thread {index}.")
thread.join()
logging.info(f"Main : thread {index} done")

View File

@@ -1,20 +0,0 @@
{
"host":"127.0.0.1",
"port":6667,
"nickname":"meeting_bot",
"channels":
[
{
"name":"#dev",
"secret":null
},
{
"name":"#enter_channel_name",
"secret":"#enter_secret"
},
{
"name":"#enter_channel_name_2",
"secret":"#enter_secret_2"
}
]
}