2025-10-07 22:10:52 -04:00
# The API used in the tweety command is made by me and can be found here:
# https://github.com/neoarz/tweety-api
# I made this out of spite since i couldnt find any free APIs for this
# Its serverless and hosted on Vercel :)
2025-10-07 13:33:53 -04:00
import os
import tempfile
import discord
from discord . ext import commands
import aiohttp
from datetime import datetime
from typing import Optional
2025-10-07 22:10:52 -04:00
import pytz
2025-10-08 21:55:13 -04:00
import time
import asyncio
2025-10-07 13:33:53 -04:00
2025-10-08 00:43:21 -04:00
def break_long_words ( text : str , max_word_length : int = 50 ) - > str :
2025-11-02 23:32:52 -05:00
words = text . split ( " " )
2025-10-08 00:43:21 -04:00
result = [ ]
2025-11-02 23:32:52 -05:00
2025-10-08 00:43:21 -04:00
for word in words :
if len ( word ) > max_word_length :
2025-11-02 23:32:52 -05:00
chunks = [
word [ i : i + max_word_length ]
for i in range ( 0 , len ( word ) , max_word_length )
]
result . append ( " " . join ( chunks ) )
2025-10-08 00:43:21 -04:00
else :
result . append ( word )
2025-11-02 23:32:52 -05:00
return " " . join ( result )
2025-10-08 00:43:21 -04:00
2025-10-07 22:02:37 -04:00
class TweetyHelpView ( discord . ui . View ) :
2025-10-07 22:10:52 -04:00
""" This is the help view for the slash command cuz only the pinging and prefix versions can use this command """
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
def __init__ ( self , user_id : int , bot ) :
super ( ) . __init__ ( timeout = 180 )
self . user_id = user_id
self . bot = bot
self . current_page = 0
self . pages = [
{
" title " : " Tweety (Method 1) " ,
" description " : " Use the prefix command `.media tweety` while replying to a message. " ,
2025-11-02 23:32:52 -05:00
" gif_url " : " https://yes.nighty.works/raw/VrKX1L.gif " ,
2025-10-07 22:02:37 -04:00
" fields " : [
2025-11-02 23:32:52 -05:00
{
" name " : " How to use " ,
" value " : " 1. Reply to any message \n 2. Type `.media tweety` \n 3. Use the buttons to customize! " ,
" inline " : False ,
} ,
] ,
2025-10-07 22:02:37 -04:00
} ,
{
" title " : " Tweety (Method 2) " ,
" description " : f " Mention <@ { bot . user . id } > with `tweety` while replying to a message. " ,
2025-11-02 23:32:52 -05:00
" gif_url " : " https://yes.nighty.works/raw/9XEe9j.gif " ,
2025-10-07 22:02:37 -04:00
" fields " : [
2025-11-02 23:32:52 -05:00
{
" name " : " How to use " ,
" value " : f " 1. Reply to any message \n 2. Type `<@ { bot . user . id } > tweety` \n 3. Use the buttons to customize! " ,
" inline " : False ,
} ,
] ,
} ,
2025-10-07 22:02:37 -04:00
]
self . update_buttons ( )
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
def create_embed ( self ) :
page = self . pages [ self . current_page ]
embed = discord . Embed (
title = page [ " title " ] ,
description = page [ " description " ] ,
color = 0x7289DA ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " , icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp "
)
2025-10-07 22:02:37 -04:00
for field in page [ " fields " ] :
2025-11-02 23:32:52 -05:00
embed . add_field (
name = field [ " name " ] , value = field [ " value " ] , inline = field [ " inline " ]
)
2025-10-07 22:02:37 -04:00
embed . set_image ( url = page [ " gif_url " ] )
embed . set_footer ( text = f " Page { self . current_page + 1 } / { len ( self . pages ) } " )
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
return embed
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
def update_buttons ( self ) :
self . clear_items ( )
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
prev_button = discord . ui . Button (
label = " Prev " ,
style = discord . ButtonStyle . secondary ,
emoji = discord . PartialEmoji ( name = " left " , id = 1420240344926126090 ) ,
2025-11-02 23:32:52 -05:00
disabled = self . current_page == 0 ,
2025-10-07 22:02:37 -04:00
)
prev_button . callback = self . previous_page
self . add_item ( prev_button )
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
next_button = discord . ui . Button (
label = " Next " ,
style = discord . ButtonStyle . secondary ,
emoji = discord . PartialEmoji ( name = " right " , id = 1420240334100627456 ) ,
2025-11-02 23:32:52 -05:00
disabled = self . current_page == len ( self . pages ) - 1 ,
2025-10-07 22:02:37 -04:00
)
next_button . callback = self . next_page
self . add_item ( next_button )
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
async def previous_page ( self , interaction : discord . Interaction ) :
if interaction . user . id != self . user_id :
2025-11-02 23:32:52 -05:00
await interaction . response . send_message (
" You can ' t control someone else ' s help menu! " , ephemeral = True
)
2025-10-07 22:02:37 -04:00
return
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
if self . current_page > 0 :
self . current_page - = 1
self . update_buttons ( )
2025-11-02 23:32:52 -05:00
await interaction . response . edit_message (
embed = self . create_embed ( ) , view = self
)
2025-10-07 22:02:37 -04:00
else :
await interaction . response . defer ( )
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
async def next_page ( self , interaction : discord . Interaction ) :
if interaction . user . id != self . user_id :
2025-11-02 23:32:52 -05:00
await interaction . response . send_message (
" You can ' t control someone else ' s help menu! " , ephemeral = True
)
2025-10-07 22:02:37 -04:00
return
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
if self . current_page < len ( self . pages ) - 1 :
self . current_page + = 1
self . update_buttons ( )
2025-11-02 23:32:52 -05:00
await interaction . response . edit_message (
embed = self . create_embed ( ) , view = self
)
2025-10-07 22:02:37 -04:00
else :
await interaction . response . defer ( )
2025-11-02 23:32:52 -05:00
2025-10-07 22:02:37 -04:00
async def on_timeout ( self ) :
for item in self . children :
item . disabled = True
2025-10-07 13:33:53 -04:00
class TweetyView ( discord . ui . View ) :
2025-11-02 23:32:52 -05:00
def __init__ (
self ,
author_id : int ,
original_message ,
tweet_data : dict ,
api_url : str ,
image_message : Optional [ discord . Message ] = None ,
) :
2025-10-07 13:33:53 -04:00
super ( ) . __init__ ( timeout = 300 )
self . author_id = author_id
self . original_message = original_message
self . tweet_data = tweet_data
self . api_url = api_url
self . is_dark = tweet_data . get ( " dark " , False )
self . is_verified = tweet_data . get ( " verified " , False )
self . image_message = image_message
2025-10-08 21:55:13 -04:00
self . last_regenerate_time = 0
self . click_count = 0
2025-10-07 13:33:53 -04:00
self . update_button_styles ( )
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
def update_button_styles ( self ) :
self . clear_items ( )
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
dark_button = discord . ui . Button (
label = " Dark Mode " if self . is_dark else " Light Mode " ,
2025-11-02 23:32:52 -05:00
style = discord . ButtonStyle . primary
if self . is_dark
else discord . ButtonStyle . secondary ,
2025-10-07 13:33:53 -04:00
emoji = discord . PartialEmoji ( name = " darkmode " , id = 1425165393751965884 ) ,
2025-11-02 23:32:52 -05:00
custom_id = " toggle_dark " ,
2025-10-07 13:33:53 -04:00
)
dark_button . callback = self . toggle_dark_callback
self . add_item ( dark_button )
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
verified_button = discord . ui . Button (
label = " Verified " ,
2025-11-02 23:32:52 -05:00
style = discord . ButtonStyle . primary
if self . is_verified
else discord . ButtonStyle . secondary ,
emoji = discord . PartialEmoji (
name = " TwitterVerifiedBadge " , id = 1425165432142172392
) ,
custom_id = " toggle_verified " ,
2025-10-07 13:33:53 -04:00
)
verified_button . callback = self . toggle_verified_callback
self . add_item ( verified_button )
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
async def regenerate_tweet ( self , interaction : discord . Interaction ) :
""" Regenerate only the image message with current settings """
2025-10-08 21:55:13 -04:00
if self . click_count > = 10 :
embed = discord . Embed (
title = " Error " ,
description = " Stop spamming! You ' ve reached the maximum number of changes for this tweet. " ,
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " , icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp "
)
2025-10-08 21:55:13 -04:00
await interaction . response . send_message ( embed = embed , ephemeral = True )
return
2025-10-07 13:33:53 -04:00
await interaction . response . defer ( )
2025-10-08 21:55:13 -04:00
current_time = time . time ( )
time_since_last = current_time - self . last_regenerate_time
2025-11-02 23:32:52 -05:00
2025-10-08 21:55:13 -04:00
if time_since_last < 3 and self . last_regenerate_time > 0 :
wait_time = 3 - time_since_last
await asyncio . sleep ( wait_time )
self . click_count + = 1
self . last_regenerate_time = time . time ( )
2025-10-07 13:33:53 -04:00
try :
async with aiohttp . ClientSession ( ) as session :
async with session . post (
f " { self . api_url } /api/render " ,
json = self . tweet_data ,
2025-11-02 23:32:52 -05:00
headers = { " Content-Type " : " application/json " } ,
2025-10-07 13:33:53 -04:00
) as response :
if response . status != 200 :
error_text = await response . text ( )
embed = discord . Embed (
title = " Error " ,
description = f " API Error ( { response . status } ): { error_text } " ,
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " ,
icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp " ,
)
2025-10-07 13:33:53 -04:00
await interaction . followup . send ( embed = embed , ephemeral = True )
return
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
image_data = await response . read ( )
2025-11-02 23:32:52 -05:00
with tempfile . NamedTemporaryFile (
delete = False , suffix = " .png "
) as temp_file :
2025-10-07 13:33:53 -04:00
temp_file . write ( image_data )
temp_file_path = temp_file . name
2025-11-02 23:32:52 -05:00
with open ( temp_file_path , " rb " ) as f :
2025-10-07 13:33:53 -04:00
author_name = self . original_message . author . name
2025-11-02 23:32:52 -05:00
filename = (
f " tweet_ { author_name } _ { int ( datetime . now ( ) . timestamp ( ) ) } .png "
2025-10-07 13:33:53 -04:00
)
2025-11-02 23:32:52 -05:00
file = discord . File ( f , filename = filename )
2025-10-07 13:33:53 -04:00
self . update_button_styles ( )
if self . image_message is not None :
await self . image_message . edit ( attachments = [ file ] , view = self )
else :
await interaction . followup . send ( file = file , view = self )
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
os . remove ( temp_file_path )
2025-11-02 23:32:52 -05:00
2025-11-22 19:51:46 -05:00
except Exception :
2025-10-07 13:33:53 -04:00
embed = discord . Embed (
title = " Error " ,
description = " Error regenerating tweet image " ,
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " , icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp "
)
2025-10-07 13:33:53 -04:00
await interaction . followup . send ( embed = embed , ephemeral = True )
2025-11-02 23:32:52 -05:00
2025-10-07 21:37:55 -04:00
async def _check_author ( self , interaction : discord . Interaction ) - > bool :
""" Check if user is authorized to modify the tweet """
2025-10-07 13:33:53 -04:00
if interaction . user . id != self . author_id :
embed = discord . Embed (
title = " Error " ,
description = " You can ' t modify someone else ' s tweet! " ,
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " , icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp "
)
2025-10-07 13:33:53 -04:00
await interaction . response . send_message ( embed = embed , ephemeral = True )
2025-10-07 21:37:55 -04:00
return False
return True
2025-11-02 23:32:52 -05:00
2025-10-07 21:37:55 -04:00
async def toggle_dark_callback ( self , interaction : discord . Interaction ) :
""" Handle dark mode toggle button click """
if not await self . _check_author ( interaction ) :
2025-10-07 13:33:53 -04:00
return
self . is_dark = not self . is_dark
self . tweet_data [ " dark " ] = self . is_dark
await self . regenerate_tweet ( interaction )
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
async def toggle_verified_callback ( self , interaction : discord . Interaction ) :
""" Handle verified toggle button click """
2025-10-07 21:37:55 -04:00
if not await self . _check_author ( interaction ) :
2025-10-07 13:33:53 -04:00
return
self . is_verified = not self . is_verified
self . tweet_data [ " verified " ] = self . is_verified
await self . regenerate_tweet ( interaction )
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
async def on_timeout ( self ) :
""" Disable buttons when view times out """
for item in self . children :
item . disabled = True
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
def tweety_command ( ) :
@commands.hybrid_command (
2025-11-02 23:32:52 -05:00
name = " tweety " , description = " Convert a replied message to a tweet image. "
2025-10-07 13:33:53 -04:00
)
@commands.cooldown ( 1 , 10 , commands . BucketType . user )
2025-10-07 21:29:42 -04:00
async def tweety ( self , context ) :
2025-10-07 21:37:55 -04:00
if hasattr ( context , " interaction " ) and context . interaction :
2025-10-07 22:02:37 -04:00
view = TweetyHelpView ( user_id = context . author . id , bot = self . bot )
embed = view . create_embed ( )
await context . send ( embed = embed , view = view , ephemeral = True )
2025-10-07 13:33:53 -04:00
return
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
if not context . message . reference or not context . message . reference . message_id :
embed = discord . Embed (
title = " Error " ,
description = " You must reply to a message to use this command! " ,
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " , icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp "
)
2025-10-07 21:37:55 -04:00
await context . send ( embed = embed )
2025-10-07 13:33:53 -04:00
return
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
try :
2025-11-02 23:32:52 -05:00
original_message = await context . channel . fetch_message (
context . message . reference . message_id
)
2025-10-07 13:33:53 -04:00
processing_embed = discord . Embed (
title = " Tweet Generator (Processing) " ,
description = " <a:mariospin:1423677027013103709> Generating tweet... This may take a moment. " ,
color = 0x7289DA ,
)
2025-11-02 23:32:52 -05:00
processing_embed . set_author (
name = " Media " , icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp "
)
2025-10-07 21:37:55 -04:00
processing_msg = await context . send ( embed = processing_embed )
2025-10-07 13:33:53 -04:00
author = original_message . author
display_name = author . display_name or author . name
username = f " @ { author . name } "
2025-11-02 23:32:52 -05:00
avatar_url = (
str ( author . avatar . url )
if author . avatar
else str ( author . default_avatar . url )
)
2025-10-07 13:33:53 -04:00
message_text = original_message . content
2025-11-02 23:32:52 -05:00
2025-10-07 22:24:30 -04:00
for mention in original_message . mentions :
2025-11-02 23:32:52 -05:00
message_text = message_text . replace (
f " <@ { mention . id } > " , f " @ { mention . name } "
)
message_text = message_text . replace (
f " <@! { mention . id } > " , f " @ { mention . name } "
)
2025-10-07 22:24:30 -04:00
for role in original_message . role_mentions :
2025-11-02 23:32:52 -05:00
message_text = message_text . replace ( f " <@& { role . id } > " , f " @ { role . name } " )
2025-10-07 22:24:30 -04:00
for channel in original_message . channel_mentions :
2025-11-02 23:32:52 -05:00
message_text = message_text . replace (
f " <# { channel . id } > " , f " # { channel . name } "
)
2025-10-07 22:41:05 -04:00
if not message_text . strip ( ) :
2025-10-07 21:37:55 -04:00
await processing_msg . delete ( )
2025-10-07 13:33:53 -04:00
embed = discord . Embed (
title = " Error " ,
2025-10-07 22:41:05 -04:00
description = " No text found! This command only works with text messages. " ,
2025-10-07 13:33:53 -04:00
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " , icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp "
)
2025-10-07 22:52:06 -04:00
await context . send ( embed = embed )
2025-10-07 13:33:53 -04:00
return
2025-11-02 23:32:52 -05:00
2025-10-08 00:43:21 -04:00
if len ( message_text ) > 300 :
await processing_msg . delete ( )
embed = discord . Embed (
title = " Error " ,
description = f " Message is too long! Maximum 300 characters allowed. \n Your message: { len ( message_text ) } characters " ,
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " , icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp "
)
2025-10-08 00:43:21 -04:00
await context . send ( embed = embed )
return
2025-11-02 23:32:52 -05:00
2025-10-08 00:43:21 -04:00
message_text = break_long_words ( message_text , max_word_length = 50 )
2025-11-02 23:32:52 -05:00
ny_tz = pytz . timezone ( " America/New_York " )
2025-10-07 22:10:52 -04:00
msg_time_ny = original_message . created_at . astimezone ( ny_tz )
timestamp = msg_time_ny . strftime ( " % I: % M % p · % b %d , % Y " ) . replace ( " 0 " , " " )
2025-10-07 13:33:53 -04:00
tweet_data = {
" name " : display_name [ : 50 ] ,
" handle " : username [ : 20 ] ,
" text " : message_text [ : 300 ] ,
" avatar " : avatar_url ,
" timestamp " : timestamp ,
2025-10-07 21:37:55 -04:00
" verified " : False ,
2025-11-02 23:32:52 -05:00
" dark " : False ,
2025-10-07 13:33:53 -04:00
}
2025-11-02 23:32:52 -05:00
2025-10-08 21:55:13 -04:00
API_BASE_URL = " http://tweet.6969.pro "
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
async with aiohttp . ClientSession ( ) as session :
try :
async with session . post (
f " { API_BASE_URL } /api/render " ,
json = tweet_data ,
2025-11-02 23:32:52 -05:00
headers = { " Content-Type " : " application/json " } ,
2025-10-07 13:33:53 -04:00
) as response :
if response . status != 200 :
2025-10-07 21:37:55 -04:00
await processing_msg . delete ( )
2025-10-07 13:33:53 -04:00
error_text = await response . text ( )
embed = discord . Embed (
title = " Error " ,
description = f " API Error ( { response . status } ): { error_text } " ,
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " ,
icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp " ,
)
2025-10-07 21:37:55 -04:00
await context . send ( embed = embed )
2025-10-07 13:33:53 -04:00
return
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
image_data = await response . read ( )
2025-11-02 23:32:52 -05:00
with tempfile . NamedTemporaryFile (
delete = False , suffix = " .png "
) as temp_file :
2025-10-07 13:33:53 -04:00
temp_file . write ( image_data )
temp_file_path = temp_file . name
2025-10-07 21:37:55 -04:00
await processing_msg . delete ( )
2025-11-02 23:32:52 -05:00
with open ( temp_file_path , " rb " ) as f :
file = discord . File (
f ,
filename = f " tweet_ { author . name } _ { int ( datetime . now ( ) . timestamp ( ) ) } .png " ,
)
2025-10-07 13:33:53 -04:00
embed = discord . Embed (
title = " Tweet Generated " ,
2025-11-22 19:51:46 -05:00
description = " <:error:1424007141768822824> Tweet sometimes may look a bit broken, im gonna rewrite the API another time... (it wasnt made for Syntrel in the first place) " ,
2025-10-07 13:33:53 -04:00
color = 0x7289DA ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " ,
icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp " ,
)
2025-10-07 13:33:53 -04:00
embed . set_footer (
text = f " Requested by { context . author . name } " ,
icon_url = context . author . display_avatar . url ,
)
view = TweetyView (
author_id = context . author . id ,
original_message = original_message ,
tweet_data = tweet_data ,
2025-11-02 23:32:52 -05:00
api_url = API_BASE_URL ,
2025-10-07 13:33:53 -04:00
)
2025-10-07 21:37:55 -04:00
await context . send ( embed = embed )
image_message = await context . send ( file = file , view = view )
view . image_message = image_message
2025-10-07 13:33:53 -04:00
os . remove ( temp_file_path )
2025-11-02 23:32:52 -05:00
2025-10-07 21:37:55 -04:00
except aiohttp . ClientError :
await processing_msg . delete ( )
2025-10-07 13:33:53 -04:00
embed = discord . Embed (
title = " Error " ,
2025-11-22 19:51:46 -05:00
description = " Connection error: Could not reach tweet API " ,
2025-10-07 13:33:53 -04:00
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " ,
icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp " ,
)
2025-10-07 21:37:55 -04:00
await context . send ( embed = embed )
except Exception :
await processing_msg . delete ( )
2025-10-07 13:33:53 -04:00
embed = discord . Embed (
title = " Error " ,
description = " Error generating tweet image " ,
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " ,
icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp " ,
)
2025-10-07 21:37:55 -04:00
await context . send ( embed = embed )
except Exception :
2025-10-07 13:33:53 -04:00
embed = discord . Embed (
title = " Error " ,
description = " Error processing the message! " ,
color = 0xE02B2B ,
)
2025-11-02 23:32:52 -05:00
embed . set_author (
name = " Media " , icon_url = " https://yes.nighty.works/raw/y5SEZ9.webp "
)
2025-10-07 21:37:55 -04:00
await context . send ( embed = embed )
2025-11-02 23:32:52 -05:00
2025-10-07 13:33:53 -04:00
return tweety