Merge pull request #8 from muskit/cleanup

Cleanup
This commit is contained in:
muskit
2024-03-08 15:34:26 -08:00
committed by GitHub
13 changed files with 111 additions and 65 deletions
+2 -1
View File
@@ -144,4 +144,5 @@ cython_debug/
# project-specific # project-specific
run/ run/
*.json *.tw_session
.venv*
+13 -6
View File
@@ -6,13 +6,20 @@ Twitter bot that tracks cross-company interactions between the non-JP branches o
**This project was created to run [this account](https://twitter.com/NijiHolo_EN_ID).** **This project was created to run [this account](https://twitter.com/NijiHolo_EN_ID).**
## Running ## Running
Install dependencies. With the way packages are setup, **you must have Docker installed and running!!**
```
pip install -r requirements.txt
```
Setup the `.env` in the project root. Refer to the `.env` section for variables.
Run the program from project root (not in `src`). Refer to the following section for options. Setup the `.env` in the project root. Refer to the [`.env`](#env) section for variables.
Build and run the Docker container:
```bash
# to run attached (can CTRL+P,CTRL+Q to detach)
sh run.sh
# ... or to run headless
sh run_detached.sh
```
If attached to a container prepared by Dockerfile, you can run the program from project root (not in `src`). Refer to the following section for options.
``` ```
python src/main.py python src/main.py
``` ```
+5 -5
View File
@@ -26,11 +26,11 @@
1465858739970273281 luca_kaneshiro 1465858739970273281 luca_kaneshiro
# --- [Noctyx] --- # --- [Noctyx] ---
1490867613915828224 alban_knox 1490867613915828224 alban_knox
1491195742123397124 uki_violeta 1491195742123397124 uki_violeta
1492604168145539072 Yugo_Asuma p 1492604168145539072 Yugo_Asuma p
1493392149664219138 Fulgur_Ovid 1493392149664219138 Fulgur_Ovid
1493394108014292993 sonny_brisko 1493394108014292993 sonny_brisko
# --- [ILUNA] --- # --- [ILUNA] ---
1545351225293426688 MariaMari0nette 1545351225293426688 MariaMari0nette
+1 -1
View File
@@ -1,6 +1,6 @@
python-dotenv python-dotenv
nest-asyncio nest-asyncio
pytz pytz
git+https://github.com/muskit/tweety.git git+https://github.com/mahrtayyab/tweety.git@e3d330280cb3b2e8f9d2bf2f20425c476f7671a5
tweepy tweepy
tweet-capture tweet-capture
+7
View File
@@ -0,0 +1,7 @@
python-dotenv
nest-asyncio
pytz
git+https://github.com/mahrtayyab/tweety.git@e3d330280cb3b2e8f9d2bf2f20425c476f7671a5
tweepy
tweet-capture
opencv-python-headless
+2 -2
View File
@@ -59,7 +59,7 @@ async def get_cross_tweets_online():
print(f"Queue has {queue.get_count()} tweets so far") print(f"Queue has {queue.get_count()} tweets so far")
except KeyboardInterrupt as e: except KeyboardInterrupt as e:
print( print(
"Interrupting tweet pulling... NOTE: remaining dates in queue file will not be updated!" "Interrupting tweet pulling. The remaining dates in queue file will not be updated!"
) )
queue.save_file() queue.save_file()
raise e raise e
@@ -144,7 +144,7 @@ async def run(PROGRAM_ARGS):
print(f"Invalid tweet {id}!") print(f"Invalid tweet {id}!")
continue continue
posted = await TwAPI.instance.post_ttweet_by_id(i) posted = await TwAPI.instance.post_ttweet_by_id(i, PROGRAM_ARGS.dry_run)
if posted: if posted:
queue.add_finished_tweet(i) queue.add_finished_tweet(i)
print("Successfully posted tweet. Sleeping for 5 minutes") print("Successfully posted tweet. Sleeping for 5 minutes")
+4 -4
View File
@@ -11,9 +11,9 @@ def run(PROGRAM_ARGS):
while True: while True:
try: try:
asyncio.run(catchup.run(PROGRAM_ARGS)) asyncio.run(catchup.run(PROGRAM_ARGS))
print('Sleeping for 60 minutes...') print("Sleeping for 60 minutes...")
sleep(60*60) # run every hour sleep(60 * 30) # run every half-hour
except KeyboardInterrupt: except KeyboardInterrupt:
print('Interrupt signal received. Exiting listen mode.') print("Interrupt signal received. Exiting listen mode.")
print(f'errors encountered throughout session.') print(f"errors encountered throughout session.")
break break
+43 -14
View File
@@ -14,27 +14,54 @@ from twapi import TwAPI
PROGRAM_ARGS = None PROGRAM_ARGS = None
MODES_HELP_STR = '''mode to run the bot at: MODES_HELP_STR = """mode to run the bot at:
<blank> scrape accounts in lists and post cross-company tweets if relevant <blank> scrape accounts in lists and post cross-company tweets if relevant
cmd drop into Python interpretor with access to initialized variables''' cmd drop into Python interpretor with access to initialized variables"""
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(
p.add_argument('mode', nargs='?', help=MODES_HELP_STR) description="Twitter bot that follows interactions between Nijisanji EN/ID and hololive EN/ID members.",
p.add_argument('--no-listen', action='store_true', help='Run one scraping-posting cycle without waiting to run again.') formatter_class=RawTextHelpFormatter,
p.add_argument('--refresh-queue', action='store_true', help='Refresh the details on each tweet currently in queue.') )
p.add_argument('--straight-to-queue', action='store_true', help='Go through queue first before attempting to pull tweets.') p.add_argument("mode", nargs="?", help=MODES_HELP_STR)
p.add_argument('--post-id', action='append', help='ID of a tweet to try and post right away. Specify multiple to post multiple tweets in a row.') p.add_argument(
"--no-listen",
action="store_true",
help="Run one scraping-posting cycle without waiting to run again.",
)
p.add_argument(
"--refresh-queue",
action="store_true",
help="Refresh the details on each tweet currently in queue.",
)
p.add_argument(
"--straight-to-queue",
action="store_true",
help="Go through queue first before attempting to pull tweets.",
)
p.add_argument(
"--dry-run",
action="store_true",
help="Don't actually post anything to Twitter; use to check outputs from console.",
)
p.add_argument(
"--post-id",
action="append",
help="ID of a tweet to try and post right away. Specify multiple to post multiple tweets in a row.",
)
return p return p
def command_line(): def command_line():
# TODO (extra): implement command line mode for manually controlling the bot # TODO (extra): implement command line mode for manually controlling the bot
print('Here\'s a Python interpretor.') print("Here's a Python interpreter.")
try: try:
code.interact(local=globals()) code.interact(local=globals())
except SystemExit: except SystemExit:
pass pass
async def async_main(): async def async_main():
global PROGRAM_ARGS global PROGRAM_ARGS
@@ -44,12 +71,13 @@ async def async_main():
else: else:
listen.run(PROGRAM_ARGS) listen.run(PROGRAM_ARGS)
return return
mode = PROGRAM_ARGS.mode.lower() mode = PROGRAM_ARGS.mode.lower()
if mode == 'cmd': if mode == "cmd":
command_line() command_line()
else: else:
print('\nunknown mode. run with no arguments or -h for help and modes') print("\nunknown mode. run with no arguments or -h for help and modes")
def init_data(): def init_data():
# Initialize shared API instance # Initialize shared API instance
@@ -60,12 +88,13 @@ def init_data():
if PROGRAM_ARGS.mode: if PROGRAM_ARGS.mode:
mode = PROGRAM_ARGS.mode.lower() mode = PROGRAM_ARGS.mode.lower()
if mode != 'cmd': if mode != "cmd":
# Initialize queue files system # Initialize queue files system
ttq.TalentTweetQueue() ttq.TalentTweetQueue()
else: else:
ttq.TalentTweetQueue() ttq.TalentTweetQueue()
def main(): def main():
global PROGRAM_ARGS global PROGRAM_ARGS
@@ -81,7 +110,7 @@ def main():
## Asynchronous execution ## Asynchronous execution
nest_asyncio.apply() nest_asyncio.apply()
asyncio.run(async_main()) asyncio.run(async_main())
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+4 -4
View File
@@ -19,7 +19,7 @@ class Scraper:
def __init__(self): def __init__(self):
Scraper.instance = self Scraper.instance = self
self.__account = AccountPool() self.__account = AccountPool()
self.try_login() self.try_login(0)
def try_login(self, account_idx: int = None) -> bool: def try_login(self, account_idx: int = None) -> bool:
# decide on which account to use # decide on which account to use
@@ -81,7 +81,7 @@ class Scraper:
if tweet.is_reply and tweet.replied_to is None: if tweet.is_reply and tweet.replied_to is None:
# print(f'{tweet.author.username}/{tweet.id} is missing reply-to tweet! Recovering...') # print(f'{tweet.author.username}/{tweet.id} is missing reply-to tweet! Recovering...')
tweet.replied_to = self.get_tweet( tweet.replied_to = self.get_tweet(
tweet.original_tweet["in_reply_to_status_id_str"] tweet._original_tweet["in_reply_to_status_id_str"]
) )
return tweet return tweet
@@ -159,7 +159,7 @@ class Scraper:
search = self.app.search( search = self.app.search(
f"from:{username}", filter_=SearchFilters.Latest(), cursor=cur f"from:{username}", filter_=SearchFilters.Latest(), cursor=cur
) )
cur_page = search.tweets cur_page = search.results
print(f"obtained {len(cur_page)} tweets") print(f"obtained {len(cur_page)} tweets")
if len(cur_page) == 0: if len(cur_page) == 0:
@@ -168,7 +168,7 @@ class Scraper:
for e in cur_page: for e in cur_page:
if isinstance(e, Tweet): if isinstance(e, Tweet):
add_tweet(e) add_tweet(e)
elif isinstance(e, TweetThread): elif isinstance(e, SelfThread):
# FIXME: rework when replied_to is fixed (currently populates user_mentions) # FIXME: rework when replied_to is fixed (currently populates user_mentions)
# latest tweet in thread = og author's reply # latest tweet in thread = og author's reply
for t in e: for t in e:
+5 -5
View File
@@ -85,7 +85,7 @@ class TalentTweet:
rt_mentions=rtm, rt_mentions=rtm,
) )
## Creates a TalentTweet from a Tweety-library Tweet. ## Creates a TalentTweet from a Tweety Tweet.
@staticmethod @staticmethod
def create_from_tweety(tweety: Tweet): def create_from_tweety(tweety: Tweet):
if tweety.is_retweet: if tweety.is_retweet:
@@ -104,7 +104,7 @@ class TalentTweet:
text=tweety.text, text=tweety.text,
mrq=( mrq=(
{int(x.id) for x in tweety.user_mentions}, {int(x.id) for x in tweety.user_mentions},
int(tweety.original_tweet["in_reply_to_user_id_str"]) int(tweety._original_tweet["in_reply_to_user_id_str"])
if tweety.is_reply if tweety.is_reply
else None, else None,
int(tweety.quoted_tweet.author.id) int(tweety.quoted_tweet.author.id)
@@ -278,7 +278,7 @@ class TalentTweet:
rt_username = ( rt_username = (
util.get_username_with_company(self.rt_author_id) util.get_username_with_company(self.rt_author_id)
if self.rt_author_id != -1 if self.rt_author_id != -1
else None else "someone"
) )
if rt_username == author_username: if rt_username == author_username:
rt_username = "themselves" rt_username = "themselves"
@@ -291,7 +291,7 @@ class TalentTweet:
reply_username = ( reply_username = (
util.get_username_with_company(self.reply_to) util.get_username_with_company(self.reply_to)
if self.reply_to != -1 if self.reply_to != -1
else None else "someone"
) )
if reply_username == author_username: if reply_username == author_username:
reply_username = "themselves" reply_username = "themselves"
@@ -303,7 +303,7 @@ class TalentTweet:
quoted_username = ( quoted_username = (
util.get_username_with_company(self.quote_tweeted) util.get_username_with_company(self.quote_tweeted)
if self.quote_tweeted != -1 if self.quote_tweeted != -1
else None else "someone"
) )
if quoted_username == author_username: if quoted_username == author_username:
quoted_username = "themselves" quoted_username = "themselves"
+5 -5
View File
@@ -136,6 +136,8 @@ class TwAPI:
# return True = successfully posted a single ttweet # return True = successfully posted a single ttweet
# return False = did not post ttweet (duplicate) # return False = did not post ttweet (duplicate)
async def post_ttweet(self, ttweet: tt.TalentTweet, dry_run=False): async def post_ttweet(self, ttweet: tt.TalentTweet, dry_run=False):
import main
print( print(
f"------{ttweet.tweet_id} ({util.get_username_local(ttweet.author_id)})------" f"------{ttweet.tweet_id} ({util.get_username_local(ttweet.author_id)})------"
) )
@@ -145,11 +147,9 @@ class TwAPI:
if dry_run: if dry_run:
print("-------------------- DRY RUN --------------------") print("-------------------- DRY RUN --------------------")
print(ttweet) print(ttweet)
if dry_run:
return False return False
# NO DRY-RUN: actually post tweet
# main tweet: text + screenshot # main tweet: text + screenshot
try: try:
print("creating main QRT w/ screenshot...") print("creating main QRT w/ screenshot...")
@@ -188,7 +188,7 @@ class TwAPI:
raise e raise e
return True return True
async def post_ttweet_by_id(self, id: int): async def post_ttweet_by_id(self, id: int, dry_run=False):
from scraper import Scraper from scraper import Scraper
print(f"Manually posting tweet {id}") print(f"Manually posting tweet {id}")
@@ -204,4 +204,4 @@ class TwAPI:
return False return False
print(f"Posting {ttweet.username}/{ttweet.tweet_id}...") print(f"Posting {ttweet.username}/{ttweet.tweet_id}...")
return await self.post_ttweet(ttweet) return await self.post_ttweet(ttweet, dry_run)
+19 -17
View File
@@ -1,24 +1,26 @@
from tweety.types import * from tweety.types import *
def url(t: Tweet): def url(t: Tweet):
return f'https://twitter.com/{t.author.username}/status/{t.id}' return f"https://twitter.com/{t.author.username}/status/{t.id}"
def print_tweets(tweets: list[Tweet | TweetThread]):
print(f'{len(tweets)} tweets:')
for t in tweets:
if isinstance(t, Tweet):
print(f'{t.date} : {url(t)} :', end=' ')
if t.is_retweet: def print_tweets(tweets: list[Tweet | SelfThread]):
print(f'RT ({t.retweeted_tweet.author.username})', end=' ') print(f"{len(tweets)} tweets:")
for t in tweets:
if isinstance(t, Tweet):
print(f"{t.date} : {url(t)} :", end=" ")
if t.is_reply: if t.is_retweet:
print(f'is reply!', end=' ') print(f"RT ({t.retweeted_tweet.author.username})", end=" ")
if t.replied_to is not None:
print(f'reply to {t.replied_to.author.username}', end=' ')
print("m=" + ",".join([x.username for x in t.user_mentions])) if t.is_reply:
elif isinstance(t, TweetThread): print(f"is reply!", end=" ")
print('-----------TTd----------') if t.replied_to is not None:
print_tweets(t.tweets) print(f"reply to {t.replied_to.author.username}", end=" ")
print('-----------end----------')
print("m=" + ",".join([x.username for x in t.user_mentions]))
elif isinstance(t, SelfThread):
print("-----------TTd----------")
print_tweets(t.tweets)
print("-----------end----------")
+1 -1
View File
@@ -26,7 +26,7 @@ def project_root(dir_path: tuple[str] = tuple(), file: str = None):
def working_path(dir_path: tuple[str] = tuple(), file: str = None): def working_path(dir_path: tuple[str] = tuple(), file: str = None):
"""Returns file path relative to the working ephemeral directory.""" """Returns file path relative to the working ephemeral directory "run"."""
dir_path = project_root(("run", *dir_path)) dir_path = project_root(("run", *dir_path))
Path(dir_path).mkdir(parents=True, exist_ok=True) Path(dir_path).mkdir(parents=True, exist_ok=True)