# 自動化將Instagram動態轉發至Twitter
## 專案說明
利用AWS Lambda觸發,設定每分鐘透過IG API到指定帳號檢測是否有新貼文/動態,如果有,則抓取其media url,將該圖片/影片發佈到Twitter的Media API後,獲得該media的media id,接著,發佈到Twitter帳號中。
## 架構圖

流程比較麻煩的點有兩部分,各是ig端以及twitter端
1. instagram提供的graph api沒辦法直接取得別人帳號的貼文內容(像是圖片、影片網址),因此需要透過徒法煉鋼方式,模擬登入、拿TOKEN,然後再透過另外的API去取回多媒體網址。
2. twitter上傳多媒體檔案需要多個步驟,第一步是要將多媒體檔傳到一個額外的media api server中,取得media id,而在上傳到media api server的過程中又有三步驟要做驗證,在結束前面的程序後,call twitter api發布貼文的指令,並附上前面得到的response: media id。
## 開發講解
**1. 登入並取得IG的csrftoken, sessionid, ds_user_id**
在這個階段中,必須準備一個實際的IG帳號來做Login行為
登入後取得csrftoken, sessionid, ds_user_id三個資訊以利後續操作
原因是IG的官方API裡,並沒有辦法直接用API讓你存取他人的貼文
官方API只能用來操作/查看自己帳號的內容
```python=
def getLoginConfig(self):
success = False
for i in range(0,5):
try:
print(f"try to get login config #{i}...")
with requests.Session() as s:
r = s.get(self.link)
csrf = re.findall(r"csrf_token\":\"(.*?)\"", r.text)[0]
r = s.post(
self.login_url,
data=self.getLoginPayload(),
headers={
"User-Agent":
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36",
"X-Requested-With": "XMLHttpRequest",
"Referer": "https://www.instagram.com/accounts/login/",
"x-csrftoken": csrf
})
if(r.status_code==429):
print('[',r.status_code,'] Too Many Reqeusts!')
sys.exit(1)
self.setLoginConfig(
s.cookies['csrftoken'], s.cookies['sessionid'], s.cookies['ds_user_id'])
self.mycookies = {'ds_user_id': self.ds_user_id,
'sessionid': self.sessionid,
'csrftoken': self.csrftoken}
success = True
except Exception as e:
print('failed with:',e)
time.sleep(1)
continue
break
if(success):
print('login config success.')
else:
print('login config fail.')
sys.exit(1)
```
> 這邊特別要提醒的是 要小心不要在同一小時裡面打太多reuqest
> instagram api有防範機制 過多請求將會鎖住IP
> 就會得到response code 429: Too Many Requests
**2. 取得限時動態資訊**
由"https://i.instagram.com/api/v1/feed/user/{cookies}"這個url做GET
在發REQUEST的時候,header記得放入
```python=
{
'User-Agent': 'Instagram 10.3.2 (iPhone7,2; iPhone OS 9_3_3; en_US; en-US; scale=2.00; 750x1334) AppleWebKit/420+'
}
```
以及剛剛拿到的csrftoken, sessionid, ds_user_id放入cookies中
**3. 拆解限時動態資訊(JSON)**
由剛剛的HTTP GET得到的response 架構如以下

而主要的內容都在items之中
items中將會有數個element,每個element都代表一個現在這個使用者有的限時動態

舉例如以下
這個帳號目前可供觀看的限時動態有九個,因此上面得到的element也是0~8沒錯

而各item中內的架構如以下,這邊稍微講一些關鍵的attribute就好
* taken_at:指這個限時動態發布的時間(可透過time.localtime轉為datetime型態)
* video_versions:可以用這個屬性推估,如果存在這屬性,表示這則限時動態為影片
* image_versions2:如果該貼文為圖片,圖片的url可以在此找到

這邊先以圖片的限時動態為講解內容
打開image version可以看到兩個element
這邊的兩個element其實都代表一樣的資訊
只是#0是高畫質 #1畫質較差罷了
因此我們都直接拿#0的url即可

