112 lines
4.8 KiB
Python
112 lines
4.8 KiB
Python
"""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 <t:{timestamp}:f>"
|
|
)
|
|
|
|
# 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)
|