#!/usr/bin/env python3

import logging
import os
import pickle
import random
import re
import sys
import time
import traceback
from datetime import datetime as dt
from subprocess import run

import feedparser
import html2markdown
from fuzzywuzzy import process
from telegram import Update, ParseMode
from telegram.ext import Updater, CommandHandler, CallbackContext

# Read bot token from environment
TOKEN = os.environ['MIA_TG_TOKEN']
CHAT_IDS = os.environ['MIA_TG_CHATID'].split(',')
URL = f"https://api.telegram.org/bot{TOKEN}/"
DUMP = os.getenv('MIA_DUMP', '')
DIRNAME = os.path.dirname(os.path.realpath(__file__))
MINKORREKT_RSS = 'http://minkorrekt.de/feed/mp3'


# Setup logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
log_format = logging.Formatter('%(asctime)s %(levelname)s - %(message)s')
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(log_format)
log_handler.setLevel(logging.INFO)
logger.addHandler(log_handler)


class PodcastFeed:
    """Represents the parsed and cached podcast RSS feed"""

    def __init__(self, url: str, max_age: int = 3600, dump: str = ''):
        """
        :param url: URL of the feed to be parsed.
        :param max_age: Time in seconds how long the parsed feed is consider valid.
                        The feed will be refreshed automatically on the first access
                        after max_age is passed.
        :param dump: Allows local storage of the feed. If a valid file path, the feed
                     object will be stored there after refresh, and reloaded on
                     re-initialization. Primarily intended to speed loading time for debugging.
        """
        self.url = url
        self.max_age = max_age
        self.dump = dump

        if dump and os.path.isfile(dump):
            try:
                with open(dump, 'rb') as f:
                    self.last_updated, self.feed = pickle.load(f)
                logger.info('Reloaded dumped feed')
            except Exception as exc:
                logger.info(f'{exc!r}\n{traceback.format_exc()}')
                logger.info('Failed loding dumped feed. Falling back to download.')
                self._get_feed()
        else:
            logger.info('Getting feed')
            self._get_feed()

    def _get_feed(self):
        self.feed = feedparser.parse(self.url)
        self.last_updated = time.time()
        logger.info('Done parsing feed')
        if self.dump:
            with open(self.dump, 'wb') as f:
                pickle.dump((self.last_updated, self.feed), f)

    def refresh(self):
        if self.last_updated + self.max_age < time.time():
            logger.info('Refreshing feed')
            self._get_feed()

    def check_new_episode(self, max_age=3600):
        latest_episode = self.latest_episode
        episode_release = dt.fromtimestamp(time.mktime(latest_episode['published_parsed']))
        if (dt.now() - episode_release).total_seconds() < max_age:
            return latest_episode
        return False

    @property
    def latest_episode(self):
        self.refresh()
        return self.feed['items'][0]

    @property
    def episode_titles(self):
        self.refresh()
        return [i.title for i in self.feed['items']]


mi_feed = PodcastFeed(url=MINKORREKT_RSS, dump=DUMP)


def tg_broadcast(text, escape_chars=['!', '#', '-']):
    """Sends the message `text` to all CHAT_IDS."""
    for c in escape_chars:
        text = re.sub(f'(?<!\\\\){c}', f'\\{c}', text)
    for chat_id in CHAT_IDS:
        bot.send_message(chat_id=chat_id,
                         text=text,
                         parse_mode=ParseMode.MARKDOWN_V2)


def check_minkorrekt(max_age=3600):
    new_episode = mi_feed.check_new_episode(max_age=max_age)
    if new_episode:
        tg_broadcast(f'*{new_episode.title}*\n'
                     'Eine neue Folge Methodisch inkorrekt ist erschienen\\!\n'
                     f'[Jetzt anhören]({new_episode.link})')


