Skip to content
Merged
38 changes: 17 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ This is a simple bot which fetches RSS feeds and posts them to an IRC channel.

# Requirements

- python2
- pip
- virtualenv (`pip2 install virtualenv`)
- python3
- pip3
- virtualenv (`pip3 install virtualenv`)

# Features

- Saves feeds and news items in a sqlite database
- Fetches every feed in a separate thread
- Posts new news items to an IRC channel
- Sends information via private messages
- SSL connection support
- Full utf-8 support
- Nick login support
- Post delayed during conversation
- Delayed post during conversation
- Keywords in news title filtering support
- Customizable post colors
- Automatic join to channel on kick
Expand All @@ -26,37 +27,29 @@ This is a simple bot which fetches RSS feeds and posts them to an IRC channel.

```
Help:
Send all commands as a private message to Feed
Send all commands as a private message
- !help Prints this help
- !list Prints all feeds
- !stats Prints some statistics
- !last Prints the last 25 entries
- !lastfeed <feedid> Prints the last 25 entries from a specific feed
- !last Prints the last 10 entries
- !lastfeed <feedid> Prints the last 10 entries from a specific feed
```

# Setup

Clone this repository and change into the directory. Create a new virtualenv and activate it:

```
virtualenv venv
. venv/bin/activate
virtualenv -p python3 venv
source venv/bin/activate
```

Proceed with the installation of all dependencies:

```
pip install -r requirements.txt
pip3 install -r requirements.txt
```

If you get an error that `sqlite3worker` couldn't be installed, use

```
pip install git+https://github.com/palantir/sqlite3worker#egg=sqlite3worker
```

and retry the installation.

Copy the sample files:

