├── README.md
├── message_broker
├── __init__.py
└── rabbitmq_service.py
├── telegram_service
├── __init__.py
├── telegram_listener.py
└── telegram_sender.py
├── instagram_service
├── __init__.py
├── instagram_listener.py
├── instagram_sender.py
└── instagram_handler_service.py
├── redis.conf
├── requirements.txt
├── dockerfile
├── utils
└── singleton.py
├── docker-compose.yml
├── config
└── config.py
├── run_all.py
├── logger
└── log.py
└── .gitignore
/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/message_broker/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/telegram_service/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/instagram_service/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/redis.conf:
--------------------------------------------------------------------------------
1 | appendonly yes
2 | appendfsync everysec
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | git+https://github.com/revisto/instagrapi.git
2 | python-telegram-bot
3 | pika
4 | redis
5 | python-dotenv
6 | Pillow
7 | loguru
--------------------------------------------------------------------------------
/dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9
2 |
3 | WORKDIR /app
4 |
5 | COPY requirements.txt .
6 | RUN pip install -r requirements.txt
7 |
8 | COPY . .
9 |
10 | CMD ["python", "run_all.py"]
--------------------------------------------------------------------------------
/utils/singleton.py:
--------------------------------------------------------------------------------
1 | class Singleton(type):
2 | _instances = {}
3 | def __call__(cls, *args, **kwargs):
4 | if cls not in cls._instances:
5 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
6 | return cls._instances[cls]
--------------------------------------------------------------------------------
/instagram_service/instagram_listener.py:
--------------------------------------------------------------------------------
1 | from time import sleep
2 | from random import randint
3 |
4 | from config.config import Config
5 | from instagram_service.instagram_handler_service import InstagramService
6 | from logger.log import Logger
7 |
8 | logger = Logger("InstagramListener")
9 | instagram_listener = InstagramService(Config)
10 |
11 | logger.log_info("Service started")
12 |
13 | while True:
14 | message = instagram_listener.listen()
15 | sleep_time = randint(0.7 * 60 * 60, 1 * 60 * 60)
16 | logger.log_info(f"Sleeping for {sleep_time} seconds")
17 | sleep(sleep_time)
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | app:
4 | build: .
5 | env_file:
6 | - .env
7 | depends_on:
8 | - redis
9 | - rabbitmq
10 | volumes:
11 | - .:/app
12 | - ./config:/app/config
13 |
14 | redis:
15 | image: redis:latest
16 | command: redis-server --requirepass ${REDIS_PASSWORD}
17 | environment:
18 | REDIS_PORT: ${REDIS_PORT}
19 | REDIS_PASSWORD: ${REDIS_PASSWORD}
20 | volumes:
21 | - ./redis-data:/data
22 | - ./redis.conf:/usr/local/etc/redis/redis.conf
23 |
24 | rabbitmq:
25 | image: rabbitmq:3-management
26 | environment:
27 | RABBITMQ_PORT: ${RABBITMQ_PORT}
28 | RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME}
29 | RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
--------------------------------------------------------------------------------
/config/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dotenv import load_dotenv
3 |
4 | load_dotenv()
5 |
6 | class Config:
7 | INSTAGRAM_USERNAME = os.getenv('INSTAGRAM_USERNAME')
8 | INSTAGRAM_PASSWORD = os.getenv('INSTAGRAM_PASSWORD')
9 | INSTAGRAM_TARGET_USERNAME = os.getenv('INSTAGRAM_TARGET_USERNAME')
10 | REDIS_HOST = os.getenv('REDIS_HOST')
11 | REDIS_PORT = int(os.getenv('REDIS_PORT'))
12 | REDIS_PASSWORD = os.getenv('REDIS_PASSWORD')
13 | RABBITMQ_HOST = os.getenv('RABBITMQ_HOST')
14 | RABBITMQ_PORT = int(os.getenv('RABBITMQ_PORT'))
15 | RABBITMQ_USERNAME = os.getenv('RABBITMQ_USERNAME')
16 | RABBITMQ_PASSWORD = os.getenv('RABBITMQ_PASSWORD')
17 | TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
18 | TELEGRAM_CHAT_ID = os.getenv('TELEGRAM_CHAT_ID')
19 | TELEGRAM_LOG_CHAT_ID = os.getenv('TELEGRAM_LOG_CHAT_ID')
--------------------------------------------------------------------------------
/run_all.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 | from time import sleep
4 |
5 | python_files = ["instagram_service/instagram_listener.py", "instagram_service/instagram_sender.py", "telegram_service/telegram_sender.py", "telegram_service/telegram_listener.py"]
6 |
7 | def run_all_main_files():
8 | processes = []
9 | try:
10 | for python_file in python_files:
11 | env = os.environ.copy()
12 | env["PYTHONPATH"] = os.getcwd() + ":" + env.get("PYTHONPATH", "")
13 | process = subprocess.Popen(["python", python_file], env=env)
14 | processes.append(process)
15 | while True: # Keep the script running
16 | pass
17 | except KeyboardInterrupt:
18 | print("\nCtrl+C received, terminating all processes")
19 | for process in processes:
20 | process.terminate()
21 | for process in processes:
22 | process.wait()
23 |
24 | if __name__ == "__main__":
25 | sleep(15) # wait for RabbitMQ to start
26 | run_all_main_files()
--------------------------------------------------------------------------------
/instagram_service/instagram_sender.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from config.config import Config
4 | from message_broker.rabbitmq_service import RabbitMQService
5 | from instagram_service.instagram_handler_service import InstagramService, ReplyToMessage
6 | from logger.log import Logger
7 |
8 | logger = Logger("InstagramSender")
9 |
10 | def callback(ch, method, properties, body):
11 | logger.log_info("Received a message")
12 | message = json.loads(body)
13 | if message.get("action") == "reply":
14 | reply_to_message = ReplyToMessage(message["id"], message["client_context"])
15 | InstagramService(Config).reply_in_direct(message["text"], reply_to_message)
16 | logger.log_info(f"Replied to message: {message['text']}")
17 | if message.get("action") == "listen":
18 | logger.log_info("Listening to messages on command")
19 | InstagramService(Config).listen()
20 |
21 | rabbitmq_service = RabbitMQService(Config)
22 | logger.log_info("Service started")
23 | rabbitmq_service.start_consuming({'telegram_to_instagram': callback})
--------------------------------------------------------------------------------
/logger/log.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from loguru import logger
3 |
4 | from utils.singleton import Singleton
5 | from message_broker.rabbitmq_service import RabbitMQService
6 | from config.config import Config
7 |
8 | class Logger(metaclass=Singleton):
9 | def __init__(self, service):
10 | self.service = service
11 | # Configure the logger
12 | logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {extra[service]} - {message}", level="INFO")
13 | logger.add("log.txt", format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {extra[service]} - {message}", level="INFO")
14 | self.logger = logger.bind(service=service)
15 |
16 | def log_and_send(self, level, message):
17 | # Log the message
18 | getattr(self.logger, level)(message)
19 |
20 | # Send the log message to RabbitMQ
21 | self.rabbitmq_service = RabbitMQService(Config)
22 | self.rabbitmq_service.send_message_logs(f"{self.service} - {level.upper()} - {message}")
23 | self.rabbitmq_service.close_connection()
24 |
25 | def log_debug(self, message):
26 | self.log_and_send('debug', message)
27 |
28 | def log_info(self, message):
29 | self.log_and_send('info', message)
30 |
31 | def log_warning(self, message):
32 | self.log_and_send('warning', message)
33 |
34 | def log_error(self, message):
35 | self.log_and_send('error', message)
36 |
37 | def log_critical(self, message):
38 | self.log_and_send('critical', message)
--------------------------------------------------------------------------------
/message_broker/rabbitmq_service.py:
--------------------------------------------------------------------------------
1 | import pika
2 |
3 | class RabbitMQService:
4 | def __init__(self, config):
5 | self.rabbitmq_username = config.RABBITMQ_USERNAME
6 | self.rabbitmq_password = config.RABBITMQ_PASSWORD
7 | self.rabbitmq_host = config.RABBITMQ_HOST
8 | self.rabbitmq_port = config.RABBITMQ_PORT
9 |
10 | # Connect to RabbitMQ
11 | credentials = pika.PlainCredentials(self.rabbitmq_username, self.rabbitmq_password)
12 | self.connection = pika.BlockingConnection(pika.ConnectionParameters(host=self.rabbitmq_host, port=self.rabbitmq_port, credentials=credentials))
13 | self.channel = self.connection.channel()
14 |
15 | # Declare the exchanges and queues
16 | self.exchanges_queues = {
17 | 'instagram_to_telegram': {
18 | 'exchange': 'instagram_to_telegram_exchange',
19 | 'queue': 'instagram_to_telegram_queue'
20 | },
21 | 'telegram_to_instagram': {
22 | 'exchange': 'telegram_to_instagram_exchange',
23 | 'queue': 'telegram_to_instagram_queue'
24 | },
25 | 'logs': {
26 | 'exchange': 'logs_exchange',
27 | 'queue': 'logs_queue'
28 | }
29 | }
30 |
31 | for key, value in self.exchanges_queues.items():
32 | self.channel.exchange_declare(exchange=value['exchange'], exchange_type='direct')
33 | self.channel.queue_declare(queue=value['queue'])
34 | self.channel.queue_bind(exchange=value['exchange'], queue=value['queue'], routing_key=key)
35 |
36 | def start_consuming(self, callbacks):
37 | for key, value in self.exchanges_queues.items():
38 | if key in callbacks:
39 | self.channel.basic_consume(queue=value['queue'], on_message_callback=callbacks[key], auto_ack=True)
40 | self.channel.start_consuming()
41 |
42 | def send_message_instagram_to_telegram(self, message):
43 | self.channel.basic_publish(exchange=self.exchanges_queues['instagram_to_telegram']['exchange'], routing_key='instagram_to_telegram', body=message)
44 |
45 | def send_message_telegram_to_instagram(self, message):
46 | self.channel.basic_publish(exchange=self.exchanges_queues['telegram_to_instagram']['exchange'], routing_key='telegram_to_instagram', body=message)
47 |
48 | def send_message_logs(self, message):
49 | self.channel.basic_publish(exchange=self.exchanges_queues['logs']['exchange'], routing_key='logs', body=message)
50 |
51 | def stop_consuming(self):
52 | self.channel.stop_consuming()
53 |
54 | def close_connection(self):
55 | self.connection.close()
--------------------------------------------------------------------------------
/telegram_service/telegram_listener.py:
--------------------------------------------------------------------------------
1 | from telegram import ForceReply, Update
2 | from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
3 | from redis import Redis
4 | import json
5 |
6 | from message_broker.rabbitmq_service import RabbitMQService
7 | from config.config import Config
8 | from logger.log import Logger
9 |
10 | logger = Logger("TelegramListener")
11 | redis_telegram_client = Redis(host=Config.REDIS_HOST, port=Config.REDIS_PORT, password=Config.REDIS_PASSWORD, db=1)
12 |
13 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
14 | """Send a message when the command /start is issued."""
15 | user = update.effective_user
16 | await update.message.reply_html(
17 | rf"Hi {user.mention_html()}!"
18 | )
19 | logger.log_info(f"Start command issued by {user.mention_html()}")
20 |
21 | async def listen(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
22 | rabbitmq_service = RabbitMQService(Config)
23 | rabbitmq_service.send_message_telegram_to_instagram(json.dumps({"action": "listen"}))
24 | rabbitmq_service.close_connection()
25 | await update.message.set_reaction("👍")
26 | logger.log_info(f"Listen command issued by {update.effective_user.mention_html()}")
27 |
28 |
29 | async def send_direct(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
30 | if update.message.reply_to_message is not None:
31 | replied_message_id = update.message.reply_to_message.message_id
32 | instagram_data = redis_telegram_client.get(replied_message_id)
33 | if instagram_data is not None:
34 | instagram_data = json.loads(instagram_data)
35 | instagram_data["text"] = update.message.text
36 | instagram_data["action"] = "reply"
37 | rabbitmq_service = RabbitMQService(Config)
38 | rabbitmq_service.send_message_telegram_to_instagram(json.dumps(instagram_data))
39 | rabbitmq_service.close_connection()
40 | await update.message.set_reaction("👍")
41 | logger.log_info(f"Message sent: {update.message.text}")
42 | return
43 |
44 | await update.message.set_reaction("👎")
45 | logger.log_info("Message not sent")
46 |
47 | def main() -> None:
48 | """Start the bot."""
49 | # Create the Application and pass it your bot's token.
50 | application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
51 |
52 | # on different commands - answer in Telegram
53 | application.add_handler(CommandHandler("start", start))
54 | application.add_handler(CommandHandler("listen", listen))
55 |
56 | # on non command i.e message - echo the message on Telegram
57 | application.add_handler(MessageHandler(filters.TEXT, send_direct))
58 |
59 | # Run the bot until the user presses Ctrl-C
60 | application.run_polling(allowed_updates=Update.ALL_TYPES)
61 | logger.log_info("Service started")
62 |
63 | if __name__ == "__main__":
64 | main()
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110 | .pdm.toml
111 | .pdm-python
112 | .pdm-build/
113 |
114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115 | __pypackages__/
116 |
117 | # Celery stuff
118 | celerybeat-schedule
119 | celerybeat.pid
120 |
121 | # SageMath parsed files
122 | *.sage.py
123 |
124 | # Environments
125 | .env
126 | .venv
127 | env/
128 | venv/
129 | ENV/
130 | env.bak/
131 | venv.bak/
132 |
133 | # Spyder project settings
134 | .spyderproject
135 | .spyproject
136 |
137 | # Rope project settings
138 | .ropeproject
139 |
140 | # mkdocs documentation
141 | /site
142 |
143 | # mypy
144 | .mypy_cache/
145 | .dmypy.json
146 | dmypy.json
147 |
148 | # Pyre type checker
149 | .pyre/
150 |
151 | # pytype static type analyzer
152 | .pytype/
153 |
154 | # Cython debug symbols
155 | cython_debug/
156 | session.json
157 |
158 | # PyCharm
159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161 | # and can be added to the global gitignore or merged into this file. For a more nuclear
162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163 | #.idea/
--------------------------------------------------------------------------------
/telegram_service/telegram_sender.py:
--------------------------------------------------------------------------------
1 | from redis import Redis
2 | import json
3 | import requests
4 | import io
5 | from time import sleep
6 |
7 | from message_broker.rabbitmq_service import RabbitMQService
8 | from config.config import Config
9 | from logger.log import Logger
10 |
11 | # Create an instance of Logger
12 | logger = Logger("TelegramSender")
13 |
14 | redis_instagram_client = Redis(
15 | host=Config.REDIS_HOST, port=Config.REDIS_PORT, password=Config.REDIS_PASSWORD, db=0
16 | )
17 | redis_telegram_client = Redis(
18 | host=Config.REDIS_HOST, port=Config.REDIS_PORT, password=Config.REDIS_PASSWORD, db=1
19 | )
20 |
21 |
22 | class TelegramSender:
23 | def __init__(self, config):
24 | self.TELEGRAM_BOT_TOKEN = config.TELEGRAM_BOT_TOKEN
25 | self.CHAT_ID = config.TELEGRAM_CHAT_ID
26 |
27 | def send_text_message(self, text, chat_id=None):
28 | if chat_id is None:
29 | chat_id = self.CHAT_ID
30 |
31 | url = f"https://api.telegram.org/bot{self.TELEGRAM_BOT_TOKEN}/sendMessage"
32 | data = {"chat_id": chat_id, "text": text}
33 | req = requests.post(url, data=data)
34 | if req.status_code == 200:
35 | message_id = json.loads(req.text)["result"]["message_id"]
36 | return message_id
37 | else:
38 | if "retry after" in json.loads(req.text)["description"]:
39 | sleep(30)
40 | else:
41 | logger.log_critical(f"Failed to send text message: {json.loads(req.text)['description']}")
42 | return None
43 |
44 | def send_video_message(self, video_url, caption):
45 | url = f"https://api.telegram.org/bot{self.TELEGRAM_BOT_TOKEN}/sendVideo"
46 | data = {"parse_mode": "HTML", "chat_id": self.CHAT_ID, "caption": caption}
47 | video = requests.get(video_url)
48 | video_stream = io.BytesIO(video.content)
49 | video_stream.name = "video.mp4"
50 | files = {"video": video_stream}
51 | req = requests.post(url, data=data, files=files)
52 | response = json.loads(req.text)
53 | if req.status_code == 200:
54 | message_id = response["result"]["message_id"]
55 | return message_id
56 | else:
57 | logger.log_critical(f"Failed to send video message: {response['description']}")
58 | return None
59 |
60 | def send_photo_message(self, photo_url, caption):
61 | url = f"https://api.telegram.org/bot{self.TELEGRAM_BOT_TOKEN}/sendPhoto"
62 | data = {"parse_mode": "HTML", "chat_id": self.CHAT_ID, "caption": caption}
63 | photo = requests.get(photo_url)
64 | photo_stream = io.BytesIO(photo.content)
65 | photo_stream.name = "photo.jpg"
66 | files = {"photo": photo_stream}
67 | req = requests.post(url, data=data, files=files)
68 | response = json.loads(req.text)
69 | if req.status_code == 200:
70 | message_id = response["result"]["message_id"]
71 | return message_id
72 | else:
73 | logger.log_critical(f"Failed to send photo message: {response['description']}")
74 | return None
75 |
76 |
77 | telegram_sender = TelegramSender(Config)
78 |
79 |
80 | def instagram_to_telegram_callback(ch, method, properties, body):
81 | message = json.loads(body)
82 | message_id = None
83 | if message.get("id") is not None:
84 | redis_instagram_client.set(message["id"], 1)
85 | logger.log_info(f"Message ID: {message['id']} saved to Redis")
86 |
87 | if message.get("type") == "text":
88 | message_id = telegram_sender.send_text_message(message["text"])
89 | logger.log_info(f"Text message sent with ID: {message_id}")
90 |
91 | if message.get("type") == "reel":
92 | caption = f'{message["caption"]}\n\nLink'
93 | message_id = telegram_sender.send_video_message(
94 | message["download_link"], caption
95 | )
96 | logger.log_info(f"Reel message sent with ID: {message_id}")
97 |
98 | if message.get("type") == "post":
99 | caption = f'{message["caption"]}\n\nLink'
100 | message_id = telegram_sender.send_photo_message(
101 | message["download_link"], caption
102 | )
103 | logger.log_info(f"Post message sent with ID: {message_id}")
104 |
105 | if message.get("type") == "unknown":
106 | telegram_sender.send_text_message("Unknown message type")
107 | logger.log_info("Unknown message type sent to Telegram")
108 |
109 | if message_id is not None:
110 | redis_telegram_client.set(
111 | message_id,
112 | json.dumps(
113 | {"id": message["id"], "client_context": message["client_context"]}
114 | ),
115 | )
116 | logger.log_info(f"Message ID: {message_id} saved to Redis")
117 |
118 |
119 | def log_in_telegram_callback(ch, method, properties, body):
120 | message = body.decode("utf-8")
121 | telegram_sender.send_text_message(message, Config.TELEGRAM_LOG_CHAT_ID)
122 |
123 |
124 | rabbitmq_service = RabbitMQService(Config)
125 | rabbitmq_service.start_consuming(
126 | {
127 | "instagram_to_telegram": instagram_to_telegram_callback,
128 | "logs": log_in_telegram_callback,
129 | }
130 | )
131 | logger.log_info("Service started")
132 |
--------------------------------------------------------------------------------
/instagram_service/instagram_handler_service.py:
--------------------------------------------------------------------------------
1 | import json
2 | from instagrapi import Client
3 | from instagrapi.exceptions import LoginRequired
4 | from redis import Redis
5 | from functools import wraps
6 | import os
7 | from time import sleep
8 | import traceback
9 |
10 | from message_broker.rabbitmq_service import RabbitMQService
11 | from logger.log import Logger
12 | from utils.singleton import Singleton
13 |
14 | def handle_login_required(func):
15 | @wraps(func)
16 | def wrapper(self, *args, **kwargs):
17 | for i in range(3):
18 | try:
19 | return func(self, *args, **kwargs)
20 | except LoginRequired:
21 | self.logger.log_info("Session is invalid, need to login via username and password")
22 | self.logger.log_error(traceback.format_exc())
23 | if i == 1:
24 | self.logger.log_error("Couldn't login user with either password or session")
25 | self.logger.log_info("Waiting 3 minutes before trying to login again")
26 | sleep(3*60)
27 | self.login()
28 | self.logger.log_error("Couldn't login user with either password or session")
29 | raise Exception("Couldn't login user with either password or session")
30 |
31 | return wrapper
32 |
33 | class ReplyToMessage:
34 | def __init__(self, message_id, client_context):
35 | self.id = message_id
36 | self.client_context = client_context
37 |
38 | class InstagramService(metaclass=Singleton):
39 |
40 | def __init__(self, config):
41 | self.config = config
42 | self.logger = Logger("InstagramService")
43 | self.client = Client()
44 | self.client.delay_range = [1, 3]
45 | self.session_path = "config/session.json"
46 | self.thread_id_path = "config/thread_id.txt"
47 | self.login()
48 | self.INSTAGRAM_TARGET_USERNAME = config.INSTAGRAM_TARGET_USERNAME
49 | self.thread_id = self.load_thread_id()
50 | if not self.thread_id:
51 | self.thread_id = self.fetch_and_save_thread_id()
52 | self.redis_client = Redis(host=config.REDIS_HOST, port=config.REDIS_PORT, password=config.REDIS_PASSWORD, db=0)
53 | self.logger.log_info("Service started")
54 |
55 | def load_thread_id(self):
56 | try:
57 | with open(self.thread_id_path, 'r') as file:
58 | thread_id = file.read().strip()
59 | self.logger.log_info("Loaded thread_id from file.")
60 | return thread_id
61 | except FileNotFoundError:
62 | self.logger.log_info("Thread_id file not found. Will fetch from Instagram.")
63 | return None
64 |
65 | def fetch_and_save_thread_id(self):
66 | self.user_id = self.client.user_id_from_username(self.INSTAGRAM_TARGET_USERNAME)
67 | thread_id = self.client.direct_thread_by_participants([self.user_id])["thread"]["thread_id"]
68 | with open(self.thread_id_path, 'w') as file:
69 | file.write(thread_id)
70 | self.logger.log_info("Fetched and saved thread_id to file.")
71 | return thread_id
72 |
73 | def login(self):
74 | if os.path.exists(self.session_path):
75 | self.session = self.client.load_settings(self.session_path)
76 | else:
77 | self.session = None
78 |
79 | login_via_session = False
80 | login_via_pw = False
81 |
82 | if self.session:
83 | try:
84 | self.client.set_settings(self.session)
85 | self.client.login(self.config.INSTAGRAM_USERNAME, self.config.INSTAGRAM_PASSWORD)
86 |
87 | # check if session is valid
88 | try:
89 | self.client.get_timeline_feed()
90 | except LoginRequired:
91 | self.logger.log_info("Session is invalid, need to login via username and password")
92 |
93 | old_session = self.client.get_settings()
94 |
95 | # use the same device uuids across logins
96 | self.client.set_settings({})
97 | self.client.set_uuids(old_session["uuids"])
98 |
99 | self.client.login(self.config.INSTAGRAM_USERNAME, self.config.INSTAGRAM_PASSWORD)
100 | login_via_session = True
101 | except Exception as e:
102 | self.logger.log_info("Couldn't login user using session information: %s" % e)
103 |
104 | if not login_via_session:
105 | try:
106 | self.logger.log_info("Attempting to login via username and password. username: %s" % self.config.INSTAGRAM_USERNAME)
107 | if self.client.login(self.config.INSTAGRAM_USERNAME, self.config.INSTAGRAM_PASSWORD):
108 | login_via_pw = True
109 | except Exception as e:
110 | self.logger.log_info("Couldn't login user using username and password: %s" % e)
111 |
112 | if not login_via_pw and not login_via_session:
113 | self.logger.log_error("Couldn't login user with either password or session")
114 | raise Exception("Couldn't login user with either password or session")
115 |
116 | self.client.dump_settings(self.session_path)
117 | self.logger.log_info("Logged in successfully")
118 |
119 |
120 | @handle_login_required
121 | def listen(self):
122 | is_rabbitmq_connected = False
123 | self.logger.log_info("Listening for messages")
124 | messages = self.client.direct_messages(self.thread_id, amount=20)
125 | messages.reverse()
126 | for message in messages:
127 | if int(message.user_id) == int(self.client.user_id):
128 | continue
129 | if self.redis_client.get(message.id) is not None:
130 | continue
131 | if not is_rabbitmq_connected:
132 | rabbitmq_service = RabbitMQService(self.config)
133 | self.logger.log_info("Connected to RabbitMQ to send messages to Telegram")
134 | is_rabbitmq_connected = True
135 |
136 | if message.text is not None:
137 | text = message.text
138 | clean_message = {
139 | "id": message.id,
140 | "client_context": message.client_context,
141 | 'type': 'text',
142 | 'text': text
143 | }
144 | elif message.clip is not None:
145 | download_link = str(message.clip.video_url)
146 | caption = message.clip.caption_text
147 | link = "https://www.instagram.com/p/" + message.clip.code
148 | clean_message = {
149 | "id": message.id,
150 | "client_context": message.client_context,
151 | 'type': 'reel',
152 | 'download_link': download_link,
153 | 'caption': caption,
154 | 'link': link
155 | }
156 | elif message.xma_share is not None:
157 | download_link = str(message.xma_share.preview_url)
158 | caption = message.xma_share.title
159 | link = str(message.xma_share.video_url)
160 | clean_message = {
161 | "id": message.id,
162 | "client_context": message.client_context,
163 | 'type': 'post',
164 | 'download_link': download_link,
165 | 'caption': caption,
166 | 'link': link
167 | }
168 | else:
169 | clean_message = {
170 | 'type': 'unknown'
171 | }
172 |
173 | rabbitmq_service.send_message_instagram_to_telegram(json.dumps(clean_message))
174 | self.logger.log_info(f"Sent message: {clean_message}")
175 |
176 | if is_rabbitmq_connected:
177 | rabbitmq_service.close_connection()
178 | self.logger.log_info("Disconnected from RabbitMQ")
179 |
180 | @handle_login_required
181 | def reply_in_direct(self, text, reply_to_message):
182 | self.client.direct_send(text, thread_ids=[self.thread_id], reply_to_message=reply_to_message)
183 | self.logger.log_info(f"Replied to message: {text}")
--------------------------------------------------------------------------------