Generated by com2ann (slightly patched to avoid also converting assignment type annotations, which require Python 3.6), followed by some manual whitespace adjustment, and two fixes for use-before-define issues: - def set_zulip_client(self, zulipToJabberClient: ZulipToJabberBot) -> None: + def set_zulip_client(self, zulipToJabberClient: 'ZulipToJabberBot') -> None: -def init_from_options(options: Any, client: Optional[str] = None) -> Client: +def init_from_options(options: Any, client: Optional[str] = None) -> 'Client': Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
		
			
				
	
	
		
			261 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			261 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
#!/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)
 |