def check_youtube(max_age=3600):
    YOUTUBE_RSS = 'https://www.youtube.com/feeds/videos.xml?channel_id=UCa8qyXCS-FTs0fHD6HJeyiw'
    yt_feed = feedparser.parse(YOUTUBE_RSS)
    newest_episode = yt_feed['items'][0]
    episode_release = dt.fromtimestamp(time.mktime(newest_episode['published_parsed']))
    if (dt.now() - episode_release).total_seconds() < max_age:
        tg_broadcast(f'*{newest_episode.title}*\n'
                     'Eine neues Youtube Video ist erschienen!\n'
                     f'[Jetzt ansehen]({newest_episode.link})')


def feed_loop():
    while True:
        check_minkorrekt(3600)
        check_youtube(3600)
        time.sleep(3595)


def latest_episode(update: Update, context: CallbackContext) -> None:
    latest_episode = mi_feed.latest_episode
    episode_release = dt.fromtimestamp(time.mktime(latest_episode['published_parsed'])).date()
    datum = episode_release.strftime('%d.%m.%Y')
    text = (f'Die letzte Episode ist *{latest_episode.title}* vom {datum}.\n'
            f'[Jetzt anhören]({latest_episode.link})')
    update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN_V2)


def cookie(update: Update, context: CallbackContext) -> None:
    text = random.choice(mi_feed.episode_titles)
    update.message.reply_text(f'\U0001F36A {text} \U0001F36A', quote=False)


def crowsay(update: Update, context: CallbackContext) -> None:
    i = update.message.text.find(' ')
    if i > 0:
        text = update.message.text[i+1:]
    else:
        r = run('fortune', capture_output=True, encoding='utf-8')
        text = r.stdout

    crowfile = os.path.join(DIRNAME, 'crow.cow')
    r = run(['cowsay', '-f', crowfile, text],
            capture_output=True, encoding='utf-8')
    text = r.stdout
    update.message.reply_text(f'```\n{text}\n```', quote=False, parse_mode=ParseMode.MARKDOWN_V2)


def fuzzy_topic_search(update: Update, context: CallbackContext) -> None:
    i = update.message.text.find(' ')
    if i > 0:
        search_term = update.message.text[i+1:]
    topics_all_episodes = [[i.title, i.content[0].value.replace(
        "<!-- /wp:paragraph -->", "").replace("<!-- wp:paragraph -->", "")
                            ] for i in mi_feed.feed.entries]
    ratios = process.extract(search_term, topics_all_episodes)
    episodes = [ratio[0][0] for ratio in ratios[:3]]
    text = "Die besten 3 Treffer sind die Episoden:\n" + "\n".join(episodes)
    update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)


def topics_of_episode(update: Update, context: CallbackContext) -> None:

    regex_pattern_episode_number = r"(?:Minkorrekt(?:\ Folge)?\ |Mi)(\d+\w?)"

    # List of titles+description per episode
    episode_numbers = [re.match(regex_pattern_episode_number, entry.title)[1] if not re.match(
        regex_pattern_episode_number, entry.title)
                       is None else None for entry in mi_feed.feed.entries]
    topics_all_episodes = [[i.title, i.content[0].value.replace(
        "<!-- /wp:paragraph -->", "").replace("<!-- wp:paragraph -->", "")
                            ] for i in mi_feed.feed.entries]

    i = update.message.text.find(' ')
    if i > 0:
        requested_episode_number = update.message.text[i+1:]
        if requested_episode_number == '101':
            import pdb
            pdb.set_trace()
    else:
        text = "Bitte eine Episodennummer beim Aufruf mit angeben."
        update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)
        return None
    if requested_episode_number == '12':
        target_episode_index = episode_numbers.index("12a")
        # Get the entries for 12a and 12b and reverse the order so they're correctly displayed
        requested_episode_topics = topics_all_episodes[
            target_episode_index:target_episode_index+1][::-1]
    else:
        try:
            target_episode_index = episode_numbers.index(requested_episode_number)
        except Exception:
            text = f"""Nicht gefunden.\nEpisode {requested_episode_number}
            gab es möglicherweise nicht."""
            update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)
            return None
        requested_episode_topics = topics_all_episodes[target_episode_index]

    requested_episode_topics = " ".join(requested_episode_topics)
    topic_start_points = [m.start() for m in re.finditer(
        "Thema [1, 2, 3, 4]", requested_episode_topics)]
    topic_end_points = []
    for start in topic_start_points:
        topic_end_points.append(start + requested_episode_topics[start:].find('\n'))
    if 0 == len(topic_start_points):
        text = "Themen nicht gefunden.\nWahrscheinlich Nobelpreis/Jahresrückblick-Folge"
        update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)
        return None
    topics = [html2markdown.convert(requested_episode_topics[start:end]) for start, end in zip(
        topic_start_points, topic_end_points)]
    topics_text = "\n".join(topics)

    if requested_episode_number == '12':
        episode_title = "12a Du wirst wieder angerufen! & 12b Previously (on) Lost"
    else:
        episode_title = topics_all_episodes[target_episode_index][0]
    text = f"Die Themen von Folge {episode_title} sind:\n{topics_text}"
    update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)


