#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# RSS integration for Zulip
#
# Copyright © 2014 Zulip, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# 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 OR COPYRIGHT HOLDERS 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.

from __future__ import print_function
import calendar
import errno
import hashlib
from six.moves.html_parser import HTMLParser
import logging
import optparse
import os
import sys
import time
from six.moves import urllib

import feedparser
import zulip
VERSION = "0.9"
RSS_DATA_DIR = os.path.expanduser(os.path.join('~', '.cache', 'zulip-rss'))
OLDNESS_THRESHOLD = 30 # days

usage = """Usage: Send summaries of RSS entries for your favorite feeds to Zulip.

This bot requires the feedparser module.

To use this script:

1. Create an RSS feed file containing 1 feed URL per line (default feed
   file location: ~/.cache/zulip-rss/rss-feeds)
2. Subscribe to the stream that will receive RSS updates (default stream: rss)
3. create a ~/.zuliprc as described on https://zulipchat.com/api#api_keys
4. Test the script by running it manually, like this:

/usr/local/share/zulip/integrations/rss/rss-bot

You can customize the location on the feed file and recipient stream, e.g.:

/usr/local/share/zulip/integrations/rss/rss-bot --feed-file=/path/to/my-feeds --stream=my-rss-stream

4. Configure a crontab entry for this script. A sample crontab entry for
processing feeds stored in the default location and sending to the default
stream every 5 minutes is:

*/5 * * * * /usr/local/share/zulip/integrations/rss/rss-bot"""

parser = optparse.OptionParser(usage)
parser.add_option('--stream',
                  dest='stream',
                  help='The stream to which to send RSS messages.',
                  default="rss",
                  action='store')
parser.add_option('--data-dir',
                  dest='data_dir',
                  help='The directory where feed metadata is stored',
                  default=os.path.join(RSS_DATA_DIR),
                  action='store')
parser.add_option('--feed-file',
                  dest='feed_file',
                  help='The file containing a list of RSS feed URLs to follow, one URL per line',
                  default=os.path.join(RSS_DATA_DIR, "rss-feeds"),
                  action='store')
parser.add_option_group(zulip.generate_option_group(parser))
(opts, args) = parser.parse_args()

def mkdir_p(path):
    # Python doesn't have an analog to `mkdir -p` < Python 3.2.
    try:
        os.makedirs(path)
    except OSError as e:
        if e.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            raise

try:
    mkdir_p(opts.data_dir)
except OSError:
    # We can't write to the logfile, so just print and give up.
    print("Unable to store RSS data at %s." % (opts.data_dir,), file=sys.stderr)
    exit(1)

log_file = os.path.join(opts.data_dir, "rss-bot.log")
log_format = "%(asctime)s: %(message)s"
logging.basicConfig(format=log_format)

formatter = logging.Formatter(log_format)
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(file_handler)

def log_error_and_exit(error):
    logger.error(error)
    logger.error(usage)
    exit(1)

class MLStripper(HTMLParser):
    def __init__(self):
        self.reset()
        self.fed = []

    def handle_data(self, data):
        self.fed.append(data)

    def get_data(self):
        return ''.join(self.fed)

def strip_tags(html):
    stripper = MLStripper()
    stripper.feed(html)
    return stripper.get_data()

def compute_entry_hash(entry):
    entry_time = entry.get("published", entry.get("updated"))
    entry_id = entry.get("id", entry.get("link"))
    return hashlib.md5(entry_id + str(entry_time)).hexdigest()

def elide_subject(subject):
    MAX_TOPIC_LENGTH = 60
    if len(subject) > MAX_TOPIC_LENGTH:
        subject = subject[:MAX_TOPIC_LENGTH - 3].rstrip() + '...'
    return subject

def send_zulip(entry, feed_name):
    content = "**[%s](%s)**\n%s\n%s" % (entry.title,
                                        entry.link,
                                        strip_tags(entry.summary),
                                        entry.link)
    message = {"type": "stream",
               "sender": opts.zulip_email,
               "to": opts.stream,
               "subject": elide_subject(feed_name),
               "content": content,
               }
    return client.send_message(message)

try:
    with open(opts.feed_file, "r") as f:
        feed_urls = [feed.strip() for feed in f.readlines()]
except IOError:
    log_error_and_exit("Unable to read feed file at %s." % (opts.feed_file,))

client = zulip.Client(email=opts.zulip_email, api_key=opts.zulip_api_key,
                      site=opts.zulip_site, client="ZulipRSS/" + VERSION)

first_message = True

for feed_url in feed_urls:
    feed_file = os.path.join(opts.data_dir, urllib.parse.urlparse(feed_url).netloc)

    try:
        with open(feed_file, "r") as f:
            old_feed_hashes = dict((line.strip(), True) for line in f.readlines())
    except IOError:
        old_feed_hashes = {}

    new_hashes = []
    data = feedparser.parse(feed_url)

    for entry in data.entries:
        entry_hash = compute_entry_hash(entry)
        # An entry has either been published or updated.
        entry_time  = entry.get("published_parsed", entry.get("updated_parsed"))
        if entry_time is not None and (time.time() - calendar.timegm(entry_time)) > OLDNESS_THRESHOLD * 60 * 60 * 24:
            # As a safeguard against misbehaving feeds, don't try to process
            # entries older than some threshold.
            continue
        if entry_hash in old_feed_hashes:
            # We've already seen this. No need to process any older entries.
            break
        if (not old_feed_hashes) and (len(new_hashes) >= 3):
            # On a first run, pick up the 3 most recent entries. An RSS feed has
            # entries in reverse chronological order.
            break

        feed_name = data.feed.title or feed_url

        response = send_zulip(entry, feed_name)
        if response["result"] != "success":
            logger.error("Error processing %s" % (feed_url,))
            logger.error(response)
            if first_message:
                # This is probably some fundamental problem like the stream not
                # existing or something being misconfigured, so bail instead of
                # getting the same error for every RSS entry.
                log_error_and_exit("Failed to process first message")
        # Go ahead and move on -- perhaps this entry is corrupt.
        new_hashes.append(entry_hash)
        first_message = False

    with open(feed_file, "a") as f:
        for hash in new_hashes:
            f.write(hash + "\n")

    logger.info("Sent zulips for %d %s entries" % (len(new_hashes), feed_url))