#!/usr/bin/env python
"""
This program lets you archive slack channels which are no longer active.
"""

# standard imports
from datetime import datetime
import os
import sys
import time
import json

# not standard imports
import requests
from config import get_channel_reaper_settings
from utils import get_logger
from dotenv import load_dotenv

load_dotenv()

class ChannelReaper():
    """
    This class can be used to archive slack channels.
    """

    def __init__(self):
        self.settings = get_channel_reaper_settings()
        self.logger = get_logger('channel_reaper', './audit.log')

    def get_whitelist_keywords(self):
        """
        Get all whitelist keywords. If this word is used in the channel
        purpose or topic, this will make the channel exempt from archiving.
        """
        keywords = []
        if os.path.isfile('whitelist.txt'):
            with open('whitelist.txt') as filecontent:
                keywords = filecontent.readlines()

        # remove whitespace characters like `\n` at the end of each line
        keywords = map(lambda x: x.strip(), keywords)
        whitelist_keywords = self.settings.get('whitelist_keywords')
        if whitelist_keywords:
            keywords = keywords + whitelist_keywords.split(',')
        return list(keywords)

    def get_channel_messages(self):
        """Get the alert message which is used to notify users in a channel of archival. """
        archive_msg = """
This channel has had no activity for %d days. It is being auto-archived.
If you feel this is a mistake you can <https://get.slack.help/hc/en-us/articles/201563847-Archive-a-channel#unarchive-a-channel|unarchive this channel>.
This will bring it back at any point. In the future, you can add '%%noarchive' to your channel topic or purpose to avoid being archived.
This script was run from a fork of this repo: https://github.com/Symantec/slack-autoarchive
""" % self.settings.get('days_inactive')
        messages = {'channel_template': archive_msg}
        if os.path.isfile('templates.json'):
            with open('templates.json') as filecontent:
                messages = json.load(filecontent)
        return messages

    # pylint: disable=too-many-arguments
    def slack_api_http(
            self,
            api_endpoint=None,
            payload=None,
            method='GET',
            # pylint: disable=unused-argument
            retry=True,
            retry_delay=0):
        """ Helper function to query the slack api and handle errors and rate limit. """
        # pylint: disable=no-member
        uri = 'https://slack.com/api/' + api_endpoint
        header = {'Authorization': 'Bearer ' + self.settings.get('bot_slack_token')}
        try:
            # Force request to take at least 1 second. Slack docs state:
            # > In general we allow applications that integrate with Slack to send
            # > no more than one message per second. We allow bursts over that
            # > limit for short periods.
            if retry_delay > 0:
                time.sleep(retry_delay)

            if method == 'POST':
                response = requests.post(uri, headers=header, data=payload)
            else:
                response = requests.get(uri, headers=header, params=payload)

            if response.status_code == requests.codes.ok and 'error' in response.json() and response.json()['error'] == 'not_authed':
                self.logger.error(
                    'Need to setup auth. eg, BOT_SLACK_TOKEN=<secret token> python slack-autoarchive.py'
                )
                sys.exit(1)
            elif response.status_code == requests.codes.ok and response.json()['ok']:
                return response.json()
            elif response.status_code == requests.codes.too_many_requests:
                retry_timeout = float(response.headers['Retry-After'])
                # pylint: disable=too-many-function-args
                return self.slack_api_http(api_endpoint, payload, method, False, retry_timeout)
        except Exception as error_msg:
            raise Exception(error_msg)
        return None

    def get_all_channels(self):
        """ Get a list of all non-archived channels from slack channels.list. """
        payload = {'exclude_archived': 1}
        api_endpoint = 'conversations.list'

        channels = []
        resp = self.slack_api_http(api_endpoint=api_endpoint, payload=payload)
        channels.extend(resp['channels'])

        while resp.get("response_metadata"):
            metadata = resp.get("response_metadata")
            if metadata.get('next_cursor'):
                payload['cursor'] = metadata.get('next_cursor')
                resp = self.slack_api_http(api_endpoint=api_endpoint, payload=payload)
                channels.extend(resp['channels'])
            else:
                break

        all_channels = []
        for channel in channels:
            all_channels.append({
                'id': channel['id'],
                'name': channel['name'],
                'created': channel['created'],
                'num_members': channel['num_members'],
                'is_member': channel['is_member']
            })

        all_channels.sort(key=lambda channel: channel['name'])

        return all_channels

    def get_last_message_timestamp(self, channel_history, too_old_datetime):
        """ Get the last message from a slack channel, and return the time. """
        last_message_datetime = too_old_datetime
        last_bot_message_datetime = too_old_datetime

        if 'messages' not in channel_history:
            return (last_message_datetime, False)  # no messages

        for message in channel_history['messages']:
            # ignore messages with some subtypes
            if 'subtype' in message and message['subtype'] in self.settings.get('skip_subtypes'):
                continue

            # messages sent by us ourselves do not have a subtype, so
            # we need to detect them differently
            if 'bot_profile' in message:
                continue

            last_message_datetime = datetime.fromtimestamp(float(message['ts']))
            break

        # for folks with the free plan, sometimes there is no last message,
        # then just set last_message_datetime to epoch
        if not last_message_datetime:
            last_bot_message_datetime = datetime.utcfromtimestamp(0)
        # return bot message time if there was no user message
        if too_old_datetime >= last_bot_message_datetime > too_old_datetime:
            return (last_bot_message_datetime, False)
        return (last_message_datetime, True)

    def is_channel_unused(self, channel, too_old_datetime):
        """ Return True or False depending on if a channel is "active" or not.  """
        num_members = channel['num_members']
        payload = {'inclusive': 0, 'oldest': 0, 'limit': 50}
        api_endpoint = 'conversations.history'

        self.logger.info('Checking #%s...' % channel['name'])

        payload['channel'] = channel['id']
        channel_history = self.slack_api_http(api_endpoint=api_endpoint, payload=payload)
        (last_message_datetime, is_user) = self.get_last_message_timestamp(
            channel_history, datetime.fromtimestamp(float(channel['created'])))
        # mark inactive if last message is too old, but don't
        # if there have been bot messages and the channel has
        # at least the minimum number of members
        min_members = self.settings.get('min_members')
        has_min_users = (min_members == 0 or min_members > num_members)

        return last_message_datetime <= too_old_datetime and (not is_user or has_min_users)

    # If you add channels to the WHITELIST_KEYWORDS constant they will be exempt from archiving.
    def is_channel_whitelisted(self, channel, white_listed_channels):
        """ Return True or False depending on if a channel is exempt from being archived. """
        # self.settings.get('skip_channel_str')
        # if the channel purpose contains the string self.settings.get('skip_channel_str'), we'll skip it.
        info_payload = {'channel': channel['id']}
        channel_info = self.slack_api_http(api_endpoint='conversations.info', payload=info_payload, method='GET')
        channel_purpose = channel_info['channel']['purpose']['value']
        channel_topic = channel_info['channel']['topic']['value']
        if self.settings.get('skip_channel_str') in channel_purpose or self.settings.get('skip_channel_str') in channel_topic:
            return True

        # check the white listed channels (file / env)
        for white_listed_channel in white_listed_channels:
            wl_channel_name = white_listed_channel.strip('#')
            if wl_channel_name in channel['name']:
                return True

        return False

    def send_channel_message(self, channel_id, message):
        """ Send a message to a channel or user. """
        payload = {
            'channel': channel_id,
            'username': 'Channel Cleaner Bot',
            'text': message
        }
        api_endpoint = 'chat.postMessage'
        self.slack_api_http(api_endpoint=api_endpoint, payload=payload, method='POST')

    def archive_channel(self, channel, message):
        """ Archive a channel, and send alert to slack admins. """
        api_endpoint = 'conversations.archive'

        self.logger.info('Archiving...')

        if not self.settings.get('dry_run'):
            channel_message = message.format(self.settings.get('days_inactive'))
            self.send_channel_message(channel['id'], channel_message)
            payload = {'channel': channel['id']}
            self.slack_api_http(api_endpoint=api_endpoint, payload=payload)

    def join_channel(self, channel_name, channel_id):
        """ Joins a channel so that the bot has access to message and archive. """
        if not self.settings.get('dry_run'):
          join_api_endpoint='conversations.join'
          join_payload = {'channel': channel_id}
          self.slack_api_http(api_endpoint=join_api_endpoint, payload=join_payload)
        else:
          self.logger.info(
            'THIS IS A DRY RUN. BOT would have joined %s.' % channel_name)

    def send_admin_report(self, channels):
        """ Optionally this will message admins with which channels were archived. """
        if self.settings.get('admin_channel') and len(channels) > 0:
            channel_names = ', '.join('#' + channel['name'] for channel in channels)
            admin_msg = 'Archiving %d channels: %s' % (len(channels), channel_names)
            if self.settings.get('dry_run'):
                admin_msg = '[DRY RUN] %s' % admin_msg
            self.send_channel_message(self.settings.get('admin_channel'), admin_msg)

    def main(self):
        """
        This is the main method that checks all inactive channels and archives them.
        """
        if self.settings.get('dry_run'):
            self.logger.info(
                'THIS IS A DRY RUN. NO CHANNELS ARE ACTUALLY ARCHIVED.')

        whitelist_keywords = self.get_whitelist_keywords()
        message_templates = self.get_channel_messages()
        archived_channels = []

        # Add bot to all channels
        self.logger.info('Ensuring bot has joined all channels...')

        for channel in self.get_all_channels():
            if not  channel['is_member']:
                self.join_channel(channel['name'], channel['id'])

        # Only able to archive channels that the bot is a member of
        self.logger.info('Checking channel activities...')

        for channel in self.get_all_channels():
            if channel['is_member']:
              channel_whitelisted = self.is_channel_whitelisted(channel, whitelist_keywords)
              channel_unused = self.is_channel_unused(channel, self.settings.get('too_old_datetime'))

              if (not channel_whitelisted and channel_unused):
                  archived_channels.append(channel)
                  self.archive_channel(channel, message_templates['channel_template'])

        self.send_admin_report(archived_channels)

if __name__ == '__main__':
    CHANNEL_REAPER = ChannelReaper()
    CHANNEL_REAPER.main()