updater = Updater(TOKEN)
bot = updater.bot

updater.dispatcher.add_handler(CommandHandler('findeStichwort', fuzzy_topic_search))
updater.dispatcher.add_handler(CommandHandler('themenVonFolge', topics_of_episode))
updater.dispatcher.add_handler(CommandHandler('letzteEpisode', latest_episode))
updater.dispatcher.add_handler(CommandHandler('keks', cookie))
updater.dispatcher.add_handler(CommandHandler('crowsay', crowsay))


if __name__ == '__main__':
    updater.start_polling()
    feed_loop()
    # updater.idle()

#!/usr/bin/env python3

import logging
import os
import pickle
import random
import re
import sys
import time
import traceback
from datetime import datetime as dt
from subprocess import run

import feedparser
import html2markdown
from fuzzywuzzy import process
from telegram import Update, ParseMode
from telegram.ext import Updater, CommandHandler, CallbackContext

# Read bot token from environment
TOKEN = os.environ['MIA_TG_TOKEN']
CHAT_IDS = os.environ['MIA_TG_CHATID'].split(',')
URL = f"https://api.telegram.org/bot{TOKEN}/"
DUMP = os.getenv('MIA_DUMP', '')
DIRNAME = os.path.dirname(os.path.realpath(__file__))
MINKORREKT_RSS = 'http://minkorrekt.de/feed/mp3'


# Setup logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
log_format = logging.Formatter('%(asctime)s %(levelname)s - %(message)s')
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(log_format)
log_handler.setLevel(logging.INFO)
logger.addHandler(log_handler)


class PodcastFeed:
    """Represents the parsed and cached podcast RSS feed"""

    def __init__(self, url: str, max_age: int = 3600, dump: str = ''):
        """
        :param url: URL of the feed to be parsed.
        :param max_age: Time in seconds how long the parsed feed is consider valid.
                        The feed will be refreshed automatically on the first access
                        after max_age is passed.
        :param dump: Allows local storage of the feed. If a valid file path, the feed
                     object will be stored there after refresh, and reloaded on
                     re-initialization. Primarily intended to speed loading time for debugging.
        """
        self.url = url
        self.max_age = max_age
        self.dump = dump

        if dump and os.path.isfile(dump):
            try:
                with open(dump, 'rb') as f:
                    self.last_updated, self.feed = pickle.load(f)
                logger.info('Reloaded dumped feed')
            except Exception as exc:
                logger.info(f'{exc!r}\n{traceback.format_exc()}')
                logger.info('Failed loding dumped feed. Falling back to download.')
                self._get_feed()
        else:
            logger.info('Getting feed')
            self._get_feed()

    def _get_feed(self):
        self.feed = feedparser.parse(self.url)
        self.last_updated = time.time()
        logger.info('Done parsing feed')
        if self.dump:
            with open(self.dump, 'wb') as f:
                pickle.dump((self.last_updated, self.feed), f)

    def refresh(self):
        if self.last_updated + self.max_age < time.time():
            logger.info('Refreshing feed')
            self._get_feed()

    def check_new_episode(self, max_age=3600):
        latest_episode = self.latest_episode
        episode_release = dt.fromtimestamp(time.mktime(latest_episode['published_parsed']))
        if (dt.now() - episode_release).total_seconds() < max_age:
            return latest_episode
        return False

    @property
    def latest_episode(self):
        self.refresh()
        return self.feed['items'][0]

    @property
    def episode_titles(self):
        self.refresh()
        return [i.title for i in self.feed['items']]


