commit a93a8d736c0ea371e7c18f16d4121478743ff219 Author: unknown Date: Sat Oct 26 00:31:27 2019 +0700 init diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..78e8c90 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +OSHI_USERNAME= + +INSTAGRAM_USER= +INSTAGRAM_PASS= + +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +BOT_TOKEN= +CHAT_ID= + +TELEGRAM_URL=https://api.telegram.org/bot \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f908269 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ + +*.json + +__pycache__/ + +stories/ + +*.pyc + +users\.txt + +\.idea/ + +\.vscode/\.ropeproject/ + +.env +Env/ +venv/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3bbee3f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3-alpine + + +RUN apk update && apk upgrade && pip install -U pip + +COPY . /app +WORKDIR /app +RUN pip --no-cache-dir install -r requirements.txt + +RUN crontab /app/crontab.txt +RUN touch /var/log/cron.log + +RUN ln -snf /usr/share/zoneinfo/Asia/Jakarta /etc/localtime && echo "Asia/Jakarta" > /etc/timezone + +CMD crond && tail -f /var/log/cron.log \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec8218f --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# OshiStories +Telegram bot to download your idol's latest media stories and posts + +### Requirements +* [Python 3](https://www.python.org/) +* [Redis](https://redis.io) +* [Docker](https://docker.com) + +### Settings before run +1. rename .env.example to .env +2. Add instagram username to OSHI_USERNAME with comma separated + +### How To Create & Get BOT Token +[BotFather](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token) + +### How To Get CHAT ID +1. Add bot as your friend & start chat +2. Open +``` +https://api.telegram.org/bot/getUpdates +``` +![alt text](./screenshot-get-chat_id.png) + +### Build Docker Images +```bash +$ docker build -t oshi-stories . +``` + +### Run Docker Container +```bash +$ docker run -it --rm --name oshi-stories-container -d oshi-stories +``` \ No newline at end of file diff --git a/crontab.txt b/crontab.txt new file mode 100644 index 0000000..5f8ad09 --- /dev/null +++ b/crontab.txt @@ -0,0 +1 @@ +* * * * * python3 /app/run.py story feed > /dev/null diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..497ab87 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +certifi==2019.6.16 +chardet==3.0.4 +idna==2.8 +instagram-private-api==1.6.0 +python-dateutil==2.8.0 +python-dotenv==0.10.3 +redis==3.3.8 +requests==2.22.0 +six==1.12.0 +urllib3==1.25.3 diff --git a/run.py b/run.py new file mode 100644 index 0000000..efb27f5 --- /dev/null +++ b/run.py @@ -0,0 +1,268 @@ +import argparse +import codecs +import datetime +import json +import os +import sys +import time +import subprocess + +from dotenv import load_dotenv +from xml.dom.minidom import parseString +import urllib as urllib +import send_telegram +import time +# try: +# import urllib.request as urllib +# except ImportError: + +try: + from instagram_private_api import ( + Client, ClientError, ClientLoginError, + ClientCookieExpiredError, ClientLoginRequiredError, + __version__ as client_version) +except ImportError: + import sys + + sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + from instagram_private_api import ( + Client, ClientError, ClientLoginError, + ClientCookieExpiredError, ClientLoginRequiredError, + __version__ as client_version) + +from instagram_private_api import ClientError +from instagram_private_api import Client +import redis + + +def to_json(python_object): + if isinstance(python_object, bytes): + return {'__class__': 'bytes', + '__value__': codecs.encode(python_object, 'base64').decode()} + raise TypeError(repr(python_object) + ' is not JSON serializable') + + +def from_json(json_object): + if '__class__' in json_object and json_object.get('__class__') == 'bytes': + return codecs.decode(json_object.get('__value__').encode(), 'base64') + return json_object + + +def onlogin_callback(api, setting_name): + cache_settings = api.settings + redis_client.setex(setting_name, 7776000000, json.dumps(cache_settings, default=to_json)) + print('[+] New auth cookie file was made: {0!s}'.format(setting_name)) + +def login(username, password): + device_id = None + try: + settings_file = "credentials.json" + credential = redis_client.get('credentials') + if credential is None: + api = Client( + username, password, + on_login=lambda x: onlogin_callback(x, 'credentials')) + else: + cached_settings = json.loads(credential, object_hook=from_json) + device_id = cached_settings.get('device_id') + # reuse auth settings + api = Client( + username, password, + settings=cached_settings) + print('[+] Using cached login cookie for "' + api.authenticated_user_name + '".') + except (ClientCookieExpiredError, ClientLoginRequiredError) as e: + print('ClientCookieExpiredError/ClientLoginRequiredError: {0!s}'.format(e)) + + # Login expired + # Do relogin but use default ua, keys and such + api = Client( + username, password, + device_id=device_id, + on_login=lambda x: onlogin_callback(x, settings_file)) + + except ClientLoginError as e: + print('[!] Could not login: {:s}.\n[!] {:s}\n\n{:s}'.format( + json.loads(e.error_response).get("error_title", "Error title not available."), + json.loads(e.error_response).get("message", "Not available"), e.error_response)) + print('-' * 70) + sys.exit(9) + except ClientError as e: + print('[!] Client Error: {:s}'.format(e.error_response)) + print('-' * 70) + sys.exit(9) + except Exception as e: + if str(e).startswith("unsupported pickle protocol"): + print("[W] This cookie file is not compatible with Python {}.".format(sys.version.split(' ')[0][0])) + print("[W] Please delete your cookie file 'credentials.json' and try again.") + else: + print('[!] Unexpected Exception: {0!s}'.format(e)) + print('-' * 70) + sys.exit(99) + + print('[+] Login to "' + api.authenticated_user_name + '" OK!') + cookie_expiry = api.cookie_jar.auth_expires + print('[+] Login cookie expiry date: {0!s}'.format( + datetime.datetime.fromtimestamp(cookie_expiry).strftime('%Y-%m-%d at %I:%M:%S %p'))) + + return api + +def like_post(post_id): + like = ig_client.post_like(post_id) + return like + +def latest_post(username, user_id): + today = datetime.date.today() + # today = "2019-09-13" + t = time.mktime(datetime.datetime.strptime(str(today), "%Y-%m-%d").timetuple()) + t = int(t) + feed = ig_client.user_feed(user_id, min_timestamp=str(t)) + f = open("feeds.json", "w") + f.write(json.dumps(feed)) + f.close() + + for post in feed['items']: + list_media = [] + # print() + post_id = post['id'] + pk = post['code'] + k = '{}:{}:{}'.format("post", username, pk) + exist = redis_client.exists(k) + if exist == 1: + print(pk + " Exist") + continue + media_type = "photo" if post['media_type'] == 1 else "video" + if "carousel_media" in post: + carousel = True + for i, cs in enumerate(post['carousel_media'], start=1): + carousel_media_type = "photo" if cs['media_type'] == 1 else "video" + if carousel_media_type == 'photo': + list_media.append({ + "caption":"Photo #{}".format(i), + "type":"photo", + "media":cs['image_versions2']['candidates'][0]['url'] + }) + else: + list_media.append({ + "caption":"Video #{}".format(i), + "type":"video", + "media":cs['video_versions'][0]['url'] + }) + else: + carousel = False + if media_type == 'photo': + url_media = post['image_versions2']['candidates'][0]['url'] + else: + url_media = post['video_versions'][0]['url'] + post_url = "https://instagram.com/p/{}".format(pk) + caption = "[POST] from {}\n\n{}\n\nLink: {}".format(username, post['caption']['text'], post_url) + if len(caption) > 1000: + caption = caption[:1000] + '...' + + like = like_post(post_id) + + print(pk + " Sending...") + if carousel is True: + send_telegram.send_media_group(caption=caption, media=list_media) + else: + send_telegram.telegram_bot_send_media(fileType=media_type, url=url_media, caption=caption) + print(pk + " OK") + redis_client.setex(k, 86400, "True") + return + +def latest_stories(username, user_id): + feed = ig_client.user_story_feed(user_id) + if feed['reel'] is None: + print('No Update') + # exit(9) + return False + taken_at = True + feed_json = feed['reel']['items'] + + list_video = [] + list_image = [] + + for media in feed_json: + if not taken_at: + taken_ts = None + else: + if media.get('imported_taken_at'): + taken_ts = datetime.datetime.utcfromtimestamp(media.get('imported_taken_at', "")).strftime( + '%Y-%m-%d %H:%M:%S') + else: + taken_ts = datetime.datetime.utcfromtimestamp(media.get('taken_at', "")).strftime( + '%Y-%m-%d %H:%M:%S') + + is_video = 'video_versions' in media and 'image_versions2' in media + pk = media['code'] + k = '{}:{}:{}:{}'.format("story", username, "video" if is_video else "photo", pk) + exist = redis_client.exists(k) + + if exist == 0: + print('{} Sending...'.format(pk)) + caption = "[STORY] from {}" + if is_video: + data_media = { + 'url': media['video_versions'][0]['url'], + 'taken': taken_ts + } + # caption = "[VIDEO] from {}" + list_video.append(data_media) + send_telegram.telegram_bot_send_media(fileType='video', url=data_media['url'], caption=caption.format(username)) + else: + data_media = { + 'url': media['image_versions2']['candidates'][0]['url'], + 'taken': taken_ts + } + # caption = "[PHOTO] from {}" + list_image.append(data_media) + send_telegram.telegram_bot_send_media(fileType='photo', url=data_media['url'], caption=caption.format(username)) + print('{} OK'.format(pk)) + redis_client.setex(k, 86400, json.dumps(data_media)) + else: + print(pk + ' Exists') + return feed + +def download_user(user, attempt=0): + try: + if not user.isdigit(): + user_res = ig_client.username_info(user) + user_id = user_res['user']['pk'] + else: + user_id = user + user_info = ig_client.user_info(user_id) + if not user_info.get("user", None): + raise Exception("No user is associated with the given user id.") + else: + user = user_info.get("user").get("username") + if "feed" in sys.argv: + print("[=] Fetch POST [=]") + latest_post(user, user_id) + if "story" in sys.argv: + print("[=] Fetch STORY [=]") + latest_stories(user, user_id) + + # print(user) + return user + except Exception as e: + print(e) + +def start(): + oshi = os.getenv('OSHI_USERNAME') + oshi = oshi.split(',') + print('------------------------------') + for o in oshi: + print("Oshi: "+ o) + download_user(o) + +if __name__ == '__main__': + load_dotenv() + try: + redis_client = redis.Redis(host=os.getenv('REDIS_HOST'), port=os.getenv('REDIS_PORT'), db=os.getenv('REDIS_DB'), password=os.getenv('REDIS_PASSWORD')) + print("Connection to redis has been established...") + except Exception as e: + print("Cannot connect to redis...") + print(e) + username = os.getenv('INSTAGRAM_USER') + password = os.getenv('INSTAGRAM_PASS') + ig_client = login(username, password) + start() diff --git a/screenshot-get-chat_id.png b/screenshot-get-chat_id.png new file mode 100644 index 0000000..d7bc21b Binary files /dev/null and b/screenshot-get-chat_id.png differ diff --git a/send_telegram.py b/send_telegram.py new file mode 100644 index 0000000..70e78a2 --- /dev/null +++ b/send_telegram.py @@ -0,0 +1,77 @@ +import os +import time +import requests +from dotenv import load_dotenv +import json + +def telegram_bot_sendtext(bot_message): + send_text = TELEGRAM_URL + BOT_TOKEN + '/sendMessage?chat_id={}&text={}'.format(CHAT_ID,bot_message) + response = requests.get(send_text) + response_json = response.json() + return response.json() + +def send_media_group(**kwargs): + caption = kwargs.get('caption') + media = kwargs.get('media') + carousel = kwargs.get('carousel', None) + + send_text = telegram_bot_sendtext(caption); + if send_text['ok'] is not True: + print('Stop') + return + else: + message_id = send_text.get('result')['message_id'] + + data = { + 'chat_id' : CHAT_ID, + "reply_to_message_id":message_id, + "media": json.dumps(media) + } + send_url = TELEGRAM_URL + '{}{}'.format(BOT_TOKEN, '/sendMediaGroup') + response = requests.post(send_url, data=data) + response_json = response.json() + if response_json.get('ok') is True: + print('DONE') + return response_json + +def telegram_bot_send_media(**kwargs): + fileType = kwargs.get('fileType') + url = kwargs.get('url', None) + caption = kwargs.get('caption') + reply_first = kwargs.get('reply_first', False) + reply_id = kwargs.get('reply_id', None) + data = { + 'chat_id' : CHAT_ID, + "caption": caption, + } + if reply_first is True: + send_text = telegram_bot_sendtext(caption); + if send_text['ok'] is not True: + print('Stop') + return + data.update({"reply_to_message_id": send_text.get('result')['message_id']}) + if reply_id is not None: data.update({"reply_to_message_id": reply_id}) + if fileType == 'video': + data.update({ + 'video': url + }) + endpoint = "/sendVideo" + elif fileType == 'photo': + data.update({ + 'photo': url + }) + endpoint = "/sendPhoto" + else: + print("Failed") + return False + + send_url = TELEGRAM_URL + '{}{}'.format(BOT_TOKEN, endpoint) + response = requests.post(send_url,data=data) + return response.json() + + +if __name__ == 'send_telegram': + load_dotenv() + BOT_TOKEN = os.getenv('BOT_TOKEN') + CHAT_ID = os.getenv('CHAT_ID') + TELEGRAM_URL = os.getenv('TELEGRAM_URL') \ No newline at end of file