#!/usr/bin/env python3

# Twitter integration for Zulip

import os
import sys
import argparse
from configparser import ConfigParser, NoSectionError, NoOptionError

import zulip
VERSION = "0.9"
CONFIGFILE = os.path.expanduser("~/.zulip_twitterrc")
INSTRUCTIONS = r"""
twitter-bot --config-file=~/.zuliprc --search="@nprnews,quantum physics"

Send Twitter tweets to a Zulip stream.

Depends on: https://github.com/bear/python-twitter version 3.1

To use this script:

0. Use `pip install python-twitter` to install `python-twitter`
1. Set up Twitter authentication, as described below
2. Set up a Zulip bot user and download its `.zuliprc` config file to e.g. `~/.zuliprc`
3. Subscribe the bot to the stream that will receive Twitter updates (default stream: twitter)
4. Test the script by running it manually, like this:

twitter-bot --config-file=<path/to/.zuliprc> --search="<search-query>"
or
twitter-bot --config-file=<path/to/.zuliprc> --twitter-name="<your-twitter-handle>"

- optional - Exclude any terms or users by using the flags `--exluded-terms` or `--excluded-users`:

twitter-bot --config-file=<path/to/.zuliprc> --search="<search-query>" --excluded-users="test-username,other-username"
or
twitter-bot --config-file=<path/to/.zuliprc> --twitter-name="<your-twitter-handle>" --excluded-terms="test-term,other-term"

5. Configure a crontab entry for this script. A sample crontab entry
that will process tweets every 5 minutes is:

*/5 * * * * /usr/local/share/zulip/integrations/twitter/twitter-bot [options]

== Setting up Twitter authentications ==

Run this on a personal or trusted machine, because your API key is
visible to local users through the command line or config file.

This bot uses OAuth to authenticate with Twitter. Please create a
~/.zulip_twitterrc with the following contents:

[twitter]
consumer_key =
consumer_secret =
access_token_key =
access_token_secret =

In order to obtain a consumer key & secret, you must register a
new application under your Twitter account:

1. Go to http://dev.twitter.com
2. Log in
3. In the menu under your username, click My Applications
4. Create a new application

Make sure to go the application you created and click "create my
access token" as well. Fill in the values displayed.
"""

def write_config(config: ConfigParser, configfile_path: str) -> None:
    with open(configfile_path, 'w') as configfile:
        config.write(configfile)

parser = zulip.add_default_arguments(argparse.ArgumentParser("Fetch tweets from Twitter."))
parser.add_argument('--instructions',
                    action='store_true',
                    help='Show instructions for the twitter bot setup and exit'
                    )
parser.add_argument('--limit-tweets',
                    default=15,
                    type=int,
                    help='Maximum number of tweets to send at once')
parser.add_argument('--search',
                    dest='search_terms',
                    help='Terms to search on',
                    action='store')
parser.add_argument('--stream',
                    dest='stream',
                    help='The stream to which to send tweets',
                    default="twitter",
                    action='store')
parser.add_argument('--twitter-name',
                    dest='twitter_name',
                    help='Twitter username to poll new tweets from"')
parser.add_argument('--excluded-terms',
                    dest='excluded_terms',
                    help='Terms to exclude tweets on')
parser.add_argument('--excluded-users',
                    dest='excluded_users',
                    help='Users to exclude tweets on')

opts = parser.parse_args()

if opts.instructions:
    print(INSTRUCTIONS)
    sys.exit()

if all([opts.search_terms, opts.twitter_name]):
    parser.error('You must only specify either a search term or a username.')
if opts.search_terms:
    client_type = 'ZulipTwitterSearch/'
    CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitterrc_fetchsearch")
elif opts.twitter_name:
    client_type = 'ZulipTwitter/'
    CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitteruserrc_fetchuser")
else:
    parser.error('You must either specify a search term or a username.')

try:
    config = ConfigParser()
    config.read(CONFIGFILE)
    config_internal = ConfigParser()
    config_internal.read(CONFIGFILE_INTERNAL)

    consumer_key = config.get('twitter', 'consumer_key')
    consumer_secret = config.get('twitter', 'consumer_secret')
    access_token_key = config.get('twitter', 'access_token_key')
    access_token_secret = config.get('twitter', 'access_token_secret')
except (NoSectionError, NoOptionError):
    parser.error("Please provide a ~/.zulip_twitterrc")

