feat: Add manual trigger bypass for web UI autonomous engagement
- Added manual_trigger parameter to /autonomous/engage endpoint to bypass 12h cooldown - Updated miku_engage_random_user_for_server() and miku_engage_random_user() to accept manual_trigger flag - Modified Web UI to always send manual_trigger=true when engaging users from the UI - Users can now manually engage the same user multiple times from web UI without cooldown restriction - Regular autonomous schedules still respect the 12h cooldown between engagements to the same user Changes: - bot/api.py: Added manual_trigger parameter with string-to-boolean conversion - bot/static/index.html: Added manual_trigger=true to engage user request - bot/utils/autonomous_v1_legacy.py: Added manual_trigger parameter and cooldown bypass logic
This commit is contained in:
@@ -4,6 +4,7 @@ import random
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
import discord
|
||||
from discord import Status
|
||||
@@ -27,6 +28,107 @@ from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('autonomous')
|
||||
|
||||
|
||||
async def fetch_tweet_by_url(tweet_url: str):
|
||||
"""Fetch a specific tweet by its URL using twscrape.
|
||||
|
||||
Args:
|
||||
tweet_url: URL of the tweet to fetch (x.com, twitter.com, or fxtwitter.com)
|
||||
|
||||
Returns:
|
||||
Dictionary with tweet data or None if fetch fails
|
||||
"""
|
||||
try:
|
||||
# Extract tweet ID from URL
|
||||
# Handle various URL formats:
|
||||
# https://twitter.com/username/status/1234567890
|
||||
# https://x.com/username/status/1234567890
|
||||
# https://fxtwitter.com/username/status/1234567890
|
||||
match = re.search(r'/status/(\d+)', tweet_url)
|
||||
if not match:
|
||||
logger.error(f"Could not extract tweet ID from URL: {tweet_url}")
|
||||
return None
|
||||
|
||||
tweet_id = int(match.group(1))
|
||||
|
||||
from twscrape import API
|
||||
|
||||
# Load cookies from JSON file
|
||||
from pathlib import Path
|
||||
COOKIE_PATH = Path(__file__).parent / "x.com.cookies.json"
|
||||
|
||||
if not COOKIE_PATH.exists():
|
||||
logger.error(f"Cookie file not found: {COOKIE_PATH}")
|
||||
return None
|
||||
|
||||
import json
|
||||
with open(COOKIE_PATH, "r", encoding="utf-8") as f:
|
||||
cookie_list = json.load(f)
|
||||
cookie_header = "; ".join(f"{c['name']}={c['value']}" for c in cookie_list)
|
||||
|
||||
api = API()
|
||||
await api.pool.add_account(
|
||||
username="HSankyuu39",
|
||||
password="x",
|
||||
email="x",
|
||||
email_password="x",
|
||||
cookies=cookie_header
|
||||
)
|
||||
await api.pool.login_all()
|
||||
|
||||
# Fetch the specific tweet using search (same approach as figurine_notifier.py)
|
||||
from twscrape import gather
|
||||
logger.debug(f"Searching for tweet with ID {tweet_id}")
|
||||
search_results = await gather(api.search(f"{tweet_id}", limit=1))
|
||||
logger.debug(f"Search returned {len(search_results)} results")
|
||||
|
||||
# Check if we found the tweet
|
||||
tweet = None
|
||||
for search_tweet in search_results:
|
||||
if str(search_tweet.id) == str(tweet_id):
|
||||
tweet = search_tweet
|
||||
logger.debug(f"Found matching tweet with ID {tweet.id}")
|
||||
break
|
||||
|
||||
if not tweet and search_results:
|
||||
# If no exact match but we have results, use the first one
|
||||
tweet = search_results[0]
|
||||
logger.debug(f"Using first search result with ID {tweet.id}")
|
||||
|
||||
if not tweet:
|
||||
logger.error(f"Failed to fetch tweet ID {tweet_id}")
|
||||
return None
|
||||
|
||||
# Extract media URLs if present
|
||||
media_urls = []
|
||||
if hasattr(tweet, 'media') and tweet.media:
|
||||
if hasattr(tweet.media, 'photos'):
|
||||
for photo in tweet.media.photos:
|
||||
if hasattr(photo, 'url'):
|
||||
media_url = photo.url
|
||||
if '?' in media_url:
|
||||
media_url = media_url.split('?')[0]
|
||||
media_url += '?name=large'
|
||||
media_urls.append(media_url)
|
||||
|
||||
# Extract username and build URL
|
||||
username = tweet.user.username
|
||||
tweet_url = f"https://twitter.com/{username}/status/{tweet.id}"
|
||||
|
||||
result = {
|
||||
"username": username,
|
||||
"text": tweet.rawContent if hasattr(tweet, 'rawContent') else "",
|
||||
"url": tweet_url,
|
||||
"media": media_urls if media_urls else []
|
||||
}
|
||||
|
||||
logger.info(f"Successfully fetched tweet {tweet_id} from @{username}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching tweet by URL {tweet_url}: {e}")
|
||||
return None
|
||||
|
||||
# Server-specific memory storage
|
||||
_server_autonomous_messages = {} # guild_id -> rotating buffer of last general messages
|
||||
_server_user_engagements = {} # guild_id -> user_id -> timestamp
|
||||
@@ -138,13 +240,14 @@ async def miku_say_something_general_for_server(guild_id: int):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send autonomous message: {e}")
|
||||
|
||||
async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None, engagement_type: str = None):
|
||||
async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None, engagement_type: str = None, manual_trigger: bool = False):
|
||||
"""Miku engages a random user in a specific server
|
||||
|
||||
Args:
|
||||
guild_id: The server ID
|
||||
user_id: Optional specific user ID to engage (as string). If None, picks random user
|
||||
engagement_type: Optional engagement style - 'activity', 'general', 'status', or None for auto-detect
|
||||
manual_trigger: If True, bypass cooldown checks (for web UI manual triggers)
|
||||
"""
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
@@ -198,10 +301,15 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
|
||||
|
||||
now = time.time()
|
||||
last_time = _server_user_engagements[guild_id].get(target.id, 0)
|
||||
if now - last_time < 43200: # 12 hours in seconds
|
||||
|
||||
# Skip cooldown check if this is a manual trigger from web UI
|
||||
if not manual_trigger and now - last_time < 43200: # 12 hours in seconds
|
||||
logger.info(f"Recently engaged {target.display_name} in server {guild_id}, switching to general message.")
|
||||
await miku_say_something_general_for_server(guild_id)
|
||||
return
|
||||
|
||||
if manual_trigger:
|
||||
logger.info(f"Manual trigger - bypassing cooldown for {target.display_name} in server {guild_id}")
|
||||
|
||||
activity_name = None
|
||||
if target.activities:
|
||||
@@ -393,26 +501,40 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to interject in conversation: {e}")
|
||||
|
||||
async def share_miku_tweet_for_server(guild_id: int):
|
||||
"""Share a Miku tweet in a specific server"""
|
||||
async def share_miku_tweet_for_server(guild_id: int, tweet_url: str = None):
|
||||
"""Share a Miku tweet in a specific server
|
||||
|
||||
Args:
|
||||
guild_id: The server ID to share the tweet to
|
||||
tweet_url: Optional URL of a specific tweet to share. If None, fetches a random tweet.
|
||||
"""
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return
|
||||
|
||||
channel = globals.client.get_channel(server_config.autonomous_channel_id)
|
||||
tweets = await fetch_miku_tweets(limit=5)
|
||||
if not tweets:
|
||||
logger.warning(f"No good tweets found for server {guild_id}")
|
||||
return
|
||||
|
||||
# If a specific tweet URL is provided, fetch that tweet
|
||||
if tweet_url:
|
||||
tweet = await fetch_tweet_by_url(tweet_url)
|
||||
if not tweet:
|
||||
logger.error(f"Failed to fetch tweet from URL: {tweet_url}")
|
||||
return
|
||||
else:
|
||||
# Fetch random tweets as usual
|
||||
tweets = await fetch_miku_tweets(limit=5)
|
||||
if not tweets:
|
||||
logger.warning(f"No good tweets found for server {guild_id}")
|
||||
return
|
||||
|
||||
fresh_tweets = [t for t in tweets if t["url"] not in LAST_SENT_TWEETS]
|
||||
fresh_tweets = [t for t in tweets if t["url"] not in LAST_SENT_TWEETS]
|
||||
|
||||
if not fresh_tweets:
|
||||
logger.warning(f"All fetched tweets were recently sent in server {guild_id}. Reusing tweets.")
|
||||
fresh_tweets = tweets
|
||||
if not fresh_tweets:
|
||||
logger.warning(f"All fetched tweets were recently sent in server {guild_id}. Reusing tweets.")
|
||||
fresh_tweets = tweets
|
||||
|
||||
tweet = random.choice(fresh_tweets)
|
||||
tweet = random.choice(fresh_tweets)
|
||||
|
||||
LAST_SENT_TWEETS.append(tweet["url"])
|
||||
if len(LAST_SENT_TWEETS) > 50:
|
||||
@@ -506,15 +628,16 @@ async def miku_say_something_general():
|
||||
for guild_id in server_manager.servers:
|
||||
await miku_say_something_general_for_server(guild_id)
|
||||
|
||||
async def miku_engage_random_user(user_id: str = None, engagement_type: str = None):
|
||||
async def miku_engage_random_user(user_id: str = None, engagement_type: str = None, manual_trigger: bool = False):
|
||||
"""Legacy function - now runs for all servers
|
||||
|
||||
Args:
|
||||
user_id: Optional specific user ID to engage
|
||||
engagement_type: Optional engagement style
|
||||
manual_trigger: If True, bypass cooldown checks (for web UI manual triggers)
|
||||
"""
|
||||
for guild_id in server_manager.servers:
|
||||
await miku_engage_random_user_for_server(guild_id, user_id=user_id, engagement_type=engagement_type)
|
||||
await miku_engage_random_user_for_server(guild_id, user_id=user_id, engagement_type=engagement_type, manual_trigger=manual_trigger)
|
||||
|
||||
async def miku_detect_and_join_conversation(force: bool = False):
|
||||
"""Legacy function - now runs for all servers
|
||||
@@ -525,10 +648,14 @@ async def miku_detect_and_join_conversation(force: bool = False):
|
||||
for guild_id in server_manager.servers:
|
||||
await miku_detect_and_join_conversation_for_server(guild_id, force=force)
|
||||
|
||||
async def share_miku_tweet():
|
||||
"""Legacy function - now runs for all servers"""
|
||||
async def share_miku_tweet(tweet_url: str = None):
|
||||
"""Legacy function - now runs for all servers
|
||||
|
||||
Args:
|
||||
tweet_url: Optional URL of a specific tweet to share. If None, fetches a random tweet.
|
||||
"""
|
||||
for guild_id in server_manager.servers:
|
||||
await share_miku_tweet_for_server(guild_id)
|
||||
await share_miku_tweet_for_server(guild_id, tweet_url=tweet_url)
|
||||
|
||||
async def handle_custom_prompt(user_prompt: str):
|
||||
"""Legacy function - now runs for all servers"""
|
||||
|
||||
Reference in New Issue
Block a user