"""JARVIS Twitter sync.""" import asyncio from datetime import datetime, timedelta from logging import Logger import tweepy from dis_snek import Snake from dis_snek.models.discord.embed import EmbedAttachment from jarvis_core.db import q from jarvis_core.db.models import TwitterAccount, TwitterFollow from jarvis_core.util import build_embed from jarvis_tasks.config import TaskConfig config = TaskConfig.from_yaml() async def twitter(bot: Snake, logger: Logger) -> None: """ Sync tweets in the background. Args: bot: Snake instance logger: Global logger """ auth = tweepy.AppAuthHandler(config.twitter["consumer_key"], config.twitter["consumer_secret"]) api = tweepy.API(auth) while True: accounts = TwitterAccount.find() accounts_to_delete = [] # Go through all actively followed accounts async for account in accounts: logger.debug(f"Checking account {account.handle}") # Check if account needs updated (handle changes) if account.last_sync + timedelta(hours=1) <= datetime.utcnow(): logger.debug(f"Account {account.handle} out of sync, updating") user = api.get_user(user_id=account.twitter_id) account.update(q(handle=user.screen_name, last_sync=datetime.utcnow())) # Get new tweets if tweets := api.user_timeline(user_id=account.twitter_id, since_id=account.last_tweet): logger.debug(f"{account.handle} has new tweets") tweets = sorted(tweets, key=lambda x: x.id) follows = TwitterFollow.find(q(twitter_id=account.twitter_id)) follows_to_delete = [] num_follows = 0 # Go through follows and send tweet if necessary async for follow in follows: num_follows += 1 guild = await bot.fetch_guild(follow.guild) if not guild: logger.warning(f"Follow {follow.id}'s guild no longer exists, deleting") follows_to_delete.append(follow) continue channel = await bot.fetch_channel(follow.channel) if not channel: logger.warning(f"Follow {follow.id}'s channel no longer exists, deleting") follows_to_delete.append(follow) continue for tweet in tweets: retweet = "retweeted_status" in tweet.__dict__ if retweet and not follow.retweets: continue timestamp = int(tweet.created_at.timestamp()) url = f"https://twitter.com/{account.handle}/status/{tweet.id}" mod = "re" if retweet else "" media = tweet.entities.get("media", None) photo = None if media and media[0]["type"] in ["photo", "animated_gif"]: photo = EmbedAttachment(url=media[0]["media_url_https"]) embed = build_embed( title="", description=(tweet.text + f"\n\n[View this tweet]({url})"), fields=[], color="#1DA1F2", image=photo, ) embed.set_author( name=account.handle, url=url, icon_url=tweet.author.profile_image_url_https, ) embed.set_footer( text="Twitter", icon_url="https://abs.twimg.com/icons/apple-touch-icon-192x192.png", ) await channel.send( f"`@{account.handle}` {mod}tweeted this at " ) # Delete invalid follows for follow in follows_to_delete: await follow.delete() if num_follows == 0: accounts_to_delete.append(account) else: newest = tweets[-1] account.update(q(last_tweet=newest.id)) await account.commit() # Delete invalid accounts (no follows) for account in accounts_to_delete: logger.info(f"Account {account.handle} has no followers, removing") await account.delete() # Only check once a minute await asyncio.sleep(60)