LinkedInのつながり申請をJavaScriptで自動化＆思わぬ落とし穴

こんにちは、大学生としてプログラミングで遊んでいるKantaです！
今回はLinkedInのつながり申請のJavaScriptでの自動化について記事を書きます。

##なぜLinkedIn？つながりを増やす意味は？

エンジニアのみなさんはLinkedInを有効活用していらっしゃいますよね。
業界のトップに位置する人の経験や経歴、スキルを分析することで、自分がどのような方向に進んでいくべきか、何を身に付けておくべきかを考えるきっかけになります。
また、業界のポジションの種類などの知識も自然と学ぶことができます。意外とつながり申請を受け入れてくれる人は多いです。
つながりの質も長期的には重要ですが、つながりの数はある程度多ければ多いほど、リクルーターからのメッセージも増え、大学生の私でも在宅ワークやインターンの募集を受け取ることは多いです。意外な出会いをもたらしてくれるSNS、それがLinkedInです。

ただ、繋がりを増やすために、知人に申請したり、憧れの企業にお勤めの方々に申請などを手動でポチポチするのは手間です。
また、日本の大学生の知人はそもそもそんなにLinkedInやってません・・。
その時間があれば、スクワットが何回できるでしょうか？魚を何匹捌けるでしょうか？

そんなことを考えていると、「リンクトインの繋がり申請をターゲットを定めて自動化できないか？」
という考えが浮かんでみました。それが今回の記事を書くきっかけです。

##「つながり申請の自動化」具体的な操作に分解してみる

Chromeの拡張機能Tampermonkeyを用いてブラウザ上でJavaScriptを実行していきます。
具体的は操作は以下です：

1. ユーザーの検索結果の画面の繋がり申請ボタンをクリックする。
1. 確認ウィンドウがでてきたら、送信ボタンをクリックする。
1. もし申請にメールアドレス認証などが必要であればキャンセルボタンを押す。
1. ページのどの部分のユーザーに申請したかを確認する。
1. もしページの最後であれば、次のページに向かう。

この1-5の流れを、繋がりボタンが存在する限り、ランダム間隔をおいてループで実行する。


##大まかな自動化の流れ
さて、目的を定めたら次はChromeの開発者ツールでサイトの調査をします。
右クリックで出てくるメニューから「検証」ボタンをクリックします。

<img width="257" alt="スクリーンショット 2020-05-24 18.26.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614389/064891b5-48d2-63aa-4e5e-29004bd042f8.png">

これがみなさん大好き、Chrome開発者ツールですね。
<img width="500" alt="スクリーンショット 2020-05-24 18.29.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614389/91262fb8-8a62-a835-5f84-0b2e773be606.png">
Chromeの開発者ツールで画像のような四角形にカーソルが載ったアイコンをクリックすると、特定の要素のコードなどが見れます。
<img width="81" alt="スクリーンショット 2020-05-24 18.32.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614389/59390253-3f86-cb2c-bc08-a73d515fc8a9.png">
このアイコンをクリックした後に、ページ上の部分をクリックすると、その部分のHTML上での場所など、様々な情報が見れます。

###要素の特定
さて、JavaScriptでDOMに変更を加えるには、目的の要素を一意に特定する必要があります。
目的の要素を一意に特定する手段としてid,class,属性があります。

下の画像のように、選択した要素のJS Pathをコピーして利用するという手もありますが、「あるタグの何番目の子要素」みたいな特定方法なので汎用性に欠けます。また、HTMLの構造の変更に影響を受けやすいです。特に自動生成されているサイトや、変更が著しいサイトではお勧めできません。
<img width="507" alt="スクリーンショット 2020-05-24 18.33.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614389/32542638-5515-9e74-8549-7f413f2ddfe2.png">

よって、一貫した要素の特定をするためには、id, class, タグの属性,クエリセレクタなどを用いるのが最適です。
WEBサイトの作成者も、CSSなどで装飾する際に、分かりやすい命名法をしてくれます。
そうした命名から、要素の機能を推定できます。

例えば、LinkedInのつながり申請ボタンの部分のHTML要素を覗いてみると次のようになっています。
（注意：一部省略してます。）

```html

<button data-control-name="srp_profile_actions">省略..</button>
```

属性がこのように与えられていると、サイトの設計者だけでなく、自動化ツールを作る我々も恩恵を受けることができます。
このようにして、classや属性のなかで、機能が推定しやすく変更の影響を受けにくいものを選んで、クエリセレクタでHTMLcollextionを取得します。
これはHTMLの要素の参照からなる配列のようなものです。

###特定した要素をJavaScriptで操作する（DOM操作）
要素が特定できたら、JavaScriptのおなじみのDOM操作をしていきましょう。
上の例のボタンを操作するためには、まずこうします。


```js

var sendInvitationElements=document.querySelectorAll('button[data-control-name="srp_profile_actions"]');
```