```
Expand All @@ -69,13 +62,16 @@ Edit `config.py` to fit your needs and IRC settings. All feeds from `feeds.sql`
You might want to update all feeds before connecting to the IRC server to prevent spamming the channel (and optionally a ban from your IRC server). Either set `update_before_connecting = True` in the `config.py` or run the update script before starting the bot:

```
python2 feedupdater.py
python3 feedupdater.py
```

To start the bot, run:

```
python2 main.py
python3 main.py
```
or in background:
```
python3 main.py 2>&1 > newsbot.log &
```

If you want to run this as a systemd service, you can use the `rss2irc.service` file after adjusting the paths in there.
Expand Down
76 changes: 41 additions & 35 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

import ssl
import threading
import irc.bot
import irc.client
import irc.connection
import tinyurl
import time
import re
import sys
import feedparser
import datetime
import dateutil.parser
import requests
from colour import Colours
from db import FeedDB
from config import Config
Expand All @@ -37,25 +34,25 @@ def __init__(self, config, db, on_connect_cb):

if self.__config.SSL:
ssl_factory = irc.connection.Factory(wrapper=ssl.wrap_socket)
print datetime.datetime.now(), u"Starting SSL connection."
print(datetime.datetime.now(), "Starting SSL connection.")
sys.stdout.flush()
super(IRCBot, self).__init__(self.__servers, self.__config.NICK, self.__config.NICK, connect_factory=ssl_factory)

else:
print datetime.datetime.now(), u"Starting connection."
print(datetime.datetime.now(), "Starting connection.")
sys.stdout.flush()
super(IRCBot, self).__init__(self.__servers, self.__config.NICK, self.__config.NICK)

def on_welcome(self, connection, event):
"""Login"""
if self.__config.NICKPASS:
print datetime.datetime.now(), u"Starting authentication."
print(datetime.datetime.now(), "Starting authentication.")
sys.stdout.flush()
self.send_msg("NickServ", "IDENTIFY {}".format(self.__config.NICKPASS))

"""Join the correct channel upon connecting"""
if irc.client.is_channel(self.__config.CHANNEL):
print datetime.datetime.now(), u"Joining to channel."
print(datetime.datetime.now(), "Joining to channel.")
sys.stdout.flush()
connection.join(self.__config.CHANNEL)

Expand All @@ -67,7 +64,7 @@ def on_join(self, connection, event):
self.__first_start = True

def welcome_msg(self):
msg = u"Hi, I'm the channel's " + self.get_bolded_text(self.__get_colored_text(self.color_feedname,"RSS")) + u" news publishing bot v2.0. Send " + self.__get_colored_text(self.color_num,"!help") + u" to receive a list of commands in private message (PM). If you find me annoying, you can to use " + self.__get_colored_text(self.color_num,"/IGNORE " + self.connection.get_nickname()) + u" to stop reading me."
msg = "Hi, I'm the channel " + self.get_bolded_text(self.__get_colored_text(self.color_feedname,"RSS")) + " news publishing bot v2.1. Send " + self.__get_colored_text(self.color_num,"!help") + " to receive a command list in private message (PM). If you find me annoying, you can to use " + self.__get_colored_text(self.color_num,"/IGNORE " + self.connection.get_nickname()) + " to stop reading me."
time.sleep(1)
return msg

Expand All @@ -77,7 +74,7 @@ def on_kick(self, connection, event):
botnick = self.connection.get_nickname().lower()
if irc.client.is_channel(self.__config.CHANNEL) and banned_nick == botnick:
time.sleep(31)
print datetime.datetime.now(), u"Joining to channel again."
print(datetime.datetime.now(), "Joining to channel again.")
sys.stdout.flush()
connection.join(self.__config.CHANNEL)

Expand All @@ -92,13 +89,13 @@ def __handle_msg(self, msg):
elif msg == "!list":
answer = ""
for entry in self.__db.get_feeds():
answer += "#" + self.__get_colored_text(self.color_num,str(entry[0]) + " - ") + self.get_bolded_text(self.__get_colored_text(self.color_feedname,entry[1] + " > ")) + self.__get_colored_text(self.color_url,entry[2] + ",") + u" updated every " + self.__get_colored_text(self.color_num,str(entry[3])) + u" minutes." + "\n"
answer += "# " + self.__get_colored_text(self.color_num,str(entry[0]) + "- ") + self.get_bolded_text(self.__get_colored_text(self.color_feedname,entry[1] + " > ")) + self.__get_colored_text(self.color_url,entry[2] + ",") + " updated every " + self.__get_colored_text(self.color_num,str(entry[3])) + " minutes." + "\n"

# Print some simple stats (Feed / News count)
elif msg == "!stats":
feeds_count = self.__db.get_feeds_count()
news_count = self.__db.get_news_count()
answer = u"Feeds: " + self.get_bolded_text(self.__get_colored_text(self.color_num,str(feeds_count))) + u", News: " + self.get_bolded_text(self.__get_colored_text(self.color_num,str(news_count)))
answer = "Feeds: " + self.get_bolded_text(self.__get_colored_text(self.color_num,str(feeds_count))) + ", News: " + self.get_bolded_text(self.__get_colored_text(self.color_num,str(news_count)))

# Print last config.feedlimit news.
elif msg == "!last":
Expand All @@ -108,28 +105,28 @@ def __handle_msg(self, msg):
items = items[::-1]

for entry in items:
answer += "#" + self.__get_colored_text(self.color_num,str(entry[0]) + " - ") + self.get_bolded_text(self.__get_colored_text(self.color_newstitle,entry[1] + " > ")) + self.__get_colored_text(self.color_url,entry[2] + ", ") + self.__get_colored_text(self.color_date,str(entry[3])) + "\n"
answer += "# " + self.__get_colored_text(self.color_num,str(entry[0]) + "- ") + self.get_bolded_text(self.__get_colored_text(self.color_newstitle,entry[1] + " > ")) + self.__get_colored_text(self.color_url,entry[2] + ", ") + self.__get_colored_text(self.color_date,str(entry[3])) + "\n"

# Print last config.feedlimit news for a specific feed
elif msg.startswith("!lastfeed"):
answer = ""
try:
feedid = int(msg.replace("!lastfeed","").strip())
except:
return self.__get_colored_text('1',u"Wrong command. ") + msg + u". Send !lastfeed <feedid>"
return self.__get_colored_text('1',"Wrong command. ") + msg + ". Send !lastfeed <feedid>"
items = self.__db.get_news_from_feed(feedid, self.__config.feedlimit)
if not self.__config.feedorderdesc:
items = items[::-1]
for entry in items:
answer += "#" + self.__get_colored_text(self.color_num,str(entry[0]) + " - ") + self.get_bolded_text(self.__get_colored_text(self.color_newstitle,entry[1] + " > ")) + self.__get_colored_text(self.color_url,entry[2] + ", ") + self.__get_colored_text(self.color_date,str(entry[3])) + "\n"
answer += "# " + self.__get_colored_text(self.color_num,str(entry[0]) + "- ") + self.get_bolded_text(self.__get_colored_text(self.color_newstitle,entry[1] + " > ")) + self.__get_colored_text(self.color_url,entry[2] + ", ") + self.__get_colored_text(self.color_date,str(entry[3])) + "\n"

# Else tell the user how to use the bot
else:
answer = u"Send !help to see the available commands."
answer = "Send !help to see the available commands."
except Exception as e:
print datetime.datetime.now(), e
print(datetime.datetime.now(), e)
sys.stdout.flush()
answer = u"Something was wrong."
answer = "Something was wrong."
return answer

def on_privmsg(self, connection, event):
Expand All @@ -139,7 +136,7 @@ def on_privmsg(self, connection, event):

# Get the message and return an answer
msg = event.arguments[0].lower().strip()
print datetime.datetime.now(), msg, "command from", event.source.nick
print(datetime.datetime.now(), msg, "command from", event.source.nick)
sys.stdout.flush()
answer = self.__handle_msg(msg)
self.send_msg(event.source.nick, answer)
Expand Down Expand Up @@ -171,28 +168,28 @@ def send_msg(self, target, msg):
for line in msg.split("\n"):
# Split lines that are longer than 510 characters into multiple messages.
for sub_line in re.findall('.{1,510}', line):
time.sleep(2) # Don't flood the target
self.connection.privmsg(target, sub_line)
time.sleep(1) # Don't flood the target
except Exception as e:
print datetime.datetime.now(), e
print(datetime.datetime.now(), e)
sys.stdout.flush()

def post_news(self, feed_name, title, url, date):
"""Cancel post if filter keyword is in title"""
for keyword in self.filterkeywords:
if keyword in title.lower():
print datetime.datetime.now(), u"Found", keyword, "keyword in title. Aborting post."
print(datetime.datetime.now(), "Found", keyword, "keyword in title. Aborting post.")
sys.stdout.flush()
return
"""Try shortening url"""
if self.__config.shorturls:
try:
post_url = tinyurl.create_one(url)
post_url = self.shorten(url)
if ("error" in post_url.lower()):
post_url = url
except Exception as e:
post_url = url
print datetime.datetime.now(), e
print(datetime.datetime.now(), e)
sys.stdout.flush()
else:
post_url = url
Expand All @@ -201,9 +198,18 @@ def post_news(self, feed_name, title, url, date):
msg = self.__get_colored_text(self.color_feedname,feed_name + ": ") + self.get_bolded_text(self.__get_colored_text(self.color_newstitle,title)) + " > " + self.__get_colored_text(self.color_url,post_url + ", ") + self.__get_colored_text(self.color_date,str(date))
self.send_msg(self.__config.CHANNEL, msg)
except Exception as e:
print datetime.datetime.now(), e
print(datetime.datetime.now(), e)
sys.stdout.flush()

def shorten(self, url):
try: # Trying to shorten URL
sresponse = requests.get('https://v.gd/create.php?format=json&url=' + url)
surl = sresponse.json()['shorturl']
except Exception as err:
print('A shortening error occurred.')
surl = url
return surl

def __get_colored_text(self, color, text):
if not self.__config.use_colors:
return text
Expand All @@ -216,11 +222,11 @@ def get_bolded_text(self, string):

def __help_msg(self):
"""Returns the help/usage message"""
return u"""\
return """\
Help:
- /IGNORE """ + self.connection.get_nickname() + u""" - Lets you stop reading the bot.
- /IGNORE """ + self.connection.get_nickname() + """ - Lets you stop reading the bot.

You can send these commands in private message (PM) to """ + self.connection.get_nickname() + u""":
You can send these commands in private message (PM) to """ + self.connection.get_nickname() + """:
- !help - Show this help message.
- !stats - Show some statistics.
- !list - Show all feeds.
Expand All @@ -241,9 +247,9 @@ def __init__(self):
self.__connected = False

def __check_config(self):
necessary_options = ["HOST", "PORT", "PASSWORD", "SSL", "CHANNEL", "NICK", "admin_nicks", "use_colors",
"num_col", "date", "feedname", "shorturls", "dateformat", "feedlimit", "update_before_connecting",
"url", "feedorderdesc"]
necessary_options = ["HOST", "PORT", "PASSWORD", "SSL", "CHANNEL",
"use_colors", "num_col", "feedname", "newstitle", "url", "date", "shorturls", "dateformat", "feedlimit",
"postdelay", "feedorderdesc", "update_before_connecting", "filterkeywords"]
missing_options = []
for key in necessary_options:
if not hasattr(self.__config, key):
Expand All @@ -255,24 +261,24 @@ def get_missing_options(self):

def start(self):
"""Starts the IRC bot"""
print datetime.datetime.now(), u"Starting bot."
print(datetime.datetime.now(), "Starting bot.")
sys.stdout.flush()
threading.Thread(target=self.__irc.start).start()

def initial_feed_update(self):
def print_feed_update(feed_title, news_title, news_url, news_date):
print datetime.datetime.now(), u"[+]: {}||{}||{}||{}".format(feed_title, news_title, news_url, news_date)
print(datetime.datetime.now(), "[+]: {}||{}||{}||{}".format(feed_title, news_title, news_url, news_date))
sys.stdout.flush()

if self.__config.update_before_connecting:
print datetime.datetime.now(), u"Starting offline update."
print(datetime.datetime.now(), "Starting offline update.")
sys.stdout.flush()
self.__feedupdater.update_feeds(print_feed_update, False)

def on_started(self):
"""Gets executed after the IRC thread has successfully established a connection."""
if not self.__connected:
print datetime.datetime.now(), u"Starting feeds periodic update..."
print(datetime.datetime.now(), "Starting feeds periodic update...")
sys.stdout.flush()
self.__feedupdater.update_feeds(self.__irc.post_news, True)
self.__connected = True
3 changes: 0 additions & 3 deletions colour.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

class Colours:
def __init__(self, col, string):
self.colour = col
Expand Down
18 changes: 9 additions & 9 deletions config.py.sample
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@ class Config(object):
lastpubmsg = 0

def __init__(self):
self.HOST = "irc.rizon.net"
self.HOST = 'irc.rizon.net'
self.PORT = 9999
self.PASSWORD = None
self.SSL = True
self.NICK = "rss2irc"
self.NICKPASS = ""
self.CHANNEL = "#tests"
self.admin_nicks= ['']
self.NICK = 'rss2irc'
self.NICKPASS = ''
self.CHANNEL = '#tests'

"""
'''
00 - white 01 - black 02 - blue (navy) 03 - green
04 - red 05 - brown 06 - purple 07 - orange
08 - yellow 09 - light green 10 - teal 11 - cyan
12 - light blue 13 - pink 14 - grey 15 - light grey
"""
'''

self.use_colors = True
self.num_col = 'red' # Empty for dafault color
Expand All @@ -33,5 +32,6 @@ class Config(object):
self.postdelay = 180 # Post delay during conversation in seconds. 0 for off.
self.feedorderdesc = False
self.update_before_connecting = True # Update all feeds before connecting to the IRC server
self.filterkeywords = {} # Lower-case keywords list to filter in title news. Use
# {"one","two","three"} to activate or {} for deactivate.
self.filterkeywords = () # Lower-case keywords list to filter in title news. Use
# ('one','two','three') to activate or () for deactivate.

3 changes: 0 additions & 3 deletions db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

from sqlite3worker import Sqlite3Worker
from config import Config
import os
Expand Down
Loading