This repository has been archived on 2021-04-24. You can view files and clone it, but cannot push or open issues or pull requests.
RikoBot/bot.py

423 lines
14 KiB
Python
Raw Permalink Normal View History

2017-09-20 23:25:57 +02:00
#!/usr/bin/env python3
2016-11-21 23:04:28 +01:00
# -*- coding: utf-8 -*-
2019-03-18 20:33:47 +01:00
import gettext
2017-09-20 18:26:31 +02:00
import html
2016-12-13 21:20:02 +01:00
import logging
2016-11-21 23:04:28 +01:00
import re
2017-09-20 23:25:57 +02:00
import sys
2016-12-13 21:20:02 +01:00
from configparser import ConfigParser
from json import loads
2016-12-13 21:20:02 +01:00
from urllib.parse import urlparse
2016-11-21 23:04:28 +01:00
2016-12-13 21:20:02 +01:00
import feedparser
import redis
2017-09-20 23:25:57 +02:00
import telegram
from telegram.error import Unauthorized
2017-09-20 23:25:57 +02:00
from telegram.ext import CommandHandler, Updater
2016-11-21 23:04:28 +01:00
from telegram.ext.dispatcher import run_async
2017-09-20 23:25:57 +02:00
import utils
2016-11-21 23:04:28 +01:00
2017-09-20 23:25:57 +02:00
config = ConfigParser()
try:
config.read_file(open("config.ini"))
2017-09-20 23:25:57 +02:00
except FileNotFoundError:
2019-03-18 18:44:21 +01:00
logging.critical("config.ini not found")
2017-09-20 23:25:57 +02:00
sys.exit(1)
2019-03-18 20:33:47 +01:00
# Language
try:
lang = config["DEFAULT"]["lang"]
except KeyError:
lang = "en"
el = gettext.translation("base", localedir="locales", languages=[lang, "en"])
el.install()
_ = el.gettext
# Logging
try:
logging_conf = config["LOGGING"]
logging_level = logging_conf.get("level", "INFO")
logging_format = logging_conf.get("format", "%(asctime)s - %(levelname)s: %(message)s", raw=True)
if logging_level not in ["DEBUG", "INFO", "CRITICAL", "ERROR", "WARNING"]:
2019-03-18 20:33:47 +01:00
logging.warning(_("Logging Level invalid. Will be changed to INFO"))
logging.basicConfig(format=logging_format, level=logging.INFO, datefmt="%d.%m.%Y %H:%M:%S")
else:
logging.basicConfig(format=logging_format,
level=eval("logging.{0}".format(logging_level.upper())),
datefmt="%d.%m.%Y %H:%M:%S")
except KeyError:
logging.basicConfig(format="%(asctime)s - %(levelname)s: %(message)s",
level=logging.INFO,
datefmt="%d.%m.%Y %H:%M:%S")
logger = logging.getLogger(__name__)
2017-09-20 23:25:57 +02:00
# Bot token
try:
bot_token = config["DEFAULT"]["token"]
2017-09-20 23:25:57 +02:00
except KeyError:
2019-03-18 20:33:47 +01:00
logger.error(_("Bot token is missing, check config.ini."))
2017-09-20 23:25:57 +02:00
sys.exit(1)
if not bot_token:
2019-03-18 20:33:47 +01:00
logger.error(_("Bot token is missing, check config.ini."))
2017-09-20 23:25:57 +02:00
sys.exit(1)
# Admins
2017-09-20 23:25:57 +02:00
try:
admins = loads(config["ADMIN"]["id"])
except KeyError:
2019-03-18 20:33:47 +01:00
logger.error(_("No admin IDs are set, check config.ini."))
2017-09-20 23:25:57 +02:00
sys.exit(1)
if not admins:
2019-03-18 20:33:47 +01:00
logger.error(_("No admin IDs are set, check config.ini."))
2017-09-20 23:25:57 +02:00
sys.exit(1)
for admin in admins:
if not isinstance(admin, int):
2019-03-18 20:33:47 +01:00
logger.error(_("Admin IDs need to be integers."))
2017-09-20 23:25:57 +02:00
sys.exit(1)
2017-09-20 23:25:57 +02:00
# Redis
redis_conf = config["REDIS"]
redis_db = redis_conf.get("db", 0)
redis_host = redis_conf.get("host", "127.0.0.1")
redis_port = redis_conf.get("port", 6379)
redis_socket = redis_conf.get("socket_path")
2016-11-21 23:04:28 +01:00
if redis_socket:
r = redis.Redis(unix_socket_path=redis_socket, db=int(redis_db), decode_responses=True)
else:
r = redis.Redis(host=redis_host, port=int(redis_port), db=int(redis_db), decode_responses=True)
if not r.ping():
2019-03-18 20:33:47 +01:00
logging.getLogger("Redis").critical(_("Failed to connect to Redis server."))
2017-09-20 23:25:57 +02:00
sys.exit(1)
2016-12-13 21:20:02 +01:00
feed_hash = "pythonbot:rss:{0}"
2016-12-13 21:20:02 +01:00
2017-09-20 23:25:57 +02:00
@run_async
2019-03-18 18:56:04 +01:00
def start(update, context):
2017-09-20 23:25:57 +02:00
if not utils.can_use_bot(update):
2016-11-21 23:04:28 +01:00
return
2017-09-20 23:25:57 +02:00
update.message.reply_text(
2017-09-21 00:13:40 +02:00
text='<b>Willkommen bei RikoBot!</b>\nSende /help, um zu starten.',
2017-09-20 23:25:57 +02:00
parse_mode=telegram.ParseMode.HTML
)
2016-12-13 21:20:02 +01:00
2016-11-21 23:04:28 +01:00
@run_async
2019-03-18 18:56:04 +01:00
def help_text(update, context):
2017-09-20 23:25:57 +02:00
if not utils.can_use_bot(update):
2016-12-13 21:20:02 +01:00
return
2017-09-20 23:25:57 +02:00
update.message.reply_text(
2019-03-18 20:33:47 +01:00
text=_("<b>/rss</b> <i>[Chat]</i>: Show subbed feeds\n"
"<b>/sub</b> <i>Feed URL</i> <i>[Chat]</i>: Sub to feed\n"
"<b>/del</b> <i>n</i> <i>[Chat]</i>: Unsubscribe feed\n"
"<i>[Chat]</i> is an optional argument with the @Channelname."),
2017-09-20 23:25:57 +02:00
parse_mode=telegram.ParseMode.HTML
2016-12-13 21:20:02 +01:00
)
2016-11-21 23:04:28 +01:00
@run_async
2019-03-18 18:56:04 +01:00
def list_feeds(update, context):
2017-09-20 23:25:57 +02:00
if not utils.can_use_bot(update):
2016-12-13 21:20:02 +01:00
return
2019-03-18 18:56:04 +01:00
if context.args:
chat_name = context.args[0]
2017-09-20 23:25:57 +02:00
try:
2019-03-18 18:56:04 +01:00
resp = context.bot.getChat(chat_name)
2017-09-20 23:25:57 +02:00
except telegram.error.BadRequest:
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("This channel does not exist."))
2017-09-20 23:25:57 +02:00
return
chat_id = str(resp.id)
chat_title = resp.title
else:
chat_id = str(update.message.chat.id)
if update.message.chat.type == "private":
2017-09-20 23:25:57 +02:00
chat_title = update.message.chat.first_name
else:
chat_title = update.message.chat.title
subs = r.smembers(feed_hash.format(chat_id))
if not subs:
2019-03-18 20:33:47 +01:00
text = "" + _("There are no feeds.")
2017-09-20 23:25:57 +02:00
else:
2019-03-18 20:33:47 +01:00
text = _("<b>{0}</b> is subscribed to:\n").format(html.escape(chat_title))
2017-09-20 23:25:57 +02:00
for n, feed in enumerate(subs):
text += "<b>" + str(n + 1) + ")</b> " + feed + "\n"
2017-09-20 23:25:57 +02:00
update.message.reply_text(
text=text,
parse_mode=telegram.ParseMode.HTML
2016-12-13 21:20:02 +01:00
)
2016-11-21 23:04:28 +01:00
2017-09-20 23:25:57 +02:00
@run_async
2019-03-18 18:56:04 +01:00
def subscribe(update, context):
2017-09-20 23:25:57 +02:00
if not utils.can_use_bot(update):
2016-12-13 21:20:02 +01:00
return
2019-03-18 18:56:04 +01:00
if not context.args:
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("No feed URL given."))
2016-12-13 21:20:02 +01:00
return
2019-03-18 18:56:04 +01:00
feed_url = context.args[0]
if not re.match("^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&~+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$", feed_url):
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("That doesn't look like an URL."))
2016-12-13 21:20:02 +01:00
return
2016-11-21 23:04:28 +01:00
2017-09-20 23:25:57 +02:00
# Get Chat ID from name if given
2019-03-18 18:56:04 +01:00
if len(context.args) > 1:
chat_name = context.args[1]
2017-09-20 23:25:57 +02:00
try:
2019-03-18 18:56:04 +01:00
resp = context.bot.getChat(chat_name)
2017-09-20 23:25:57 +02:00
except telegram.error.BadRequest:
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("This channel does not exist."))
2017-09-20 23:25:57 +02:00
return
chat_id = str(resp.id)
2019-03-18 18:56:04 +01:00
resp = context.bot.getChatMember(chat_id, context.bot.id)
if resp.status != "administrator":
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("You need to add the bot as an administrator to the channel."))
2016-12-13 21:20:02 +01:00
return
2016-11-21 23:04:28 +01:00
else:
2017-09-20 23:25:57 +02:00
chat_id = str(update.message.chat.id)
2016-12-13 21:20:02 +01:00
2019-03-18 18:56:04 +01:00
context.bot.sendChatAction(update.message.chat.id, action=telegram.ChatAction.TYPING)
2017-09-20 23:25:57 +02:00
data = feedparser.parse(feed_url)
if "link" not in data.feed:
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("Not a valid feed."))
2016-12-13 21:20:02 +01:00
return
2017-09-20 23:25:57 +02:00
feed_url = data.href # Follow all redirects
if r.sismember(feed_hash.format(chat_id), feed_url):
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("Already subscribed to this feed."))
2016-12-13 21:20:02 +01:00
return
if "title" not in data.feed:
2017-09-20 23:25:57 +02:00
feed_title = feed_url
2016-11-21 23:04:28 +01:00
else:
feed_title = html.escape(data.feed["title"])
2017-09-20 23:25:57 +02:00
# Save the last entry in Redis, if it doesn't exist
if data.entries:
last_entry_hash = feed_hash.format(feed_url + ":last_entry")
2017-09-20 23:25:57 +02:00
if not r.exists(last_entry_hash):
if "id" not in data.entries[0]:
last_entry = data.entries[0]["link"]
2017-09-20 23:25:57 +02:00
else:
last_entry = data.entries[0]["id"]
2017-09-20 23:25:57 +02:00
r.set(last_entry_hash, last_entry)
r.sadd(feed_hash.format(feed_url + ":subs"), chat_id)
2017-09-20 23:25:57 +02:00
r.sadd(feed_hash.format(chat_id), feed_url)
update.message.reply_text(
2019-03-18 20:33:47 +01:00
text="" + _("<b>{0}</b> added!").format(feed_title),
2017-09-20 23:25:57 +02:00
parse_mode=telegram.ParseMode.HTML
2016-12-13 21:20:02 +01:00
)
2016-11-21 23:04:28 +01:00
2017-09-20 23:25:57 +02:00
@run_async
2019-03-18 18:56:04 +01:00
def unsubscribe(update, context):
2017-09-20 23:25:57 +02:00
if not utils.can_use_bot(update):
2016-12-13 21:20:02 +01:00
return
2019-03-18 18:56:04 +01:00
if not context.args:
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("No number given."))
2016-11-21 23:04:28 +01:00
return
2016-12-13 21:20:02 +01:00
2017-09-20 23:25:57 +02:00
# Get Chat ID from name if given
2019-03-18 18:56:04 +01:00
if len(context.args) > 1:
chat_name = context.args[1]
2017-09-20 23:25:57 +02:00
try:
2019-03-18 18:56:04 +01:00
resp = context.bot.getChat(chat_name)
2017-09-20 23:25:57 +02:00
except telegram.error.BadRequest:
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("This channel does not exist."))
2016-12-13 21:20:02 +01:00
return
2017-09-20 23:25:57 +02:00
chat_id = str(resp.id)
2016-11-21 23:04:28 +01:00
else:
2017-09-20 23:25:57 +02:00
chat_id = str(update.message.chat.id)
2016-12-13 21:20:02 +01:00
2017-09-20 23:25:57 +02:00
try:
2019-03-18 18:56:04 +01:00
n = int(context.args[0])
2017-09-20 23:25:57 +02:00
except ValueError:
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("No number given."))
2016-12-13 21:20:02 +01:00
return
2017-09-20 23:25:57 +02:00
chat_hash = feed_hash.format(chat_id)
subs = r.smembers(chat_hash)
if n < 1:
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("Number must be bigger than 0!"))
2017-09-20 23:25:57 +02:00
return
elif n > len(subs):
2019-03-18 20:33:47 +01:00
update.message.reply_text("" + _("Feed ID too high."))
2016-11-21 23:04:28 +01:00
return
2017-09-20 23:25:57 +02:00
feed_url = list(subs)[n - 1]
sub_hash = feed_hash.format(feed_url + ":subs")
2017-09-20 23:25:57 +02:00
r.srem(chat_hash, feed_url)
r.srem(sub_hash, chat_id)
if not r.smembers(sub_hash): # no one subscribed, remove it
r.delete(feed_hash.format(feed_url + ":last_entry"))
2016-12-13 21:20:02 +01:00
2017-09-20 23:25:57 +02:00
update.message.reply_text(
2019-03-18 20:33:47 +01:00
text="" + _("<b>{0}</b> removed!").format(feed_url),
2017-09-20 23:25:57 +02:00
parse_mode=telegram.ParseMode.HTML
)
2016-11-21 23:04:28 +01:00
2016-12-13 21:20:02 +01:00
@run_async
2017-09-20 23:25:57 +02:00
def check_feed(bot, key):
feed_url = re.match("^" + feed_hash.format("(.+):subs$"), key).group(1)
2017-09-20 23:25:57 +02:00
logger.info(feed_url)
data = feedparser.parse(feed_url)
if "link" not in data.feed:
if "status" in data and data["status"] != 200:
2019-03-18 20:33:47 +01:00
logger.warning(_("{0} - Not a valid feed, got HTTP Code {1}").format(feed_url, data["status"]))
else:
2019-03-18 20:33:47 +01:00
logger.warning(_("{0} - Not a valid feed: {1}").format(feed_url, str(data.bozo_exception)))
2017-09-20 23:25:57 +02:00
return None
if "title" not in data.feed:
feed_title = data.feed["link"]
2016-11-21 23:04:28 +01:00
else:
feed_title = data.feed["title"]
last_entry_hash = feed_hash.format(feed_url + ":last_entry")
2017-09-20 23:25:57 +02:00
last_entry = r.get(last_entry_hash)
new_entries = utils.get_new_entries(data.entries, last_entry)
for entry in reversed(new_entries):
if "title" not in entry:
2019-03-18 20:33:47 +01:00
post_title = _("No title")
2016-12-13 21:20:02 +01:00
else:
post_title = utils.remove_html_tags(entry["title"]).strip()
post_title = post_title.replace("<", "&lt;").replace(">", "&gt;")
if "link" not in entry:
post_link = data.link
2017-09-20 23:25:57 +02:00
link_name = post_link
else:
2017-09-20 23:25:57 +02:00
post_link = entry.link
feedproxy = re.search("^https?://feedproxy\.google\.com/~r/(.+?)/.*", post_link) # feedproxy.google.com
2017-09-20 23:25:57 +02:00
if feedproxy:
link_name = feedproxy.group(1)
2016-12-13 21:20:02 +01:00
else:
2017-09-20 23:25:57 +02:00
link_name = urlparse(post_link).netloc
link_name = re.sub("^www\d?\.", "", link_name) # remove www.
if "content" in entry:
content = utils.get_content(entry.content[0]["value"])
elif "summary" in entry:
2017-09-20 23:25:57 +02:00
content = utils.get_content(entry.summary)
else:
content = ""
2019-10-22 19:52:46 +02:00
text = "<b>[</b>#RSS<b>] {post_title}</b>\n{content}".format(
2017-09-20 23:25:57 +02:00
post_title=post_title,
feed_title=feed_title,
content=content
)
2019-03-18 20:33:47 +01:00
readmore = _("Read more on {0}").format(link_name)
2019-10-22 20:42:26 +02:00
text += '\n<a href="{post_link}">{readmore}</a>\n'.format(
2017-09-20 23:25:57 +02:00
post_link=post_link,
2019-03-18 20:33:47 +01:00
readmore=readmore
2017-09-20 23:25:57 +02:00
)
for member in r.smembers(key):
try:
bot.sendMessage(
chat_id=member,
text=text,
parse_mode=telegram.ParseMode.HTML,
disable_web_page_preview=True
)
except telegram.error.Unauthorized:
2019-03-18 20:33:47 +01:00
logger.warning(_("Chat {0} doesn't exist anymore, will be deleted.").format(member))
2017-09-20 23:25:57 +02:00
r.srem(key, member)
r.delete(feed_hash.format(member))
except telegram.error.ChatMigrated as new_chat:
new_chat_id = new_chat.new_chat_id
2019-03-18 20:33:47 +01:00
logger.info(_("Chat migrated: ") + member + " -> " + str(new_chat_id))
2017-09-20 23:25:57 +02:00
r.srem(key, member)
r.sadd(key, new_chat_id)
r.rename(feed_hash.format(member), feed_hash.format(new_chat_id))
bot.sendMessage(
chat_id=member,
text=text,
parse_mode=telegram.ParseMode.HTML,
disable_web_page_preview=True
)
except telegram.error.TimedOut:
pass
2017-10-10 20:05:47 +02:00
except telegram.error.BadRequest as exception:
logger.error(exception)
2017-09-20 23:25:57 +02:00
if not r.exists(key):
r.delete(last_entry_hash)
2016-12-13 21:20:02 +01:00
return
2017-09-20 23:25:57 +02:00
# Set the new last entry if there are any
if new_entries:
if "id" not in new_entries[0]:
2017-09-20 23:25:57 +02:00
new_last_entry = new_entries[0].link
else:
new_last_entry = new_entries[0].id
r.set(last_entry_hash, new_last_entry)
2016-11-21 23:04:28 +01:00
2016-12-13 21:20:02 +01:00
2017-09-20 23:25:57 +02:00
@run_async
2019-03-18 18:56:04 +01:00
def run_job(context):
logger.info("================================")
keys = r.keys(feed_hash.format("*:subs"))
2017-09-20 23:25:57 +02:00
for key in keys:
2019-03-18 18:56:04 +01:00
check_feed(context.bot, key)
2016-11-21 23:04:28 +01:00
2019-03-18 18:56:04 +01:00
def run_job_manually(update, context):
run_job(context)
2016-11-21 23:04:28 +01:00
2019-03-18 18:56:04 +01:00
def onerror(update, context):
2019-03-18 20:33:47 +01:00
logger.error(_("Update \"%s\" caused error \"%s\""), update, context.error)
2017-09-20 23:25:57 +02:00
# Main function
2016-11-21 23:04:28 +01:00
def main():
2017-09-20 23:25:57 +02:00
# Setup the updater and show bot info
2019-03-18 18:56:04 +01:00
updater = Updater(token=bot_token, use_context=True)
2017-09-20 23:25:57 +02:00
try:
2019-03-18 20:33:47 +01:00
logger.info(
_("Starting {0}, AKA @{1} ({2})").format(updater.bot.first_name, updater.bot.username, updater.bot.id))
except Unauthorized:
2019-03-18 20:33:47 +01:00
logger.critical(_("Logging in failed, check bot token."))
2017-09-20 23:25:57 +02:00
sys.exit(1)
# Register Handlers
handlers = [
CommandHandler("start", start),
CommandHandler("help", help_text),
CommandHandler("rss", list_feeds, pass_args=True),
CommandHandler("sub", subscribe, pass_args=True),
CommandHandler("del", unsubscribe, pass_args=True),
2019-03-18 18:56:04 +01:00
CommandHandler("sync", run_job_manually)
2017-09-20 23:25:57 +02:00
]
for handler in handlers:
updater.dispatcher.add_handler(handler)
# Hide "Error while getting Updates" because it's not our fault
updater.logger.addFilter((lambda log: not log.msg.startswith("Error while getting Updates:")))
# Fix for Python <= 3.5
updater.dispatcher.add_error_handler(onerror)
2017-09-20 23:25:57 +02:00
updater.job_queue.run_repeating(
run_job,
interval=60.0,
first=2.0
)
2016-11-21 23:04:28 +01:00
2017-09-20 23:25:57 +02:00
# Start this thing!
updater.start_polling(
clean=True,
bootstrap_retries=-1,
allowed_updates=["message"]
)
2016-11-21 23:04:28 +01:00
2017-09-20 23:25:57 +02:00
# Run Bot until CTRL+C is pressed or a SIGINIT,
# SIGTERM or SIGABRT is sent.
2016-11-21 23:04:28 +01:00
updater.idle()
if __name__ == "__main__":
2016-11-22 19:42:38 +01:00
main()