こうして各要素に対しておなじみのdocumentオブジェクトのメソッドやプロパティがつかえます。
例えば、上で取得した要素たちのうち、i番目の要素をクリックしたことにするには、

```js

sendInvitationElements[i].click();
```
のようなコードを書けばいいですね。このような感じでHTMLの一部の中身を取得したり、クリックしたり、ループしたり、条件分岐の処理を書いていけば案外すぐに自動化は可能です。
このようにして、自動化のスクリプトを書いていきましょう。



##完成したもの "LinkedInAutoConnector"

コード：
https://github.com/kantasv/LinkedInAutoConnector/blob/master/linkedInAutoConnector.js

これをChrome拡張機能のTampermonkeyに入れれば、"LinkedInAutoConnector"が実行できます。

まず、拡張機能を有効にした後、キーワードで検索し、その後"People"の欄をクリックすると繋がり申請が自動で始まります。
<img width="365" alt="スクリーンショット 2020-05-24 19.50.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614389/694fe003-2077-ab17-f22c-7fe312ea8746.png">
一応、「もう十分」となったときのために、画面左下にボタンを用意しています。
これをクリックすることで一時的につながり申請が止まります。
<img width="531" alt="スクリーンショット 2020-05-24 19.51.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614389/51da067f-a8ff-12a4-35eb-c6be2ad507d7.png">
これで、つながりを増やしまくるぜ！と怒涛のつながり申請をしまくると意気込んでいた筆者ですが、世の中はそんなに甘くありませんでした。
正確には覚えていませんが、二百人くらいはつながりが増えたところで、思わぬ落とし穴にはまりました。

##落とし穴：LinkedInで自動化ツールの利用は利用規約違反

###アカウント一時停止
ある朝、LinkedInにアクセスすると、ログインができません。
眠い目をこすりながら、メッセージを見ると、アカウントが一時的に停止されたとのメッセージに気付きました。
めちゃめちゃ朝から焦りました。
本人確認が必要とのことで、カスタマーサポートとやりとりを行いました。

###LinkedInの利用規約には・・・
アカウントが停止された背景について詳しく調べてみると・・・
次のページに該当するように、LinkedInでの各種自動化ツールの利用は利用規約違反だったのです。
https://www.linkedin.com/help/linkedin/answer/60453/-?lang=ja
>"ボットやその他の自動化された方法を使用して、弊社のサービスしたり、連絡先を追加/ダウンロードしたり、メッセージを送信またはリダイレクトすること"
この部分が該当していました。

###本人確認によりアカウント復活
自分のパスポートの画像を送信することで、アカウントは復活しました。
<img width="679" alt="スクリーンショット 2020-05-24 20.02.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614389/2c9f16fe-b62a-d9e1-ffff-b75650ea258a.png">
このような出来事は人生で初めてだったので、正直焦りました。。。

##まとめ
自動化ツールは便利なので、自粛期間中にみなさんも量産してみてはいかがでしょうか。
ただ、そうしたツールの利用を許可しないWEBサイトも少なからず存在します。
また、こうしたツールは一歩間違えればDDoS Attackのようにサーバーに負荷をかけてしまうことも考えれます。
たとえ自動操作の間隔をランダムにしたり、回りくどい回避策をとったとして、いつかは対策されますし、ばれます。
節度をもって、自動化で毎日をより充実させることができるといいですね。
それでは、ここまで読んでいただきありがとうございました。

コメント欄に、あなたが最近した自動化、これからしたい自動化、あったらいいなこんな自動化など書き込んでください！
楽しみにしています！



どのようにしたら自動化できるかということに「純粋な知的好奇心」があるみなさんに最後にソースコードをここにも残しておきます。
GitHubにも挙げてます。
https://github.com/kantasv/LinkedInAutoConnector/blob/master/linkedInAutoConnector.js

