+2
-1
@@ -144,4 +144,5 @@ cython_debug/
|
|||||||
|
|
||||||
# project-specific
|
# project-specific
|
||||||
run/
|
run/
|
||||||
*.json
|
*.tw_session
|
||||||
|
.venv*
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
+41
-12
@@ -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
|
||||||
|
|
||||||
@@ -46,10 +73,11 @@ async def async_main():
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -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
@@ -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"
|
||||||
|
|||||||
+4
-4
@@ -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)})------"
|
||||||
)
|
)
|
||||||
@@ -146,10 +148,8 @@ 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)
|
||||||
|
|||||||
+13
-11
@@ -1,24 +1,26 @@
|
|||||||
from tweety.types import *
|
from tweety.types import *
|
||||||
|
|
||||||
def url(t: Tweet):
|
|
||||||
return f'https://twitter.com/{t.author.username}/status/{t.id}'
|
|
||||||
|
|
||||||
def print_tweets(tweets: list[Tweet | TweetThread]):
|
def url(t: Tweet):
|
||||||
print(f'{len(tweets)} tweets:')
|
return f"https://twitter.com/{t.author.username}/status/{t.id}"
|
||||||
|
|
||||||
|
|
||||||
|
def print_tweets(tweets: list[Tweet | SelfThread]):
|
||||||
|
print(f"{len(tweets)} tweets:")
|
||||||
for t in tweets:
|
for t in tweets:
|
||||||
if isinstance(t, Tweet):
|
if isinstance(t, Tweet):
|
||||||
print(f'{t.date} : {url(t)} :', end=' ')
|
print(f"{t.date} : {url(t)} :", end=" ")
|
||||||
|
|
||||||
if t.is_retweet:
|
if t.is_retweet:
|
||||||
print(f'RT ({t.retweeted_tweet.author.username})', end=' ')
|
print(f"RT ({t.retweeted_tweet.author.username})", end=" ")
|
||||||
|
|
||||||
if t.is_reply:
|
if t.is_reply:
|
||||||
print(f'is reply!', end=' ')
|
print(f"is reply!", end=" ")
|
||||||
if t.replied_to is not None:
|
if t.replied_to is not None:
|
||||||
print(f'reply to {t.replied_to.author.username}', end=' ')
|
print(f"reply to {t.replied_to.author.username}", end=" ")
|
||||||
|
|
||||||
print("m=" + ",".join([x.username for x in t.user_mentions]))
|
print("m=" + ",".join([x.username for x in t.user_mentions]))
|
||||||
elif isinstance(t, TweetThread):
|
elif isinstance(t, SelfThread):
|
||||||
print('-----------TTd----------')
|
print("-----------TTd----------")
|
||||||
print_tweets(t.tweets)
|
print_tweets(t.tweets)
|
||||||
print('-----------end----------')
|
print("-----------end----------")
|
||||||
|
|||||||
+1
-1
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user