mi_feed = PodcastFeed(url=MINKORREKT_RSS, dump=DUMP)


def tg_broadcast(text, escape_chars=['!', '#', '-']):
    """Sends the message `text` to all CHAT_IDS."""
    for c in escape_chars:
        text = re.sub(f'(?<!\\\\){c}', f'\\{c}', text)
    for chat_id in CHAT_IDS:
        bot.send_message(chat_id=chat_id,
                         text=text,
                         parse_mode=ParseMode.MARKDOWN_V2)


def check_minkorrekt(max_age=3600):
    new_episode = mi_feed.check_new_episode(max_age=max_age)
    if new_episode:
        tg_broadcast(f'*{new_episode.title}*\n'
                     'Eine neue Folge Methodisch inkorrekt ist erschienen\\!\n'
                     f'[Jetzt anhören]({new_episode.link})')


def check_youtube(max_age=3600):
    YOUTUBE_RSS = 'https://www.youtube.com/feeds/videos.xml?channel_id=UCa8qyXCS-FTs0fHD6HJeyiw'
    yt_feed = feedparser.parse(YOUTUBE_RSS)
    newest_episode = yt_feed['items'][0]
    episode_release = dt.fromtimestamp(time.mktime(newest_episode['published_parsed']))
    if (dt.now() - episode_release).total_seconds() < max_age:
        tg_broadcast(f'*{newest_episode.title}*\n'
                     'Eine neues Youtube Video ist erschienen!\n'
                     f'[Jetzt ansehen]({newest_episode.link})')


def feed_loop():
    while True:
        check_minkorrekt(3600)
        check_youtube(3600)
        time.sleep(3595)


def latest_episode(update: Update, context: CallbackContext) -> None:
    latest_episode = mi_feed.latest_episode
    episode_release = dt.fromtimestamp(time.mktime(latest_episode['published_parsed'])).date()
    datum = episode_release.strftime('%d.%m.%Y')
    text = (f'Die letzte Episode ist *{latest_episode.title}* vom {datum}.\n'
            f'[Jetzt anhören]({latest_episode.link})')
    update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN_V2)


def cookie(update: Update, context: CallbackContext) -> None:
    text = random.choice(mi_feed.episode_titles)
    update.message.reply_text(f'\U0001F36A {text} \U0001F36A', quote=False)


def crowsay(update: Update, context: CallbackContext) -> None:
    i = update.message.text.find(' ')
    if i > 0:
        text = update.message.text[i+1:]
    else:
        r = run('fortune', capture_output=True, encoding='utf-8')
        text = r.stdout

    crowfile = os.path.join(DIRNAME, 'crow.cow')
    r = run(['cowsay', '-f', crowfile, text],
            capture_output=True, encoding='utf-8')
    text = r.stdout
    update.message.reply_text(f'```\n{text}\n```', quote=False, parse_mode=ParseMode.MARKDOWN_V2)


def fuzzy_topic_search(update: Update, context: CallbackContext) -> None:
    i = update.message.text.find(' ')
    if i > 0:
        search_term = update.message.text[i+1:]
    topics_all_episodes = [[i.title, i.content[0].value.replace(
        "<!-- /wp:paragraph -->", "").replace("<!-- wp:paragraph -->", "")
                            ] for i in mi_feed.feed.entries]
    ratios = process.extract(search_term, topics_all_episodes)
    episodes = [ratio[0][0] for ratio in ratios[:3]]
    text = "Die besten 3 Treffer sind die Episoden:\n" + "\n".join(episodes)
    update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)


