├── .gitignore ├── requirements.txt ├── config.yaml ├── LICENSE ├── README.md └── whatsupbot.py /.gitignore: -------------------------------------------------------------------------------- 1 | bots.yaml -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | TwitterAPI >= 2.5.9 2 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apps: 3 | sample_app_name: 4 | consumer_key: a1b2c3 5 | consumer_secret: d4e5f6 6 | another_app: 7 | consumer_key: e7f8g9 8 | consumer_secret: h0i1j2 9 | 10 | users: 11 | sample_bot_name: 12 | key: 123-abc 13 | secret: xyz 14 | app: sample_app_name 15 | # check that this bot has tweeted in the last 10 hours 16 | whatsupbot: 17 | hours: 10 18 | 19 | super_secret_bot: 20 | key: 999-mno 21 | secret: uvw999 22 | app: sample_app_name 23 | # whats up bot will ignore this bot 24 | whatsupbot: false 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | __ _| |__ __ _| |_( )___ _ _ _ __ | |__ ___ | |_ 2 | \ \ /\ / / '_ \ / _` | __|// __| | | | | '_ \ | '_ \ / _ \| __| 3 | \ V V /| | | | (_| | |_ \__ \ | |_| | |_) | | |_) | (_) | |_ 4 | \_/\_/ |_| |_|\__,_|\__| |___/ \__,_| .__/ |_.__/ \___/ \__| 5 | |_| 6 | 7 | Copyright (c) 2016-9 Neil Freeman 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of 10 | this software and associated documentation files (the "Software"), to deal in 11 | the Software without restriction, including without limitation the rights to 12 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 13 | of the Software, and to permit persons to whom the Software is furnished to do 14 | so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What's up, bot? 2 | 3 | Check if your Twitter bots are running, get a DM if they aren't. 4 | 5 | ## For example 6 | 7 | Send a DM to @yourname if @examplebot hasn't tweeted in 13 hours. 8 | ```` 9 | python whatsupbot.py --screen_name examplebot --hours 13 --to yourname 10 | ```` 11 | 12 | The DM will come from @examplebot, and it will say something like "I'm not working. It's been 14 hours since my last tweet. Fix me!". If everything is running fine, nothing will happen! 13 | 14 | Oh yeah, in real life you're going to have to pass in your authentication tokens! Run `python whatsupbot.py --help` for details. 15 | 16 | ## Installing 17 | 18 | At a minimum **What's up bot** requires [tweepy](https://github.com/tweepy/tweepy). Some additional features are unlocked if you install [twitter_bot_utils](https://github.com/fitnr/twitter_bot_utils), which is just a handy wrapper around `tweepy`. 19 | 20 | To install twitter_bot_utils, download the repo and run: 21 | ``` 22 | pip -r requirements.txt 23 | ``` 24 | 25 | Then copy `whatsupbot.py` to somewhere handy. 26 | 27 | Install with a cron job, which might look something like this: 28 | ``` 29 | 5 * * * * python path/to/whatsupbot.py --screen_name botname --to yourname --consumer-key [etc] 30 | ``` 31 | 32 | ## Using a third party account 33 | 34 | Wait, you ask - what if my bot has been suspended, then it can't send me DMs! That's a good point. If you would like another account to do the DM sending, use the `--from` option: 35 | ``` 36 | python whatsupbot.py --screen_name botname --from thirdparty --to yourname --consumer-key ... --consumer-secret ... --key ... --secret ... 37 | ``` 38 | 39 | ## Checking lots of bots 40 | 41 | To check lots of bots at once, create a config file, following the format in `config.yaml` (the file can also be JSON, if you prefer). This also gives you a handy place to put your authentication tokens. An example `config.yaml` file is in the repository (json works too). 42 | 43 | Use the `whatsupbot` key to either ignore bots or customize the hours limit on each bot. 44 | 45 | Then run: 46 | ``` 47 | python whatsupbot.py --to yourname --config config.yaml 48 | ``` 49 | 50 | The `--from` flag also works with this set-up. If the "from" account is in the bots.yaml file, its authentication keys will be used. 51 | 52 | ## Acknowledgments 53 | 54 | Thanks to [mattlaschneider](https://github.com/mattlaschneider) for the [urbotbroke](https://twitter.com/urbotbroke) code, and to all the fine friendly folks at #botALLY for being fine friendly folks. 55 | -------------------------------------------------------------------------------- /whatsupbot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import logging 4 | import sys 5 | from os import path 6 | import json 7 | import argparse 8 | from datetime import datetime 9 | from email.utils import parsedate 10 | import yaml 11 | from TwitterAPI import TwitterAPI 12 | 13 | 14 | log = logging.getLogger('whatsupbot') 15 | log.setLevel(logging.INFO) 16 | ch = logging.StreamHandler(sys.stderr) 17 | ch.setFormatter(logging.Formatter('%(filename)-11s %(lineno)-3d: %(message)s')) 18 | log.addHandler(ch) 19 | 20 | 21 | def parse(file_path): 22 | '''Parse a YAML or JSON file.''' 23 | _, ext = path.splitext(file_path) 24 | if ext in ('.yaml', '.yml'): 25 | func = yaml.load 26 | elif ext == '.json': 27 | func = json.load 28 | else: 29 | raise ValueError("Unrecognized config file type %s" % ext) 30 | with open(file_path, 'r') as f: 31 | return func(f) 32 | 33 | 34 | def last_tweet(api, screen_name): 35 | '''Hours since last tweet. Returns float/int.''' 36 | try: 37 | resp = api.request('statuses/user_timeline', {'screen_name': screen_name, 'count': '1'}) 38 | created_at = parsedate(resp.json()[0]['created_at']) 39 | dt = datetime(*created_at[:6]) 40 | elasped = (datetime.utcnow() - dt).total_seconds() / 3600. 41 | logging.getLogger('whatsupbot').debug('@%s elapsed %s', screen_name, elasped) 42 | return elasped 43 | 44 | except (TypeError, IndexError) as e: 45 | logging.getLogger('whatsupbot').error('error fetching @%s: %s', screen_name, e) 46 | return -1 47 | 48 | 49 | def compose(screen_name, elapsed, hours, sender=None, confirm=False): 50 | message = '' 51 | 52 | if elapsed == -1: 53 | if sender == screen_name: 54 | message = "My timeline isn't showing up in the Twitter API. Can you check on me?" 55 | else: 56 | message = "@{}'s timeline isn't showing up in the Twitter API.".format(screen_name) 57 | 58 | elif elapsed > hours: 59 | if sender == screen_name: 60 | message = "It's been more than {} hours since my last tweet. Fix me!".format(int(elapsed)) 61 | else: 62 | message = 'No tweets from @{} in more than {} hours'.format(screen_name, int(elapsed)) 63 | 64 | elif confirm: 65 | if sender == screen_name: 66 | message = "It's been {} hours since my last tweet".format(int(elapsed)) 67 | else: 68 | message = '@{} last tweeted {} hours ago'.format(screen_name, int(elapsed)) 69 | 70 | return message 71 | 72 | 73 | def main(): 74 | parser = argparse.ArgumentParser() 75 | parser.add_argument('--screen_name', default=None, required=False, help='screen name to check') 76 | parser.add_argument('--from', dest='sender', default=None, 77 | help='account that will send DM notifications') 78 | parser.add_argument('--hours', type=int, default=24, 79 | help="Gaps of this many hours are a problem (default: 24)") 80 | parser.add_argument('--to', dest='recipient', metavar='USER', type=str, default=None, 81 | help='user to notify when screen_name is down. If omitted, prints to stdout') 82 | parser.add_argument('--confirm', action='store_true', 83 | help='Always send message with the time of the most recent tweet') 84 | parser.add_argument('-c', '--config', dest='config_file', metavar='PATH', default=None, type=str, 85 | help='bots config file (json or yaml). By default, all accounts in the file will be checked.') 86 | parser.add_argument('-v', '--verbose', action='store_true') 87 | parser.add_argument('--key', type=str, help='access token') 88 | parser.add_argument('--secret', type=str, help='access token secret') 89 | parser.add_argument('--consumer-key', metavar='KEY', type=str, help='consumer key (aka consumer token)') 90 | parser.add_argument('--consumer-secret', metavar='SECRET', type=str, help='consumer secret') 91 | 92 | args = parser.parse_args() 93 | logger = logging.getLogger('whatsupbot') 94 | 95 | if args.verbose: 96 | logger.setLevel(logging.DEBUG) 97 | 98 | if args.key and args.secret and args.consumer_secret and args.consumer_key: 99 | api = TwitterAPI(args.consumer_key, args.consumer_secret, args.key, args.secret) 100 | else: 101 | api = None 102 | 103 | if getattr(args, 'config_file'): 104 | conf = parse(args.config_file) 105 | if not api: 106 | try: 107 | sender = conf['users'][args.sender] 108 | if 'app' in sender: 109 | app = conf['apps'][sender['app']] 110 | elif 'consumer_key' in sender and 'consumer_secret' in sender: 111 | app = sender 112 | 113 | api = TwitterAPI(app['consumer_key'], app['consumer_secret'], sender['key'], sender['secret']) 114 | 115 | except: 116 | pass 117 | 118 | if not api: 119 | logger.error("unable to set up api") 120 | exit(1) 121 | 122 | users = conf.get('users', {args.screen_name: {}}) 123 | messages = [ 124 | compose(u, last_tweet(api, u), attrs.get('whatsupbot', {}).get('hours', args.hours), args.sender, args.confirm) 125 | for u, attrs in users.items() if attrs.get('whatsupbot', True) 126 | ] 127 | message = '\n'.join(m for m in messages if m) 128 | 129 | if args.recipient and message: 130 | recipient = api.request('users/show', {'screen_name': args.recipient}) 131 | id_str = recipient.json().get('id_str') 132 | payload = { 133 | "event": { 134 | "type": "message_create", 135 | "message_create": { 136 | "target": {"recipient_id": id_str}, "message_data": {"text": message} 137 | } 138 | } 139 | } 140 | resp = api.request('direct_messages/events/new', json.dumps(payload)) 141 | logger.debug('status code %s', resp.status_code) 142 | if resp.status_code != 200: 143 | logger.error(resp.text) 144 | 145 | elif message: 146 | print(message) 147 | 148 | 149 | if __name__ == '__main__': 150 | main() 151 | --------------------------------------------------------------------------------