程式部分,tm這邊是要用來預留,之後可以給時間limit選擇要留下來的限時動態
(假設Lambda每分鐘call,就判斷此限時貼文是否為前一中所發出的,是則觸發)
```python=
def getInsStory(self,target_username):
# get user id by username with ?__a=1 parameter
try:
acc_info = requests.get(f"https://www.instagram.com/{target_username}/?__a=1",
cookies=self.mycookies, headers=self.myheaders).json()
user_id = acc_info['graphql']['user']['id']
print('username:',target_username,' user_id:',user_id)
except requests.exceptions.RequestException as e:
print('no such user!')
raise SystemExit(e)
# get all reel medias by insta api
try:
item = requests.get("https://i.instagram.com/api/v1/feed/user/" + str(
user_id) + "/reel_media/", cookies=self.mycookies, headers=self.myheaders).json()
print("-------------------------------")
print(f"successfully get {len(item)} stories.")
print("-------------------------------")
except requests.exceptions.RequestException as e:
raise SystemExit(e)
for m in item['items']:
tm = time.strftime('%Y-%m-%d %H:%M:%S',
time.localtime(m['taken_at']))
# now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# if now-timedelta(minutes=15)<tm:
if 'video_versions' in m:
self.videos.append([m['video_versions'][0]['url'], tm])
else:
self.images.append([m['image_versions2']
['candidates'][0]['url'], tm])
```
**4. 登入Twitter API驗證**
這邊採用了函式庫Twython
https://twython.readthedocs.io/en/latest/
其實這個函式庫也只是幫你把大部分twitter official的一些步驟寫好
比較簡略 容易開發而已
```python=
class Twitter(object):
def __init__(self, CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET) -> None:
print(f"initilize and verify twitter object....")
self.twitter = Twython(CONSUMER_KEY, CONSUMER_SECRET,
ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
self.twitter.verify_credentials()
```
**5. 將IG的多媒體檔由Twitter Media Upload API上傳**
這邊的步驟跟一些官方教學的步驟比較不同
大部分的教學都是去打開local的圖片文件
但這邊因為我們使用的是url的圖片
所以透過request打get到url圖片
然後再轉用BytesIO接
就可以達到一樣的效果
```python=
# Image
def uploadImg(self):
img = ins.getImages()
if not img:
print('there is no images.')
return None
mids = []
for url in img:
try:
r = requests.get(url[0])
photo = BytesIO(r.content)
mids.append([self.twitter.upload_media(media=photo),url[1]])
print(f" - successfully uploaded one photo to twitter media server.")
except requests.exceptions.RequestException as e:
raise SystemExit(e)
return mids
```
在影片上傳這部分其實比起圖片複雜許多
> Upload happens in 3 stages:
- INIT call with size of media to be uploaded(in bytes). If this is more than 15mb, twitter will return error.
- APPEND calls each with media chunk. This returns a 204(No Content) if chunk is received.
- FINALIZE call to complete media upload. This returns media_id to be used with status update.
https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload
基本上他是有三個步驟要做: INIT, APPEND, FINALIZE
有興趣可以自己去看document
但這邊因為我們直接用了函式庫就不用特別自己寫
```python=
# Video
def uploadVid(self):
vids = ins.getVideos()
if not vids:
print('there is no videos.')
return None
mids = []
for url in vids:
r = requests.get(url[0])
video = BytesIO(r.content)
mids.append([self.twitter.upload_video(media=video, media_type='video/mp4',media_category='amplify_video', check_progress=True),url[1]])
print(f" - successfully uploaded one video to twitter media server.")
return mids
```
特別要注意的是twitter.upload_video參數
media_category這邊要用對的型態 才會上傳成功
> The category that represents how the media will be used. This field is required when using the media with the Ads APIPossible values: amplify_video, tweet_gif, tweet_image, and tweet_video
**6. 由Twitter Media Upload API得到的media id發布至Twitter中**
```python=
def uploadStatus(self, ids, type):
print(f"========= start uploading status with one {type} attachment to twitter =========")
for id in ids:
self.twitter.update_status(
status=f'{id[1]} story uploaded.',
media_ids=[id[0]['media_id']]
)
print(f" | {id[0]['media_id']} successfully uploaded.")
```