wrap up catch-up mode, add tweets nuker
This commit is contained in:
+21
-8
@@ -62,10 +62,12 @@ def get_finished_user_timestamps(queue_file):
|
|||||||
results = dict()
|
results = dict()
|
||||||
for line in queue_file:
|
for line in queue_file:
|
||||||
tokens = line.split()
|
tokens = line.split()
|
||||||
if len(tokens) != 3 or tokens[0][0] != '#':
|
if len(tokens) == 0: continue
|
||||||
|
|
||||||
|
if tokens[0][0] != '#':
|
||||||
|
print(f'{line} is our stopper!')
|
||||||
# reached end of accounts list
|
# reached end of accounts list
|
||||||
break
|
break
|
||||||
|
|
||||||
if tokens[2] != '-1':
|
if tokens[2] != '-1':
|
||||||
results[int(tokens[1])] = float(tokens[2])
|
results[int(tokens[1])] = float(tokens[2])
|
||||||
return results
|
return results
|
||||||
@@ -86,11 +88,13 @@ def get_user_timestamps_str(queue_file):
|
|||||||
async def get_cross_talent_tweets(queue_path):
|
async def get_cross_talent_tweets(queue_path):
|
||||||
finished_user_timestamps = dict()
|
finished_user_timestamps = dict()
|
||||||
ttweets_dict = dict()
|
ttweets_dict = dict()
|
||||||
|
posted_ttweets = set() # TODO: don't add TTweet to ttweets_dict if its id exists in posted_ttweets
|
||||||
|
|
||||||
# Populate structures with existing data from queue.txt
|
# Populate structures with existing data from queue.txt
|
||||||
try:
|
try:
|
||||||
with open(queue_path, 'r') as f:
|
with open(queue_path, 'r') as f:
|
||||||
finished_user_timestamps = get_finished_user_timestamps(f)
|
finished_user_timestamps = get_finished_user_timestamps(f)
|
||||||
|
print(finished_user_timestamps)
|
||||||
|
|
||||||
# Get existing queued TalentTweets
|
# Get existing queued TalentTweets
|
||||||
for line in f:
|
for line in f:
|
||||||
@@ -141,20 +145,23 @@ async def get_cross_talent_tweets(queue_path):
|
|||||||
|
|
||||||
return ttweets_dict
|
return ttweets_dict
|
||||||
|
|
||||||
async def process_queue(ttweets_dict: dict):
|
async def process_queue(ttweets_dict: dict) -> int:
|
||||||
global PROGRAM_ARGS
|
global PROGRAM_ARGS
|
||||||
|
ttweets_posted = 0
|
||||||
|
|
||||||
if len(ttweets_dict) == 0: return
|
if len(ttweets_dict) == 0: return ttweets_posted
|
||||||
|
|
||||||
if PROGRAM_ARGS.announce_catchup:
|
if PROGRAM_ARGS.announce_catchup:
|
||||||
TwAPI.instance.post_tweet(text=f'Starting to catch-up through {len(ttweets_dict)} logged tweets.')
|
TwAPI.instance.post_tweet(text=f'Starting to catch up through {len(ttweets_dict)} logged tweets.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while len(ttweets_dict) > 0:
|
while len(ttweets_dict) > 0:
|
||||||
key = list(ttweets_dict.keys())[0]
|
key = list(ttweets_dict.keys())[0]
|
||||||
ttweet = ttweets_dict[key]
|
ttweet = ttweets_dict[key]
|
||||||
await TwAPI.instance.post_ttweet(ttweet)
|
if await TwAPI.instance.post_ttweet(ttweet, is_catchup=True):
|
||||||
|
ttweets_posted += 1
|
||||||
ttweets_dict.pop(key)
|
ttweets_dict.pop(key)
|
||||||
|
# TODO: add ttweet.tweet_id to some success list
|
||||||
except:
|
except:
|
||||||
print('Unhandled error occurred while posting tweets from queue.')
|
print('Unhandled error occurred while posting tweets from queue.')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -170,10 +177,16 @@ async def process_queue(ttweets_dict: dict):
|
|||||||
for ttweet in ttweets_dict.values():
|
for ttweet in ttweets_dict.values():
|
||||||
f.write(f'{ttweet.serialize()}\n')
|
f.write(f'{ttweet.serialize()}\n')
|
||||||
|
|
||||||
|
return ttweets_posted
|
||||||
|
|
||||||
async def run(program_args):
|
async def run(program_args):
|
||||||
global PROGRAM_ARGS
|
global PROGRAM_ARGS
|
||||||
PROGRAM_ARGS = program_args
|
PROGRAM_ARGS = program_args
|
||||||
queue_path = get_queue_path()
|
queue_path = get_queue_path()
|
||||||
|
while True:
|
||||||
ttweets_dict = await get_cross_talent_tweets(queue_path)
|
ttweets_dict = await get_cross_talent_tweets(queue_path)
|
||||||
print(f'got {len(ttweets_dict)} tweets')
|
print(f'found {len(ttweets_dict)} cross-company tweets')
|
||||||
await process_queue(ttweets_dict)
|
if await process_queue(ttweets_dict) == 0:
|
||||||
|
print('Posted no new tweets; we\'re caught up!')
|
||||||
|
break
|
||||||
|
# TODO: go to listen mode
|
||||||
+17
-3
@@ -2,6 +2,7 @@ import sys
|
|||||||
import asyncio
|
import asyncio
|
||||||
import argparse
|
import argparse
|
||||||
from argparse import RawTextHelpFormatter
|
from argparse import RawTextHelpFormatter
|
||||||
|
import code
|
||||||
|
|
||||||
import nest_asyncio
|
import nest_asyncio
|
||||||
|
|
||||||
@@ -15,7 +16,8 @@ PROGRAM_ARGS = None
|
|||||||
|
|
||||||
MODES_HELP_STR = '''mode to run the bot at:
|
MODES_HELP_STR = '''mode to run the bot at:
|
||||||
l,listen: listen for new tweets from all accounts; will not terminate unless error occurs
|
l,listen: listen for new tweets from all accounts; will not terminate unless error occurs
|
||||||
c,catchup: scan all tweets from all accounts; will terminate when done'''
|
c,catchup: scan all tweets from all accounts; will terminate when done
|
||||||
|
d,delete-all: delete all tweets on account provided by secrets.ini; make sure the function is uncommented in twapi.py'''
|
||||||
|
|
||||||
def init_argparse():
|
def init_argparse():
|
||||||
p = argparse.ArgumentParser(description='Twitter bot that follows interactions between Nijisanji EN/ID and hololive EN/ID members.', formatter_class=RawTextHelpFormatter)
|
p = argparse.ArgumentParser(description='Twitter bot that follows interactions between Nijisanji EN/ID and hololive EN/ID members.', formatter_class=RawTextHelpFormatter)
|
||||||
@@ -23,12 +25,21 @@ def init_argparse():
|
|||||||
help=MODES_HELP_STR)
|
help=MODES_HELP_STR)
|
||||||
p.add_argument('--show-tokens', action='store_true', help='[DO NOT USE IN PUBLIC SETTING] print stored tokens from secrets.ini')
|
p.add_argument('--show-tokens', action='store_true', help='[DO NOT USE IN PUBLIC SETTING] print stored tokens from secrets.ini')
|
||||||
p.add_argument('--announce-catchup', action='store_true', help='In catch-up mode, post a tweet announcing catch-up mode.')
|
p.add_argument('--announce-catchup', action='store_true', help='In catch-up mode, post a tweet announcing catch-up mode.')
|
||||||
|
p.add_argument('--no-delay', action='store_true', help='In self-destruct mode, clear tweets without safety waiting.')
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def command_line():
|
def command_line():
|
||||||
# TODO: implement command line mode for manually controlling the bot
|
# TODO: implement command line mode for manually controlling the bot
|
||||||
|
print('Shell coming soon. For now, here\'s a Python interpretor.')
|
||||||
|
code.interact(local=globals())
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def self_destruct():
|
||||||
|
if not PROGRAM_ARGS.no_delay:
|
||||||
|
print('\033[31;6m-----DELETING ALL TWEETS IN 10 SECONDS!! PRESS CTRL+C TO CANCEL.-----\033[0m')
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
await TwAPI.instance.nuke_tweets()
|
||||||
|
|
||||||
async def async_main():
|
async def async_main():
|
||||||
global PROGRAM_ARGS
|
global PROGRAM_ARGS
|
||||||
|
|
||||||
@@ -40,9 +51,12 @@ async def async_main():
|
|||||||
case 'c' | 'catchup':
|
case 'c' | 'catchup':
|
||||||
print('RUNNING IN CATCH-UP MODE\n')
|
print('RUNNING IN CATCH-UP MODE\n')
|
||||||
await catchup.run(PROGRAM_ARGS)
|
await catchup.run(PROGRAM_ARGS)
|
||||||
case _:
|
case 'd' | 'delete-all':
|
||||||
|
print('WARNING: SELF-DESTRUCT MODE')
|
||||||
|
await self_destruct()
|
||||||
|
case 'cmd':
|
||||||
command_line()
|
command_line()
|
||||||
#TODO: remove message
|
case _:
|
||||||
print('\ninvalid mode. run with no arguments or "-h" for help page, including mode list.')
|
print('\ninvalid mode. run with no arguments or "-h" for help page, including mode list.')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
+1
-3
@@ -36,7 +36,5 @@ def init():
|
|||||||
# nijiexID
|
# nijiexID
|
||||||
__create_dict(f'{util.get_project_dir()}/lists/nijiexid.txt', niji_exid)
|
__create_dict(f'{util.get_project_dir()}/lists/nijiexid.txt', niji_exid)
|
||||||
|
|
||||||
test_talents = {
|
test_talents = holo_en
|
||||||
1390637197167038464: 'PomuRainpuff'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import pytz
|
|||||||
|
|
||||||
from twapi import *
|
from twapi import *
|
||||||
import talent_lists
|
import talent_lists
|
||||||
|
import util
|
||||||
|
|
||||||
class TalentTweet:
|
class TalentTweet:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
+106
-11
@@ -1,8 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
|
import traceback
|
||||||
|
|
||||||
import tweepy
|
import tweepy
|
||||||
from tweetcapture import TweetCapture
|
import twint
|
||||||
|
|
||||||
import api_secrets
|
import api_secrets
|
||||||
import talenttweet as tt
|
import talenttweet as tt
|
||||||
@@ -67,6 +68,72 @@ class TwAPI:
|
|||||||
access_token=api_secrets.access_token(), access_token_secret=api_secrets.access_secret()
|
access_token=api_secrets.access_token(), access_token_secret=api_secrets.access_secret()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.me = self.client.get_me().data
|
||||||
|
print(f'Assuming the account of @{self.me.data["username"]} ({self.me["id"]})')
|
||||||
|
|
||||||
|
## ---[COMMENT OUT WHEN NOT IN USE]---
|
||||||
|
async def nuke_tweets(self):
|
||||||
|
async def delete_tweet(id):
|
||||||
|
try:
|
||||||
|
self.client.delete_tweet(id)
|
||||||
|
except tweepy.TooManyRequests as e:
|
||||||
|
wait_for = float(e.response.headers["x-rate-limit-reset"]) - datetime.datetime.now().timestamp() + 1
|
||||||
|
print(f'\thit rate limit deleting {id}, retrying in {wait_for} seconds...')
|
||||||
|
await asyncio.sleep(wait_for)
|
||||||
|
print('continuing...')
|
||||||
|
await delete_tweet(id)
|
||||||
|
|
||||||
|
print(f'Retrieving all of {self.me["username"]}\'s tweets...')
|
||||||
|
tweets = self.get_all_tweet_ids_from_user(self.me['id'])
|
||||||
|
|
||||||
|
print(f'Retrieved {len(tweets)} tweets.')
|
||||||
|
if not len(tweets) > 0:
|
||||||
|
print('No tweets obtained. Make sure the profile is public.')
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f'Deleting {len(tweets)} tweets...')
|
||||||
|
deleted_count = 0
|
||||||
|
try:
|
||||||
|
for tweet in tweets:
|
||||||
|
print(f'deleted {deleted_count}/{len(tweets)}')
|
||||||
|
await delete_tweet(tweet.id)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
deleted_count += 1
|
||||||
|
except:
|
||||||
|
print('Unhandled error occurred while trying to delete tweets.')
|
||||||
|
traceback.print_exc()
|
||||||
|
print('Try running again.')
|
||||||
|
else:
|
||||||
|
print('Saul Gone')
|
||||||
|
|
||||||
|
def get_all_tweet_ids_from_user(self, user_id):
|
||||||
|
next_page_token = None
|
||||||
|
tokens_retrieved = 0
|
||||||
|
tweets_retrieved = 0
|
||||||
|
tweets = list()
|
||||||
|
while True:
|
||||||
|
print(f'Retrieved {tokens_retrieved} tokens so far...')
|
||||||
|
resp = self.client.get_users_tweets(
|
||||||
|
user_id, max_results=100, pagination_token=next_page_token,
|
||||||
|
media_fields=TwAPI.TWEET_MEDIA_FIELDS,
|
||||||
|
tweet_fields=TwAPI.TWEET_FIELDS,
|
||||||
|
expansions=TwAPI.TWEET_EXPANSIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
for tweet in resp.data:
|
||||||
|
tweets.append(tweet)
|
||||||
|
|
||||||
|
# update counters and pagination token
|
||||||
|
tweets_retrieved += resp.meta['result_count']
|
||||||
|
try:
|
||||||
|
next_page_token = resp.meta['next_token']
|
||||||
|
tokens_retrieved += 1
|
||||||
|
except KeyError:
|
||||||
|
print("next_token wasn't provided; we've reached the end!")
|
||||||
|
break # reached end of user's tweets
|
||||||
|
|
||||||
|
print(f'Retrieved {tweets_retrieved} tweets using {tokens_retrieved} tokens.')
|
||||||
|
return tweets
|
||||||
|
|
||||||
async def get_tweet_response(self, id, attempt = 0):
|
async def get_tweet_response(self, id, attempt = 0):
|
||||||
try:
|
try:
|
||||||
@@ -84,7 +151,7 @@ class TwAPI:
|
|||||||
await asyncio.sleep(wait_for)
|
await asyncio.sleep(wait_for)
|
||||||
return await self.get_tweet_response(id, attempt=attempt+1)
|
return await self.get_tweet_response(id, attempt=attempt+1)
|
||||||
|
|
||||||
async def post_tweet(self, text, media_id=None, reply_to_tweet: int=None):
|
async def post_tweet(self, text='', media_id=None, reply_to_tweet: int=None):
|
||||||
try:
|
try:
|
||||||
tweet = self.client.create_tweet(text=text, media_ids=None if media_id == None else [media_id], in_reply_to_tweet_id=reply_to_tweet)
|
tweet = self.client.create_tweet(text=text, media_ids=None if media_id == None else [media_id], in_reply_to_tweet_id=reply_to_tweet)
|
||||||
return tweet
|
return tweet
|
||||||
@@ -92,42 +159,70 @@ class TwAPI:
|
|||||||
wait_for = float(e.response.headers["x-rate-limit-reset"]) - datetime.datetime.now().timestamp() + 1
|
wait_for = float(e.response.headers["x-rate-limit-reset"]) - datetime.datetime.now().timestamp() + 1
|
||||||
print(f'\thit rate limit -- attempting to create Tweet again in {wait_for} seconds...')
|
print(f'\thit rate limit -- attempting to create Tweet again in {wait_for} seconds...')
|
||||||
await asyncio.sleep(wait_for)
|
await asyncio.sleep(wait_for)
|
||||||
return await self.post_tweet(text=text, media_ids=[media_id])
|
return await self.post_tweet(text=text, media_id=media_id, reply_to_tweet=reply_to_tweet)
|
||||||
|
|
||||||
async def get_ttweet_image_media_id(self, ttweet):
|
async def get_ttweet_image_media_id(self, ttweet):
|
||||||
img = await util.create_ttweet_image(ttweet)
|
img = await util.create_ttweet_image(ttweet)
|
||||||
media = self.api.media_upload(img)
|
media = self.api.media_upload(img)
|
||||||
return media.media_id
|
return media.media_id
|
||||||
|
|
||||||
async def post_ttweet(self, ttweet: tt.TalentTweet):
|
async def post_ttweet(self, ttweet: tt.TalentTweet, is_catchup=False):
|
||||||
|
print(f'------{ttweet.tweet_id} ({util.get_username_local(ttweet.author_id)})------')
|
||||||
|
|
||||||
REPLY = '{0} replied to {1}!\n'
|
REPLY = '{0} replied to {1}!\n'
|
||||||
MENTION = '{0} mentioned {1}!\n'
|
|
||||||
QUOTE_TWEET = '{0} quote tweeted {1}!\n'
|
QUOTE_TWEET = '{0} quote tweeted {1}!\n'
|
||||||
|
MENTION = '{0} tweeted with '
|
||||||
|
|
||||||
def create_text():
|
def create_text():
|
||||||
if ttweet.reply_to is not None:
|
|
||||||
author_username = f'@/{util.get_username_online(ttweet.author_id)}'
|
author_username = f'@/{util.get_username_online(ttweet.author_id)}'
|
||||||
|
ret = str()
|
||||||
|
if is_catchup:
|
||||||
|
# ret += '[catch-up tweet]\n'
|
||||||
|
pass
|
||||||
|
ret += f'{ttweet.get_datetime_str()}\n'
|
||||||
|
if ttweet.reply_to is not None: # reply (w/ qrt; push it into mentions)
|
||||||
reply_username = f'@/{util.get_username_online(ttweet.reply_to)}'
|
reply_username = f'@/{util.get_username_online(ttweet.reply_to)}'
|
||||||
|
ret += REPLY.format(author_username, reply_username)
|
||||||
|
|
||||||
mention_ids = set(ttweet.mentions)
|
mention_ids = set(ttweet.mentions)
|
||||||
mention_ids.add(ttweet.quote_retweeted)
|
mention_ids.add(ttweet.quote_retweeted)
|
||||||
try: mention_ids.remove(None)
|
try: mention_ids.remove(None)
|
||||||
except: pass
|
except: pass
|
||||||
mention_usernames = [f'@/{util.get_username_online(x)}' for x in mention_ids]
|
elif ttweet.quote_retweeted is not None: # standalone qrt
|
||||||
|
quoted_username = f'@/{util.get_username_online(ttweet.quote_retweeted)}'
|
||||||
|
ret += QUOTE_TWEET.format(author_username, quoted_username)
|
||||||
|
elif len(ttweet.mentions) > 0: # standalone tweet w/ mentions
|
||||||
|
ret += MENTION.format(author_username)
|
||||||
|
else:
|
||||||
|
raise ValueError(f'TalentTweet {ttweet.tweet_id} has insufficient other parties')
|
||||||
|
|
||||||
ret = REPLY.format(author_username, reply_username)
|
# mention line
|
||||||
|
if len(mention_ids) > 0:
|
||||||
|
mention_usernames = [f'@/{util.get_username_online(x)}' for x in mention_ids]
|
||||||
ret += (
|
ret += (
|
||||||
'mentions '
|
'mentions '
|
||||||
f'{" ".join(mention_usernames)}'
|
f'{" ".join(mention_usernames)}\n'
|
||||||
f'\n{util.ttweet_to_url(ttweet)}'
|
|
||||||
)
|
)
|
||||||
|
ret += f'\n{util.ttweet_to_url(ttweet)}'
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
img_media_id_task = asyncio.create_task(self.get_ttweet_image_media_id(ttweet))
|
img_media_id_task = asyncio.create_task(self.get_ttweet_image_media_id(ttweet))
|
||||||
text = create_text()
|
text = create_text()
|
||||||
media_id = await img_media_id_task
|
media_id = await img_media_id_task
|
||||||
|
try:
|
||||||
|
print('posting main tweet')
|
||||||
twt_resp = await self.post_tweet(text)
|
twt_resp = await self.post_tweet(text)
|
||||||
twt_id = twt_resp.data['id']
|
twt_id = twt_resp.data['id']
|
||||||
await self.post_tweet(text='Image backup', reply_to_tweet=twt_id, media_id=media_id,)
|
print('posting reply tweet')
|
||||||
|
await self.post_tweet(reply_to_tweet=twt_id, media_id=media_id,)
|
||||||
|
print('successfully posted ttweet!')
|
||||||
|
return True
|
||||||
|
except tweepy.Forbidden as e:
|
||||||
|
if 'duplicate content' in e.api_messages[0]:
|
||||||
|
print('Twitter says the TalentTweet is a duplicate; skipping error-free...')
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ def get_key_from_value(d, val):
|
|||||||
|
|
||||||
async def create_ttweet_image(ttweet):
|
async def create_ttweet_image(ttweet):
|
||||||
tc = TweetCapture()
|
tc = TweetCapture()
|
||||||
filename = 'img.png'
|
filename = f'{get_project_dir()}/img.png'
|
||||||
url = ttweet_to_url(ttweet)
|
url = ttweet_to_url(ttweet)
|
||||||
img = None
|
img = None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user