def topics_of_episode(update: Update, context: CallbackContext) -> None:

    regex_pattern_episode_number = r"(?:Minkorrekt(?:\ Folge)?\ |Mi)(\d+\w?)"

    # List of titles+description per episode
    episode_numbers = [re.match(regex_pattern_episode_number, entry.title)[1] if not re.match(
        regex_pattern_episode_number, entry.title)
                       is None else None for entry in mi_feed.feed.entries]
    topics_all_episodes = [[i.title, i.content[0].value.replace(
        "<!-- /wp:paragraph -->", "").replace("<!-- wp:paragraph -->", "")
                            ] for i in mi_feed.feed.entries]

    i = update.message.text.find(' ')
    if i > 0:
        requested_episode_number = update.message.text[i+1:]
        if requested_episode_number == '101':
            import pdb
            pdb.set_trace()
    else:
        text = "Bitte eine Episodennummer beim Aufruf mit angeben."
        update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)
        return None
    if requested_episode_number == '12':
        target_episode_index = episode_numbers.index("12a")
        # Get the entries for 12a and 12b and reverse the order so they're correctly displayed
        requested_episode_topics = topics_all_episodes[
            target_episode_index:target_episode_index+1][::-1]
    else:
        try:
            target_episode_index = episode_numbers.index(requested_episode_number)
        except Exception:
            text = f"""Nicht gefunden.\nEpisode {requested_episode_number}
            gab es möglicherweise nicht."""
            update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)
            return None
        requested_episode_topics = topics_all_episodes[target_episode_index]

    requested_episode_topics = " ".join(requested_episode_topics)
    topic_start_points = [m.start() for m in re.finditer(
        "Thema [1, 2, 3, 4]", requested_episode_topics)]
    topic_end_points = []
    for start in topic_start_points:
        topic_end_points.append(start + requested_episode_topics[start:].find('\n'))
    if 0 == len(topic_start_points):
        text = "Themen nicht gefunden.\nWahrscheinlich Nobelpreis/Jahresrückblick-Folge"
        update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)
        return None
    topics = [html2markdown.convert(requested_episode_topics[start:end]) for start, end in zip(
        topic_start_points, topic_end_points)]
    topics_text = "\n".join(topics)

    if requested_episode_number == '12':
        episode_title = "12a Du wirst wieder angerufen! & 12b Previously (on) Lost"
    else:
        episode_title = topics_all_episodes[target_episode_index][0]
    text = f"Die Themen von Folge {episode_title} sind:\n{topics_text}"
    update.message.reply_text(text, quote=False, parse_mode=ParseMode.MARKDOWN)


updater = Updater(TOKEN)
bot = updater.bot

updater.dispatcher.add_handler(CommandHandler('findeStichwort', fuzzy_topic_search))
updater.dispatcher.add_handler(CommandHandler('themenVonFolge', topics_of_episode))
updater.dispatcher.add_handler(CommandHandler('letzteEpisode', latest_episode))
updater.dispatcher.add_handler(CommandHandler('keks', cookie))
updater.dispatcher.add_handler(CommandHandler('crowsay', crowsay))


if __name__ == '__main__':
    updater.start_polling()
    feed_loop()
    # updater.idle()

#!/usr/bin/env python3 # Read bot token from environment # Setup logging Represents the parsed and cached podcast RSS feed :param url: URL of the feed to be parsed.         :param max_age: Time in seconds how long the parsed feed is consider valid.                         The feed will be refreshed automatically on the first access                         after max_age is passed.         :param dump: Allows local storage of the feed. If a valid file path, the feed                      object will be stored there after refresh, and reloaded on                      re-initialization. Primarily intended to speed loading time for debugging. Sends the message `text` to all CHAT_IDS. # List of titles+description per episode # Get the entries for 12a and 12b and reverse the order so they're correctly displayed Nicht gefunden.\nEpisode {requested_episode_number}             gab es möglicherweise nicht. # updater.idle()