Skip to content

Commit

Permalink
Merge pull request #61 from kh31d4r/master
Browse files Browse the repository at this point in the history
Add discord  support
  • Loading branch information
mosbth authored Oct 1, 2024
2 parents 5a08377 + 20a3fe0 commit 95b3dbe
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 322 deletions.
1 change: 1 addition & 0 deletions .requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ feedparser
beautifulsoup4
chardet
requests
discord

# For development
pylint >= 1.7.1
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
Marvin, an IRC bot
==================

[![Join the chat at https://gitter.im/mosbth/irc2phpbb](https://badges.gitter.im/mosbth/irc2phpbb.svg)](https://gitter.im/mosbth/irc2phpbb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Join the chat at https://discord.gg/6qQATZjv](https://dcbadge.limes.pink/api/server/https://discord.gg/6qQATZjv?theme=default-inverted&compact=true)](https://discord.gg/6qQATZjv)
[![Build Status](https://github.com/mosbth/irc2phpbb/actions/workflows/main.yml/badge.svg)](https://github.com/mosbth/irc2phpbb/actions)
=======

Get a quick start by checking out the main script `main.py` and read on how to contribute.

Expand Down
42 changes: 42 additions & 0 deletions bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Module for the common base class for all Bots
"""

import re

class Bot():
"""Base class for things common between different protocols"""
def __init__(self):
self.CONFIG = {}
self.ACTIONS = []
self.GENERAL_ACTIONS = []

def getConfig(self):
"""Return the current configuration"""
return self.CONFIG

def setConfig(self, config):
"""Set the current configuration"""
self.CONFIG = config

def registerActions(self, actions):
"""Register actions to use"""
print("Adding actions:")
for action in actions:
print(" - " + action.__name__)
self.ACTIONS.extend(actions)

def registerGeneralActions(self, actions):
"""Register general actions to use"""
print("Adding general actions:")
for action in actions:
print(" - " + action.__name__)
self.GENERAL_ACTIONS.extend(actions)

@staticmethod
def tokenize(message):
"""Split a message into normalized tokens"""
return re.sub("[,.?:]", " ", message).strip().lower().split()
49 changes: 49 additions & 0 deletions discord_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Module for the Discord bot.
Connecting, sending and receiving messages and doing custom actions.
"""

import discord

from bot import Bot

class DiscordBot(discord.Client, Bot):
"""Bot implementing the discord protocol"""
def __init__(self):
Bot.__init__(self)
self.CONFIG = {
"token": ""
}
intents = discord.Intents.default()
intents.message_content = True
discord.Client.__init__(self, intents=intents)

def begin(self):
"""Start the bot"""
self.run(self.CONFIG.get("token"))

async def checkMarvinActions(self, message):
"""Check if Marvin should perform any actions"""
words = self.tokenize(message.content)
if self.user.name.lower() in words:
for action in self.ACTIONS:
response = action(words)
if response:
await message.channel.send(response)
else:
for action in self.GENERAL_ACTIONS:
response = action(words)
if response:
await message.channel.send(response)

async def on_message(self, message):
"""Hook run on every message"""
print(f"#{message.channel.name} <{message.author}> {message.content}")
if message.author.name == self.user.name:
# don't react to own messages
return
await self.checkMarvinActions(message)
240 changes: 240 additions & 0 deletions irc_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Module for the IRC bot.
Connecting, sending and receiving messages and doing custom actions.
Keeping a log and reading incoming material.
"""
from collections import deque
from datetime import datetime
import json
import os
import re
import shutil
import socket

import chardet

from bot import Bot

class IrcBot(Bot):
"""Bot implementing the IRC protocol"""
def __init__(self):
super().__init__()
self.CONFIG = {
"server": None,
"port": 6667,
"channel": None,
"nick": "marvin",
"realname": "Marvin The All Mighty dbwebb-bot",
"ident": None,
"irclogfile": "irclog.txt",
"irclogmax": 20,
"dirIncoming": "incoming",
"dirDone": "done",
"lastfm": None,
}

# Socket for IRC server
self.SOCKET = None

# Keep a log of the latest messages
self.IRCLOG = None


def connectToServer(self):
"""Connect to the IRC Server"""

# Create the socket & Connect to the server
server = self.CONFIG["server"]
port = self.CONFIG["port"]

if server and port:
self.SOCKET = socket.socket()
print("Connecting: {SERVER}:{PORT}".format(SERVER=server, PORT=port))
self.SOCKET.connect((server, port))
else:
print("Failed to connect, missing server or port in configuration.")
return

# Send the nick to server
nick = self.CONFIG["nick"]
if nick:
msg = 'NICK {NICK}\r\n'.format(NICK=nick)
self.sendMsg(msg)
else:
print("Ignore sending nick, missing nick in configuration.")

# Present yourself
realname = self.CONFIG["realname"]
self.sendMsg('USER {NICK} 0 * :{REALNAME}\r\n'.format(NICK=nick, REALNAME=realname))

# This is my nick, i promise!
ident = self.CONFIG["ident"]
if ident:
self.sendMsg('PRIVMSG nick IDENTIFY {IDENT}\r\n'.format(IDENT=ident))
else:
print("Ignore identifying with password, ident is not set.")

# Join a channel
channel = self.CONFIG["channel"]
if channel:
self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=channel))
else:
print("Ignore joining channel, missing channel name in configuration.")

def sendPrivMsg(self, message, channel):
"""Send and log a PRIV message"""
if channel == self.CONFIG["channel"]:
self.ircLogAppend(user=self.CONFIG["nick"].ljust(8), message=message)

msg = "PRIVMSG {CHANNEL} :{MSG}\r\n".format(CHANNEL=channel, MSG=message)
self.sendMsg(msg)

def sendMsg(self, msg):
"""Send and occasionally print the message sent"""
print("SEND: " + msg.rstrip('\r\n'))
self.SOCKET.send(msg.encode())

def decode_irc(self, raw, preferred_encs=None):
"""
Do character detection.
You can send preferred encodings as a list through preferred_encs.
http://stackoverflow.com/questions/938870/python-irc-bot-and-encoding-issue
"""
if preferred_encs is None:
preferred_encs = ["UTF-8", "CP1252", "ISO-8859-1"]

changed = False
enc = None
for enc in preferred_encs:
try:
res = raw.decode(enc)
changed = True
break
except Exception:
pass

if not changed:
try:
enc = chardet.detect(raw)['encoding']
res = raw.decode(enc)
except Exception:
res = raw.decode(enc, 'ignore')

return res

def receive(self):
"""Read incoming message and guess encoding"""
try:
buf = self.SOCKET.recv(2048)
lines = self.decode_irc(buf)
lines = lines.split("\n")
buf = lines.pop()
except Exception as err:
print("Error reading incoming message. " + err)

return lines

def ircLogAppend(self, line=None, user=None, message=None):
"""Read incoming message and guess encoding"""
if not user:
user = re.search(r"(?<=:)\w+", line[0]).group(0)

if not message:
message = ' '.join(line[3:]).lstrip(':')

self.IRCLOG.append({
'time': datetime.now().strftime("%H:%M").rjust(5),
'user': user,
'msg': message
})

def ircLogWriteToFile(self):
"""Write IRClog to file"""
with open(self.CONFIG["irclogfile"], 'w', encoding="UTF-8") as f:
json.dump(list(self.IRCLOG), f, indent=2)

def readincoming(self):
"""
Read all files in the directory incoming, send them as a message if
they exists and then move the file to directory done.
"""
if not os.path.isdir(self.CONFIG["dirIncoming"]):
return

listing = os.listdir(self.CONFIG["dirIncoming"])

for infile in listing:
filename = os.path.join(self.CONFIG["dirIncoming"], infile)

with open(filename, "r", encoding="UTF-8") as f:
for msg in f:
self.sendPrivMsg(msg, self.CONFIG["channel"])

try:
shutil.move(filename, self.CONFIG["dirDone"])
except Exception:
os.remove(filename)

def mainLoop(self):
"""For ever, listen and answer to incoming chats"""
self.IRCLOG = deque([], self.CONFIG["irclogmax"])

while 1:
# Write irclog
self.ircLogWriteToFile()

# Check in any in the incoming directory
self.readincoming()

for line in self.receive():
print(line)
words = line.strip().split()

if not words:
continue

self.checkIrcActions(words)
self.checkMarvinActions(words)

def begin(self):
"""Start the bot"""
self.connectToServer()
self.mainLoop()

def checkIrcActions(self, words):
"""
Check if Marvin should take action on any messages defined in the
IRC protocol.
"""
if words[0] == "PING":
self.sendMsg("PONG {ARG}\r\n".format(ARG=words[1]))

if words[1] == 'INVITE':
self.sendMsg('JOIN {CHANNEL}\r\n'.format(CHANNEL=words[3]))

def checkMarvinActions(self, words):
"""Check if Marvin should perform any actions"""
if words[1] == 'PRIVMSG' and words[2] == self.CONFIG["channel"]:
self.ircLogAppend(words)

if words[1] == 'PRIVMSG':
raw = ' '.join(words[3:])
row = self.tokenize(raw)

if self.CONFIG["nick"] in row:
for action in self.ACTIONS:
msg = action(row)
if msg:
self.sendPrivMsg(msg, words[2])
break
else:
for action in self.GENERAL_ACTIONS:
msg = action(row)
if msg:
self.sendPrivMsg(msg, words[2])
break
Loading

0 comments on commit 95b3dbe

Please sign in to comment.