init
This commit is contained in:
commit
f6f900b4e2
8 changed files with 553 additions and 0 deletions
38
README.md
Normal file
38
README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# affection-x-bot
|
||||
|
||||
Creates posts on X from messages on Telegram. Supports images and video but limited to 512mb and 140 seconds.
|
||||
|
||||
## Getting Started
|
||||
|
||||
- Clone the repo
|
||||
- Open the `app` folder
|
||||
- Rename `sample.env` to `.env`
|
||||
- Set your Telegram Admin ID. This is an integer value representing your main account.
|
||||
- Enter your Telegram bot token from [@botfather](https://t.me/botfather)
|
||||
- Enter your Telegram App ID and App Hash from [my.telegram.org](https://my.telegram.org)
|
||||
- Enter your X username and access/consumer/client credentials from the [Developer Portal](https://developer.twitter.com/en/portal/dashboard)
|
||||
|
||||
### Bare Metal
|
||||
|
||||
- Type `pip install -r requirements.txt` to install dependencies
|
||||
- Follow the instructions below to authenticate with X
|
||||
- Run by typing `python main.py`
|
||||
|
||||
### Docker
|
||||
|
||||
- Run by typing `docker compose up -d` in the repo's root folder
|
||||
- Enter the docker container by typing `docker exec -it affection-x-bot bash`
|
||||
- Follow the instructions below to authenticate with X
|
||||
- Press CTRL+D to exit the container
|
||||
|
||||
### X
|
||||
You will need both v1 and v2 API credentials from the [Developer Portal](https://developer.twitter.com/en/portal/dashboard)
|
||||
|
||||
- Run `python login-to-x.py` to authorize your X account with the bot
|
||||
- Copy the the URL and authorize your X account using a browser
|
||||
- It will redirect you to a localhost URL. Copy/paste that URL back into the auth script and press enter. It will save your X credentials to the data folder
|
||||
|
||||
### Telegram
|
||||
This bot only works for supergroups (public). You will need a bot token from [@botfather](https://t.me/botfather) + a custom app created using [my.telegram.org](https://my.telegram.org)
|
||||
|
||||
- Add the bot to your supergroup as admin
|
||||
8
app/Dockerfile
Normal file
8
app/Dockerfile
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
FROM python:3.12
|
||||
RUN apt update
|
||||
RUN apt upgrade -y
|
||||
RUN apt install -y build-essential
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN pip install -r requirements.txt
|
||||
CMD ["sh", "-c", "python main.py"]
|
||||
199
app/custom.py
Normal file
199
app/custom.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from json import JSONDecodeError
|
||||
|
||||
import tweepy
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def get_name_from_user(user):
|
||||
if user.username:
|
||||
name = "@{}".format(user.username)
|
||||
else:
|
||||
name = []
|
||||
if user.first_name:
|
||||
name.append(user.first_name)
|
||||
if user.last_name:
|
||||
name.append(user.last_name)
|
||||
name = ' '.join(name)
|
||||
return name if name else 'ser'
|
||||
|
||||
|
||||
def validate_x_url(url):
|
||||
_url = url.split("//")
|
||||
if not _url[1].startswith("x.com/") and not _url[1].startswith("twitter.com/"):
|
||||
return False
|
||||
match = re.search(r"\.com/([a-zA-Z0-9_]+)/status/(.*)", url)
|
||||
return match.group(1), match.group(2)
|
||||
|
||||
|
||||
def load_x_v1_api():
|
||||
x_api_v1 = tweepy.OAuth1UserHandler(
|
||||
os.getenv('X_CONSUMER_KEY'),
|
||||
os.getenv('X_CONSUMER_SECRET'),
|
||||
os.getenv('X_ACCESS_TOKEN'),
|
||||
os.getenv('X_ACCESS_SECRET')
|
||||
)
|
||||
return tweepy.API(x_api_v1)
|
||||
|
||||
|
||||
def load_x_v2_api():
|
||||
try:
|
||||
access_token = json.load(open("{}/creds.json".format(os.getenv('DATA_FOLDER')), "r"))
|
||||
except (JSONDecodeError, FileNotFoundError):
|
||||
raise Exception("Invalid X credentials")
|
||||
else:
|
||||
fields = ["token_type", "access_token", "scope", "refresh_token", "expires_in", "expires_at"]
|
||||
if [f for f in fields if f in access_token] != fields:
|
||||
raise Exception("Invalid X credentials")
|
||||
else:
|
||||
refresh_x_oauth2_token()
|
||||
client = tweepy.Client(access_token=os.getenv('X_ACCESS_TOKEN'), access_token_secret=os.getenv('X_ACCESS_SECRET'), consumer_key=os.getenv('X_CONSUMER_KEY'), consumer_secret=os.getenv('X_CONSUMER_SECRET'))
|
||||
return client
|
||||
|
||||
|
||||
def load_x_v2_api_oauth2_handler():
|
||||
return tweepy.OAuth2UserHandler(
|
||||
client_id=os.getenv('X_CLIENT_ID'),
|
||||
client_secret=os.getenv('X_CLIENT_SECRET'),
|
||||
redirect_uri="https://localhost/",
|
||||
scope=["tweet.read", "tweet.write", "users.read", "offline.access"]
|
||||
)
|
||||
|
||||
|
||||
async def create_tweet(content, media_ids=None):
|
||||
client = load_x_v2_api()
|
||||
post = client.create_tweet(text=content, media_ids=media_ids)
|
||||
if post.data:
|
||||
open("{}/created.txt".format(os.getenv('DATA_FOLDER')), "a+").write("{}\n".format(post.data['id']))
|
||||
return post.data
|
||||
|
||||
|
||||
async def delete_tweet(post_id):
|
||||
deleted_ids_file = "{}/deleted.txt".format(os.getenv('DATA_FOLDER'))
|
||||
try:
|
||||
with open(deleted_ids_file, 'r') as f:
|
||||
for line in f.readlines():
|
||||
if post_id == line.strip():
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
open(deleted_ids_file, "w").write('')
|
||||
client = load_x_v2_api()
|
||||
result = client.delete_tweet(post_id)
|
||||
if result.data['deleted'] is True:
|
||||
open(deleted_ids_file, "a").write("{}\n".format(post_id))
|
||||
return result
|
||||
|
||||
|
||||
async def find_text_and_download_media(bot, message):
|
||||
os.makedirs(media_folder := "{}/media".format(os.getenv('DATA_FOLDER')), exist_ok=True)
|
||||
text = message.message
|
||||
media_path, media_size = None, None
|
||||
media_channel_id = message.peer_id.channel_id
|
||||
# everything else
|
||||
if hasattr(message.media, 'document'):
|
||||
try:
|
||||
# check video length
|
||||
if message.document.attributes[0].duration > 140:
|
||||
raise Exception("Video is too long")
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
mime_type = message.document.mime_type.split('/')
|
||||
media_id = message.media.document.id
|
||||
media_path = "{}/{}.{}".format(media_folder, media_id, mime_type[1])
|
||||
media_size = message.media.document.size
|
||||
# compressed pictures
|
||||
elif hasattr(message.media, 'photo'):
|
||||
media_id = message.media.photo.id
|
||||
media_path = "{}/{}.{}".format(media_folder, media_id, 'jpg')
|
||||
media_size = message.media.photo.size if hasattr(message.media.photo, 'size') else message.media.photo.sizes[-1].sizes[-1]
|
||||
# check filesize
|
||||
if media_size > 536870912:
|
||||
raise Exception("File is too big")
|
||||
# create a download progress file
|
||||
os.makedirs(progress_folder := "{}/progress".format(os.getenv('DATA_FOLDER')), exist_ok=True)
|
||||
media_progress_file = "{}/{}-{}.json".format(progress_folder, media_channel_id, message.id)
|
||||
progress_data = {"last_update": time.time(), "percent": 0, "file_path": media_path, "cancelled": False, "id": None}
|
||||
if media_path:
|
||||
# get local media size
|
||||
if os.path.exists(media_path):
|
||||
_media_size = os.path.getsize(media_path)
|
||||
else:
|
||||
_media_size = 0
|
||||
# check if sizes match
|
||||
if media_size > _media_size:
|
||||
progress_message = await message.reply("Downloading from TG... 0%")
|
||||
progress_data['id'] = progress_message.id
|
||||
# deleting existing file
|
||||
if os.path.exists(media_path):
|
||||
os.remove(media_path)
|
||||
open(media_progress_file, "w").write(json.dumps(progress_data))
|
||||
|
||||
# callback to update the download progress
|
||||
async def progress_callback(current, total):
|
||||
progress = json.loads(open(media_progress_file, "r").read())
|
||||
percent = round(current / total, 2)
|
||||
if progress['cancelled'] is True:
|
||||
await bot.edit_message(progress_message, "Downloading from TG... cancelled!!")
|
||||
raise Exception('Cancelled')
|
||||
if float(progress['last_update']) + 3 < time.time():
|
||||
try:
|
||||
await bot.edit_message(progress_message, "Downloading from TG... {}%".format(round(percent * 100, 1)))
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
else:
|
||||
progress['last_update'] = time.time()
|
||||
progress['percent'] = percent
|
||||
open(media_progress_file, "w").write(json.dumps(progress))
|
||||
|
||||
# download a new copy of the media
|
||||
await bot.download_media(message, media_path, progress_callback=progress_callback)
|
||||
# show it's completed
|
||||
await bot.edit_message(progress_message, "Downloading from TG... 100%")
|
||||
|
||||
open(media_progress_file, "w").write(json.dumps(progress_data))
|
||||
return text, media_path
|
||||
|
||||
|
||||
async def upload_media(filename):
|
||||
api = load_x_v1_api()
|
||||
media = api.media_upload(filename)
|
||||
if hasattr(media, 'image') or media.processing_info['state'] == 'succeeded':
|
||||
return media.media_id
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def refresh_x_oauth2_token():
|
||||
try:
|
||||
access_token = json.load(open("{}/creds.json".format(os.getenv('DATA_FOLDER')), "r"))
|
||||
except JSONDecodeError:
|
||||
logging.debug("Failed to refresh access token")
|
||||
return False
|
||||
else:
|
||||
if access_token["expires_at"] - 900 < time.time():
|
||||
x_api_v2 = load_x_v2_api_oauth2_handler()
|
||||
access_token = x_api_v2.refresh_token("https://api.twitter.com/2/oauth2/token", refresh_token=access_token["refresh_token"])
|
||||
open("{}/creds.json".format(os.getenv('DATA_FOLDER')), "w").write(json.dumps(access_token, indent=4))
|
||||
logging.debug("Refreshed access token")
|
||||
else:
|
||||
logging.debug("Access token is still valid")
|
||||
return access_token
|
||||
|
||||
|
||||
def delete_old_media():
|
||||
days = int(os.getenv('DELETE_OLD_MEDIA_IN_DAYS'))
|
||||
os.makedirs("{}/media".format(os.getenv('DATA_FOLDER')), exist_ok=True)
|
||||
for file in os.listdir("{}/media".format(os.getenv('DATA_FOLDER'))):
|
||||
file_path = os.path.join("{}/media".format(os.getenv('DATA_FOLDER'), file))
|
||||
if os.path.isfile(file_path):
|
||||
mod_time = os.path.getmtime(file_path)
|
||||
if (datetime.datetime.now() - datetime.datetime.fromtimestamp(mod_time)).days > days:
|
||||
os.remove(file_path)
|
||||
logging.debug("Deleted old media ({} days): {}".format(days, file))
|
||||
19
app/login-to-x.py
Normal file
19
app/login-to-x.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import json
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from custom import load_x_v2_api_oauth2_handler
|
||||
|
||||
|
||||
def gen_x_oauth2_token():
|
||||
x_api_v2 = load_x_v2_api_oauth2_handler()
|
||||
url = x_api_v2.get_authorization_url()
|
||||
print("Authorization URL: {}".format(url))
|
||||
response_url = input("Enter response URL: ")
|
||||
access_token = x_api_v2.fetch_token(response_url)
|
||||
open("{}/creds.json".format(os.getenv('DATA_FOLDER')), "w").write(json.dumps(access_token, indent=4))
|
||||
print("Generated new access token")
|
||||
|
||||
|
||||
load_dotenv('./app/.env')
|
||||
gen_x_oauth2_token()
|
||||
259
app/main.py
Normal file
259
app/main.py
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
import sys
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from telethon import TelegramClient
|
||||
from telethon.sync import events
|
||||
from telethon.tl.types import MessageReplyStoryHeader
|
||||
|
||||
from custom import *
|
||||
|
||||
log_file = '{}/app.log'.format(os.getenv('DATA_FOLDER'))
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s %(name)s %(levelname)s %(message)s',
|
||||
datefmt='%H:%M:%S',
|
||||
level=logging.INFO,
|
||||
handlers=[
|
||||
logging.FileHandler(log_file),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
for package in (silence_packages := (
|
||||
'apscheduler',
|
||||
'telethon'
|
||||
)):
|
||||
logging.getLogger(package).setLevel(logging.ERROR)
|
||||
bot = TelegramClient('bot', int(os.getenv('TELEGRAM_APP_ID')), os.getenv('TELEGRAM_APP_HASH')).start(bot_token=os.getenv('TELEGRAM_BOT_TOKEN'))
|
||||
|
||||
|
||||
@bot.on(events.NewMessage)
|
||||
async def echo(event):
|
||||
user_input = event.text
|
||||
# skip if bot or no one sent a command
|
||||
if (event.sender and event.sender.bot) or not user_input:
|
||||
return
|
||||
user_input = user_input.split(" ")
|
||||
chat_id = event.chat_id
|
||||
channel_id = event.message.peer_id.channel_id
|
||||
|
||||
# get reply from the message
|
||||
reply_to_message, reply_to_user = None, None
|
||||
reply_user_id, reply_user_name, reply_message_id, reply_top_id = None, None, None, None
|
||||
if event.is_reply:
|
||||
# skip if replied to a story
|
||||
if type(event.reply_to) is MessageReplyStoryHeader:
|
||||
return
|
||||
reply_to_message = await event.get_reply_message()
|
||||
reply_to_user = await reply_to_message.get_sender()
|
||||
reply_user_id = reply_to_user.id
|
||||
reply_user_name = get_name_from_user(reply_to_user)
|
||||
reply_message_id = reply_to_message.id
|
||||
if event.reply_to.reply_to_top_id:
|
||||
reply_message_id = event.reply_to.reply_to_top_id
|
||||
|
||||
# get message
|
||||
message_id = event.message.id
|
||||
user = await event.message.get_sender()
|
||||
user_id = user.id
|
||||
user_name = get_name_from_user(user)
|
||||
|
||||
# load perms file or create one
|
||||
permissions_file = "{}/permissions.json".format(os.getenv('DATA_FOLDER'))
|
||||
try:
|
||||
permissions = json.load(open(permissions_file))
|
||||
except (JSONDecodeError, FileNotFoundError):
|
||||
permissions = {"allowed": [], "disallowed": []}
|
||||
open(permissions_file, "w").write(json.dumps(permissions, indent=4))
|
||||
|
||||
# check if user is allowed to tweet
|
||||
if user_id not in permissions["allowed"] \
|
||||
and str(user_id) != os.getenv('TELEGRAM_ADMIN_ID'):
|
||||
return
|
||||
|
||||
# stage progress file to track the download process
|
||||
media_progress_file = "{}/progress/{}-{}.json".format(os.getenv('DATA_FOLDER'), channel_id, reply_message_id)
|
||||
|
||||
# allow/disallow users from reposting to x
|
||||
if user_input[0] in ('/approve', '/disapprove',):
|
||||
# check if user is admin
|
||||
if str(user_id) != str(os.getenv('TELEGRAM_ADMIN_ID')):
|
||||
return
|
||||
# swap user id between lists
|
||||
match user_input[0]:
|
||||
case '/approve':
|
||||
# add user to allow list
|
||||
if reply_user_id not in permissions['allowed']:
|
||||
permissions['allowed'].append(reply_user_id)
|
||||
# remove user from disallow list
|
||||
if reply_user_id in permissions['disallowed']:
|
||||
permissions['disallowed'].remove(reply_user_id)
|
||||
await event.reply("Approved {}".format(reply_user_name))
|
||||
case '/disapprove':
|
||||
# add user to disallow list
|
||||
if reply_user_id not in permissions['disallowed']:
|
||||
permissions['disallowed'].append(reply_user_id)
|
||||
# remove user from allow list
|
||||
if reply_user_id in permissions['allowed']:
|
||||
permissions['allowed'].remove(reply_user_id)
|
||||
await event.reply("Disapproved {}".format(reply_user_name))
|
||||
# save users file
|
||||
open(permissions_file, "w").write(json.dumps(permissions, indent=4))
|
||||
|
||||
# new tweet
|
||||
elif user_input[0] in ('/tweet',):
|
||||
# check if post is in progress
|
||||
if os.path.exists(media_progress_file):
|
||||
return await event.reply("This post is already in progress")
|
||||
|
||||
# tweet replied to messages to x
|
||||
if reply_to_message:
|
||||
record_path = "{}/posts/forward_{}_{}.json".format(os.getenv('DATA_FOLDER'), chat_id, message_id)
|
||||
# check if already posted
|
||||
if os.path.exists(record_path):
|
||||
return await event.reply("Already posted")
|
||||
|
||||
# tweet any text and replies with the url
|
||||
else:
|
||||
user_name = get_name_from_user(event.message.from_user)
|
||||
record_path = "{}/posts/tweet_{}_{}.json".format(os.getenv('DATA_FOLDER'), chat_id, message_id)
|
||||
|
||||
# get text, switch to caption if media is attached
|
||||
try:
|
||||
text, media_path = await find_text_and_download_media(bot, reply_to_message or event.message)
|
||||
except Exception as e:
|
||||
if os.path.exists(media_progress_file):
|
||||
os.remove(media_progress_file)
|
||||
if "Cancelled" in str(e):
|
||||
logging.debug("Cancelled media download")
|
||||
return
|
||||
logging.error(e)
|
||||
if "File is too big" in str(e):
|
||||
return await event.reply("Error: file is too big for X")
|
||||
elif "Video is too long" in str(e):
|
||||
return await event.reply("Error: video is too long for X")
|
||||
else:
|
||||
return await event.reply("Error: unknown TG download error")
|
||||
else:
|
||||
logging.info("{} ({}) Downloaded file: {}".format(user_name, user_id, media_path))
|
||||
media_progress = json.loads(open(media_progress_file, "r").read())
|
||||
if media_progress['id']:
|
||||
await bot.edit_message(channel_id, media_progress['id'], "Uploading to X...")
|
||||
else:
|
||||
progress_message = await event.reply("Uploading to X...")
|
||||
media_progress['id'] = progress_message.id
|
||||
open(media_progress_file, "w").write(json.dumps(media_progress))
|
||||
|
||||
text = text.replace(user_input[0], '').strip() if text else ''
|
||||
|
||||
# check if tweet is empty
|
||||
if len(user_input) == 1 and not media_path and not text:
|
||||
return await event.reply("Not enough parameters")
|
||||
# upload media
|
||||
try:
|
||||
media_id = await upload_media(media_path) if media_path else None
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
if "maxFileSizeExceeded" in str(e):
|
||||
return await event.reply("Error: max file size exceeded for X")
|
||||
else:
|
||||
return await event.reply("Error: unknown X upload error")
|
||||
else:
|
||||
if media_id:
|
||||
logging.info("{} ({}) Uploaded file: {}".format(user_name, user_id, media_path))
|
||||
elif media_id is False:
|
||||
logging.info("{} ({}) Failed to uploaded file: {}".format(user_name, user_id, media_path))
|
||||
finally:
|
||||
if os.path.exists(media_progress_file):
|
||||
media_progress = json.loads(open(media_progress_file, "r").read())
|
||||
await bot.delete_messages(channel_id, media_progress['id'])
|
||||
os.remove(media_progress_file)
|
||||
|
||||
# send tweet
|
||||
try:
|
||||
post = await create_tweet(text, [media_id] if media_id else None)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
if "include either text or media" in str(e):
|
||||
return await event.reply("Error: no text or media")
|
||||
elif "duplicate content" in str(e):
|
||||
return await event.reply("Error: duplicate content")
|
||||
elif "Invalid X credentials" in str(e):
|
||||
return await event.reply("Error: not authenticated on X")
|
||||
else:
|
||||
return await event.reply("Error: unknown X posting error")
|
||||
# send the tweet url to telegram
|
||||
tweet_url = "https://x.com/{}/status/{}".format(os.getenv('X_USER'), post['id'])
|
||||
await event.reply(message := "Posted: {}".format(tweet_url))
|
||||
logging.info("{} ({}) {}".format(user_name, user_id, message))
|
||||
# form a record of this tweet
|
||||
record = {
|
||||
"chat_id": chat_id,
|
||||
"origin_message_id": reply_message_id,
|
||||
"origin_user_id": reply_user_id,
|
||||
"origin_user_name": reply_user_name,
|
||||
"user_id": user_id,
|
||||
"user_name": user_name,
|
||||
"text": text,
|
||||
"tweet_id": post['id'],
|
||||
"tweet_url": tweet_url
|
||||
}
|
||||
# save a record of this tweet
|
||||
os.makedirs("{}/posts".format(os.getenv('DATA_FOLDER')), exist_ok=True)
|
||||
open(record_path, "w").write(json.dumps(record, indent=4))
|
||||
|
||||
# delete tweet
|
||||
elif user_input[0] in ('/untweet',):
|
||||
# nothing to delete
|
||||
if len(user_input) == 1 and not reply_to_message:
|
||||
return await event.reply("Not enough parameters")
|
||||
# get the tweet id from the message
|
||||
post_id = user_input[1] if len(user_input) > 1 else None
|
||||
# get text from the telegram replied to message
|
||||
text = " ".join(user_input[1:])
|
||||
if reply_to_message:
|
||||
text = reply_to_message.text
|
||||
if type(post_id) is not int:
|
||||
_, post_id = validate_x_url(text)
|
||||
# delete tweet
|
||||
try:
|
||||
result = await delete_tweet(post_id)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
if "Invalid X credentials" in str(e):
|
||||
return await event.reply("Error: not authenticated on X")
|
||||
else:
|
||||
return await event.reply("Error: unknown X error")
|
||||
else:
|
||||
# mark as deleted
|
||||
if not result:
|
||||
return await event.reply("Already deleted")
|
||||
# send the tweet url to telegram
|
||||
else:
|
||||
await event.reply(message := "Deleted: https://x.com/{}/status/{}".format(os.getenv('X_USER'), post_id))
|
||||
logging.info("{} ({}) {}".format(user_name, user_id, message))
|
||||
|
||||
# cancel tweet if downloading is in progress
|
||||
elif user_input[0] in ('/cancel', '/stop',):
|
||||
# ignore if download progress file doesn't exist for this message
|
||||
if not os.path.exists(media_progress_file):
|
||||
return await event.reply("Nothing to cancel")
|
||||
try:
|
||||
# load download progress if there's one that matches
|
||||
media_progress = json.loads(open(media_progress_file, "r").read())
|
||||
except (JSONDecodeError, FileNotFoundError) as e:
|
||||
logging.error(e)
|
||||
return await event.reply("Error: failed to cancel")
|
||||
else:
|
||||
# set download progress file as cancelled
|
||||
media_progress['cancelled'] = True
|
||||
open(media_progress_file, "w").write(json.dumps(media_progress, indent=4))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.info("-" * 50)
|
||||
logging.info("Affection TG X Bot")
|
||||
logging.info("-" * 50)
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.add_job(refresh_x_oauth2_token, 'interval', minutes=1)
|
||||
scheduler.add_job(delete_old_media, 'interval', minutes=1)
|
||||
scheduler.start()
|
||||
bot.run_until_disconnected()
|
||||
5
app/requirements.txt
Normal file
5
app/requirements.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
httpx
|
||||
telethon
|
||||
python-dotenv
|
||||
apscheduler
|
||||
tweepy
|
||||
13
app/sample.env
Normal file
13
app/sample.env
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
DATA_FOLDER=../data
|
||||
DELETE_OLD_MEDIA_IN_DAYS=3
|
||||
TELEGRAM_ADMIN_ID=
|
||||
TELEGRAM_APP_ID=
|
||||
TELEGRAM_APP_HASH=
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
X_USER=
|
||||
X_ACCESS_TOKEN=
|
||||
X_ACCESS_SECRET=
|
||||
X_CONSUMER_KEY=
|
||||
X_CONSUMER_SECRET=
|
||||
X_CLIENT_ID=
|
||||
X_CLIENT_SECRET=
|
||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
version: "3.1"
|
||||
services:
|
||||
app:
|
||||
container_name: affection-x-bot
|
||||
build: app
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- "./app:/app"
|
||||
- "./data:/data"
|
||||
command: ['python', 'main.py']
|
||||
env_file:
|
||||
- ./app/.env
|
||||
Loading…
Reference in a new issue