```js
| (function () { |
|:--|
|     'use strict'; |
|  |
|     var pageIndex; |
|     var setIntervalId; |
|  |
|     var isCuurentPageSearchResultPage = () => { |
|         return window.location.href.toLocaleLowerCase().indexOf('search/results/people/') != 1 |
|     } |
|  |
|  |
|     //detects page result index when current page is clearly the people search result page |
|     if (isCuurentPageSearchResultPage()) { |
|         window.location.href.split('&').forEach(elm => { |
|             if (elm.indexOf('page=') != -1) { |
|                 var start = 'page='.length |
|                 pageIndex = parseInt(elm.slice(start)) |
|                 console.log('pageIndex', pageIndex) |
|             } |
|         }) |
|         //when could not "page=" but clearly the current page is clearly the people search result page |
|         if (!pageIndex) { |
|             window.location.href += '&page=1' |
|         } |
|  |
|     } |
|  |
|     var initAutoConnector = () => { |
|         var connectButtonCandidates = document.querySelectorAll('button[data-control-name="srp_profile_actions"]'); |
|         var connectButtons = []; |
|  |
|         //filter buttons: get only connect buttons |
|         for (var i = 0; i < connectButtonCandidates.length; i++) { |
|             //console.log('Button condition', connectButtonCandidates[i].innerText) |
|             if (connectButtonCandidates[i].innerText == 'Connect') { connectButtons.push(connectButtonCandidates[i]) }; |
|         } |
|  |
|  |
|         //gets random integers ranged from 0 to 300 |
|         //by changing intervals, LinkedIn is not likely to detect this sort of automation |
|         var getRandomInteger = () => { |
|             var min = 0; |
|             var max = 300; |
|             return Math.floor(Math.random() * (max + 1 - min)) + min; |
|         } |
|  |
|         var connectButtonCount = 0 |
|  |
|         var connectButtonOperation = () => { |
|  |
|             if (connectButtonCount < connectButtons.length) { |
|  |
|                 //clicks "Connect button" |
|                 connectButtons[connectButtonCount].click() |
|                 //clicks "Send Invitation" button |
|                 setTimeout(() => { |
|                     //clicks only when "Send Invitation" exists |
|                     var sendInvitationElement = document.querySelector('button[aria-label="Send invitation"]') |
|                     var verificationOperationDismissElement = document.querySelector('button[aria-label="Dismiss"]') |
|                     if (sendInvitationElement) { |
|                         console.log('sendInvitationElement exists', sendInvitationElement) |
|                         sendInvitationElement.click() |
|                     } else if (verificationOperationDismissElement) { |
|                         console.log(verificationOperationDismissElement) |
|                         //cancels when you face "verify" dialog |
|                         verificationOperationDismissElement.click() |
|                     } else { |
|                         console.log('Unexpected error. Could not find button elements for operations.') |
|                     } |
|                     connectButtonCount++ |
|                 }, getRandomInteger()) |
|                  |
|  |
|             } else { |
|                 console.log('already clicked all connect buttons') |
|                 clearInterval(setIntervalId) |
|                 //when no connect buttons available |
|                 //then move to next result page |
|                 var currentUrl = window.location.href |
|                 console.log(`page=${pageIndex}`, '->', `page=${pageIndex + 1}`) |
|                 var nextPageUrl = currentUrl.replace(`page=${pageIndex}`, `page=${pageIndex + 1}`) |
|                 //console.log(currentUrl.slice(50),nextPageUrl.slice(50)) |
|                 setTimeout(() => { window.location.href = nextPageUrl }, 1000) |
|  |
|             } |
|  |
|         } |
|  |
|         setIntervalId = setInterval(connectButtonOperation, 1000 + getRandomInteger()) |
|  |
|     } |
|  |
|  |
|     //checks if the current page is the search result page |
|     if (isCuurentPageSearchResultPage()) { |
|  |
|         console.log('Found "Connect" buttons. Startsing automatically conectiong...') |
|         window.onload = () => { initAutoConnector() } |
|  |
|     } else { |
|         console.log('Could not find "Connect" buttons. This page may not be search result page.') |
|     } |
|  |
|  |
|     //adds Autoconnector bar element to DOM |
|     var initACBar = () => { |
|  |
|         var autoconnectStopButton = document.createElement('div'); |
|         autoconnectStopButton.innerHTML = `<span id='ACstatus'>Sending invitations automatically...</span><p id='ACstopButton'>Click here to stop LinkedIn Autoconector temporalily</p> |
|         <h6>LinkedIn Autoconnector - Powered by Kanta Yamaoka.</h6>` |
|  |
|  |
|         var css = (prop, value) => { |
|             autoconnectStopButton.style[prop] = value |
|         } |
|  |
|         css('width', '30%') |
|         css('height', '100px') |
|         css('backgroundColor', 'white') |
|         css('color', '#0178B5') |
|         css('border', '2px solid #0178B5') |
|         css('borderRadius', '10px') |
|         css('textAlign', 'center') |
|         css('textHeight', '10px') |
|         css('position', 'fixed') |
|         css('bottom', '10%') |
|         css('left', '10%') |
|         css('zIndex', '10000') |
|  |
|  |
|  |
|         document.body.appendChild(autoconnectStopButton) |
|         document.getElementById('ACstopButton').style.margin = '10px' |
|  |
|         autoconnectStopButton.onclick = () => { |
|             clearInterval(setIntervalId) |
|             css('backgroundColor', '#c8c8c8') |
|             document.getElementById('ACstatus').innerText = 'Autoconnector temporalily disabled.' |
|             document.getElementById('ACstopButton').innerText = 'To use Autoconecttor again, please refresh the page.' |
|  |
|         } |
|  |
|  |
|     } |
|  |
|     initACBar() |
|  |
| })(); |

```