if not all([consumer_key, consumer_secret, access_token_key, access_token_secret]):
    parser.error("Please provide a ~/.zulip_twitterrc")

try:
    since_id = config_internal.getint('twitter', 'since_id')
except (NoOptionError, NoSectionError):
    since_id = 0
try:
    previous_twitter_name = config_internal.get('twitter', 'twitter_name')
except (NoOptionError, NoSectionError):
    previous_twitter_name = ''
try:
    previous_search_terms = config_internal.get('twitter', 'search_terms')
except (NoOptionError, NoSectionError):
    previous_search_terms = ''

try:
    import twitter
except ImportError:
    parser.error("Please install python-twitter")

api = twitter.Api(consumer_key=consumer_key,
                  consumer_secret=consumer_secret,
                  access_token_key=access_token_key,
                  access_token_secret=access_token_secret)

user = api.VerifyCredentials()

if not user.id:
    print("Unable to log in to twitter with supplied credentials. Please double-check and try again")
    sys.exit(1)

client = zulip.init_from_options(opts, client=client_type+VERSION)

if opts.search_terms:
    search_query = " OR ".join(opts.search_terms.split(","))
    if since_id == 0 or opts.search_terms != previous_search_terms:
        # No since id yet, fetch the latest and then start monitoring from next time
        # Or, a different user id is being asked for, so start from scratch
        # Either way, fetch last 5 tweets to start off
        statuses = api.GetSearch(search_query, count=5)
    else:
        # We have a saved last id, so insert all newer tweets into the zulip stream
        statuses = api.GetSearch(search_query, since_id=since_id)
elif opts.twitter_name:
    if since_id == 0 or opts.twitter_name != previous_twitter_name:
        # Same strategy as for search_terms
        statuses = api.GetUserTimeline(screen_name=opts.twitter_name, count=5)
    else:
        statuses = api.GetUserTimeline(screen_name=opts.twitter_name, since_id=since_id)

if opts.excluded_terms:
    excluded_terms = opts.excluded_terms.split(",")
else:
    excluded_terms = []

if opts.excluded_users:
    excluded_users = opts.excluded_users.split(",")
else:
    excluded_users = []

for status in statuses[::-1][:opts.limit_tweets]:
    # Check if the tweet is from an excluded user
    exclude = False
    for user in excluded_users:
        if user == status.user.screen_name:
            exclude = True
            break
    if exclude:
        continue  # Continue with the loop for the next tweet

    # https://twitter.com/eatevilpenguins/status/309995853408530432
    composed = "%s (%s)" % (status.user.name, status.user.screen_name)
    url = "https://twitter.com/%s/status/%s" % (status.user.screen_name, status.id)
    # This contains all strings that could have caused the tweet to match our query.
    text_to_check = [status.text, status.user.screen_name]
    text_to_check.extend(url.expanded_url for url in status.urls)

    text_to_check = [text.lower() for text in text_to_check]

    # Check that the tweet doesn't contain any terms that
    # are supposed to be excluded
    for term in excluded_terms:
        if any(term.lower() in text for text in text_to_check):
            exclude = True  # Tweet should be excluded
            break
    if exclude:
        continue  # Continue with the loop for the next tweet

    if opts.search_terms:
        search_term_used = None
        for term in opts.search_terms.split(","):
            # Remove quotes from phrase:
            #   "Zulip API" -> Zulip API
            if term.startswith('"') and term.endswith('"'):
                term = term[1:-1]
            if any(term.lower() in text for text in text_to_check):
                search_term_used = term
                break
        # For some reason (perhaps encodings or message tranformations we
        # didn't anticipate), we don't know what term was used, so use a
        # default.
        if not search_term_used:
            search_term_used = "mentions"
        subject = search_term_used
    elif opts.twitter_name:
        subject = composed

    message = {
        "type": "stream",
        "to": [opts.stream],
        "subject": subject,
        "content": url
    }

    ret = client.send_message(message)

    if ret['result'] == 'error':
        # If sending failed (e.g. no such stream), abort and retry next time
        print("Error sending message to zulip: %s" % ret['msg'])
        break
    else:
        since_id = status.id

if 'twitter' not in config_internal.sections():
    config_internal.add_section('twitter')
config_internal.set('twitter', 'since_id', str(since_id))
config_internal.set('twitter', 'search_terms', str(opts.search_terms))
config_internal.set('twitter', 'twitter_name', str(opts.twitter_name))

write_config(config_internal, CONFIGFILE_INTERNAL)