【Twitter改めX】詐欺アカウント自動ブロックツール作成

↓この記事で問題が解決し、ある程度動くものが出来たので公開します。

https://qiita.com/nobukz/questions/481c20d2f3f96727bceb

## 【このツールを使用するメリット】
何といっても売りは簡単で速い＆完全無料という点です。しかも2023年7月現在、猛威を振るっている「勝手に自分を卑猥なリストに追加する迷惑詐欺アカ」をブロックし、リストからも削除する事が出来ます。

:::note warn
また、見落としがちなメリットですが、このツールが普及し皆さんが短期スパンでブロックするようになれば、詐欺アカの凍結が劇的に速くなります。（複数からブロックされると、システム側が不審なアカウントと判断するため）
:::

https://twitter.com/Nobukz_bot/status/1685187691359686656?s=20

## 【開発の背景】 
2023年6月頃から、詐欺アカからのフォローが急増。7月からは新たな嫌がらせ手法として、自分を勝手に卑猥なタイトルのリストに登録する詐欺垢が多発しました。

## 【A:開発前から懸念された問題】 
1. TwitterAPIが有料、しかもボッタクリ価格(月額100ドル:現在約14,000円）となり、無料アカウントでAPI使用は敷居が高くなった
2. スクレイピングに対する規制が厳しくなり、botが凍結される事案が多くなってきた
3. 開発に時間が掛かるとTwitterの仕様が変更される可能性が高く、実用性が低くなる
4. サーバーに設置する仕様では、ハッキングによる使用者のIDとパスワード漏洩やアカウント乗っ取りのリスクがあり、責任を負いきれない
5. どのPCでも単体で実行出来るファイル形式にするには C#やVB.Netなどでコード作成しコンパイルするのが一般的だが、私のスキルでは短期間の制作は難しい

## 【B:開発中に発覚した問題】
1. 自分を勝手にリストへ登録する詐欺アカをtweepy経由でブロックしても、リストから自分が消えないバグがある（Twitter側の不具合） 
2. どの環境でも単体ファイルで実行出来る形式に変換する pyinstaller で上手く変換できない

## 【C:現時点の状況】
1. 自分のTwitterIDとパスワード、検索するフォロアー数を入力してチェックし、検出した詐欺アカを確認の上、一括ブロック出来るようになった 
2. ブロック処理はtweepyのreport_spamを使用しているため、通報＆ブロックしている 
3. 登録した判定ワード以外でも、「フォロアー:10未満／ツイート:5未満／いいね:5未満」は詐欺アカと判定
4. A1～2の問題について、get_cookie_apiを使用してTwitterAPIを不使用（詳細割愛：お察しください）
5. B1の問題は、playwright を使用して実現（詳細割愛：お察しください）
6. tkinter でユーザーインターフェイスを作成（定期自動実行は今後検討）

---
## 【ソースコード】 

```python:blockbot.py
#■■ blockbot Twitterの詐欺アカウント自動ブロックツール ■■ 2023.7.15 Nobukz
# ver.1.0 API不要のcookie仕様
# ver.2.0 tkinterにてUI実装
# ver.3.0 複数アカウントの切替対応 
# ver.4.0 自分を勝手にリストへ登録する詐欺アカをブロック
# 　※通常blockだとリストから削除されないため、playwrightでブラウザ操作をエミュレート
# ver.5.0 詐欺アカ判定用ワードを外部ファイルへ分離

import utils
import tweepy
import time
import tkinter
from tkinter import *
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
import tkinter as tk
from tkinter import scrolledtext
from playwright.sync_api import Playwright, sync_playwright, expect

num_listing_followers = 200
csvfile = 'block_words.csv'

hit_count = 0
block_users = []
block_IDs = []
block_lists = []
block_list_makers = []
block_list_IDs = []
block_group = []
block_border = []
group_max = []
block_words =[list(range(99)),list(range(99))]
word_check = []

user_ID = []
password = []
#CSVファイル詠み込み
row_count=0
word_count=0
group_count=0
mem_group=0

lst = [line.rstrip().split(",") for line in open(csvfile, newline='\n',encoding='utf-8').readlines()]
for row in lst:
    if row_count ==0:
        block_group.append(int(row[0])-1)
        block_border.append(int(row[1])-1)
    if int(row[0])-1 != mem_group:
        group_max.append(word_count)
        group_count=group_count+1
        block_group.append(int(row[0])-1)
        block_border.append(int(row[1])-1)
        mem_group=int(row[0])-1
        word_count=0
    
    print("group["+str(group_count)+"]:"+str(block_group[group_count])+"/border:"+str(block_border[group_count]))
    block_words[group_count][word_count] = str(row[2])
    print("■ブロックword["+str(group_count)+","+str(word_count)+"]:"+"/"+str(row[2]))   
    word_count=word_count+1
    row_count=row_count+1
group_max.append(word_count)

# クリックイベント 
def btn_clear():
    hit_count = 0
    block_users.clear()
    block_IDs.clear()
    block_lists.clear()
    block_list_IDs.clear()
    block_list_makers.clear()

    user_ID.clear()
    password.clear()
    txt_1.delete(0, tkinter.END)
    txt_2.delete(0, tkinter.END)
    txt_3.delete(0, tkinter.END)
    txt_1.focus_set()

#チェックボタン処理
def btn_check():
    #messagebox.showinfo("確認", "チェックを開始します")
    # テキスト取得
    hit_count = 0
    textBox.delete("1.0", END)
    textBox.update()

    user_ID.append(txt_1.get())
    password.append(txt_2.get())

    if txt_3.get() == '':
        messagebox.showinfo("【エラー】", "全てのボックスに入力して下さい")
        return()
    max_count = int(txt_3.get())
    if user_ID == "" or password == "" or max_count < 1:
        messagebox.showinfo("【エラー】", "全てのボックスに入力して下さい")
        return()
    api = utils.get_cookie_api(user_ID[0],password[0])

    followers = []

    for page in tweepy.Cursor(api.get_followers, screen_name=user_ID[0],count=num_listing_followers).pages():
        followers += [user._json for user in page]
        for i in range(len(page)):
            judge=0
            word_check.clear()
            print("■■■■ フォロワー:" + str(page[i].screen_name)) 
            chk_count=0
            for chk_group in range(len(block_group)):
                print("■group_max:"+str(group_max[chk_count]))
                chk_count2=0
                for chk in range(group_max[chk_count]):
                    print("■ word:"+str(block_words[chk_count][chk_count2]))
                    if str(page[i].description).rfind(block_words[chk_count][chk_count2]) > 0:
                        word_check.append(1)
                        print("■ Hit:"+str(block_words[chk_count][chk_count2]))
                    chk_count2=chk_count2+1
                chk_count=chk_count+1
            print("■ Hit:"+str(sum(word_check))+"/"+str(block_border[chk_count-1]))
            if sum(word_check) > block_border[chk_count-1]:
                judge=1
            chk1 = page[i].followers_count
            chk2 = page[i].statuses_count
            chk3 = page[i].favourites_count

            if chk1 < 10 and chk2 < 5 and chk3 < 5:
                judge=1
            result = "Flw-Tw-Fav:" + str(chk1) + "-" + str(chk2) + "-" + str(chk3)
            if judge == 1:
                block_users.append(page[i].screen_name)
                block_IDs.append(page[i].id)
                hit_count = hit_count +1
                textBox.insert(END, "【" + str(hit_count) +"】" + str(page[i].screen_name) + "/" + result + "/" + str(page[i].description) + "\n\n")
                textBox.see(END)
                textBox.update()
                print("■■■ フォロワー:" + str(page[i].id) + "/" + str(page[i].screen_name) + "/" + result + "/" + str(page[i].description))
        print("取得数",len(followers))
        time.sleep(1)
        if len(followers)>max_count:
            break 

    mem_list = api.get_list_memberships(screen_name=user_ID[0])
    for mem in mem_list:

        judge=0
        word_check.clear()
        print("■■■■ フォロワー:" + str(page[i].screen_name)) 
        chk_count=0
        for chk_group in range(len(block_group)):
            print("■group_max:"+str(group_max[chk_count]))
            chk_count2=0
            for chk in range(group_max[chk_count]):
                #print("■ word:"+str(block_words[chk_count][chk_count2]))
                if str(mem.user.description).rfind(block_words[chk_count][chk_count2]) > 0:
                    word_check.append(1)
                    print("■ Hit:"+str(block_words[chk_count][chk_count2]))
                chk_count2=chk_count2+1
            chk_count=chk_count+1
        print("■ Hit:"+str(sum(word_check))+"/"+str(block_border[chk_count-1]))
        if sum(word_check) > block_border[chk_count-1]:
            judge=1
        chk1 = page[i].followers_count
        chk2 = page[i].statuses_count
        chk3 = page[i].favourites_count

        if chk1 < 10 and chk2 < 5 and chk3 < 5:
            judge=1
        result = "Flw-Tw-Fav:" + str(chk1) + "-" + str(chk2) + "-" + str(chk3)
        if judge == 1:
            print("list: "+mem.name, "screen_name: "+mem.user.screen_name)
            block_lists.append(mem.name)
            block_list_makers.append(mem.user.screen_name)
            block_list_IDs.append(mem.id)
            hit_count = hit_count +1
            textBox.insert(END, "【"+str(hit_count) +"】"+"List: "+mem.name+"/"+str(mem.user.screen_name)+"/"+str(mem.id)+"/"+result+"/"+str(mem.user.description)+"\n\n")
            textBox.see(END)
            textBox.update()

    messagebox.showinfo("チェック処理", "抽出数：" + str(hit_count) + " / " + str(max_count) + "\n\nチェック完了しました")

#ブロックボタン処理
def btn_execute():
    messagebox.askquestion("【ブロック処理】", "検出したアカウントを全てブロックします。\n本当によろしいですか？", icon='warning')
    api = utils.get_cookie_api(user_ID[0],password[0])
    #詐欺アカブロック（ユーザー単位）
    for block_user in range(len(block_users)):
        api.report_spam(screen_name=block_users[block_user])
        textBox.insert(END, "■ブロック【" + str(block_user) +"】" + str(block_users[block_user]) + "\n")
        textBox.see(END)
        textBox.update()
        print("■■■ ブロック完了:" + str(block_users[block_user])) 

    #詐欺アカに追加されたリストから自分を消す処理１：いったんブロック解除
    for block_list in range(len(block_lists)):
        api.destroy_block(screen_name=block_list_makers[block_list])
        textBox.insert(END, "■いったんブロック解除:" + str(block_list_makers[block_list]) + "\n")
        textBox.see(END)
        textBox.update()
        print("■いったんブロック解除:" + str(block_list_makers[block_list]))

    #詐欺アカに追加されたリストから自分を消す処理２：リストから自分を消すブロック処理
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        context = browser.new_context()
        page = context.new_page()
        page.goto("https://twitter.com/i/flow/login?redirect_after_login=%2F" + user_ID[0])
        page.locator("label div").nth(3).click()
        page.get_by_label("電話番号/メールアドレス/ユーザー名").fill(user_ID[0])
        page.get_by_role("button", name="次へ").click()
        page.get_by_label("パスワード", exact=True).click()
        page.get_by_label("パスワード", exact=True).fill(password[0])
        page.get_by_test_id("LoginForm_Login_Button").click()
        page.get_by_label("リスト").click()
        page.get_by_test_id("primaryColumn").get_by_label("もっと見る").click()
        page.get_by_role("menuitem", name="自分がメンバーとなっているリスト").click()
        for block_list in range(len(block_lists)):
            fix_text="https://twitter.com/i/lists/" + str(block_list_IDs[block_list])
            print("■リスト検索:" + str(fix_text))
            page.goto(fix_text)
            page.get_by_test_id("primaryColumn").get_by_label("もっと見る").click()
            page.get_by_text("@" + block_list_makers[block_list] + "さんをブロック").click()
            page.get_by_test_id("confirmationSheetConfirm").click()
            print("■対象リストブロック完了:" + str(block_lists[block_list]) + "/@" + str(block_list_makers[block_list])) 
            time.sleep(1)

    messagebox.showinfo("ブロック処理", "ブロック完了しました")

if __name__ == '__main__': 
    # 画面作成
    tki = tkinter.Tk()
    tki.geometry('500x500')
    tki.title('ブロックツール')

    # Frame1の作成
    frame1 = ttk.Frame(tki, padding=10,width=480,height=120)
    frame1.grid()
    # ラベル

    s = StringVar()
    s.set('あなたのID')
    lbl_1 = ttk.Label(tki,textvariable=s, padding=5)
    lbl_1.place(x=60,y=20)
    # テキストボックス
    txt_1 = ttk.Entry(tki,show='*',width=20)
    txt_1.place(x=120,y=25)

    s2 = StringVar()
    s2.set('パスワード')
    lbl_2 = ttk.Label(tki,textvariable=s2, padding=5)
    lbl_2.place(x=60,y=50)
    # テキストボックス
    txt_2 = ttk.Entry(tki,show='*',width=20)
    txt_2.place(x=120,y=55)

    s3 = StringVar()
    s3.set('チェックユーザ数')
    lbl_3 = ttk.Label(tki,textvariable=s3, padding=5)
    lbl_3.place(x=260,y=20)
    # テキストボックス
    txt_3 = ttk.Entry(tki,width=20)
    txt_3.place(x=350,y=25)

    # Frame2の作成
    frame2 = ttk.Frame(tki, padding=00)
    frame2.place(x=10,y=120)

    # ブロック実行ボタンの作成
    export_button = ttk.Button(tki, text='ブロック実行', command=btn_execute, width=20)
    export_button.place(x=350,y=90)

    # テキスト出力ボックスの作成
    textboxname = StringVar()
    textboxname.set('出力内容')
    label3 = ttk.Label(frame2, textvariable=textboxname)
    label3.grid(row=0, column=0)
    textBox = scrolledtext.ScrolledText(frame2, width=68,height=26)
    textBox.grid(row=1, column=0)

    # チェックボタン
    btn = ttk.Button(tki, text='チェック', command=btn_check, width=20)
    #btn.bind("<Return>", press_enter_event())
    btn.place(x=120,y=90)

    # クリアボタン
    btn = ttk.Button(tki, text='クリア', command=btn_clear, width=10)
    #btn.bind("<Return>", press_enter_event())
    btn.place(x=20,y=90)
    
    # キー入力時のコールバック関数を定義
    def on_key1(event):
        txt_2.focus_set()

    # コールバック関数のバインド
    txt_1.bind("<Key-Return>", on_key1)

    # 以下、entry2用
    def on_key2(event):
        txt_3.focus_set()

    txt_2.bind("<Key-Return>", on_key2)

    def on_key3(event):
        txt_1.focus_set()

    txt_3.bind("<Key-Return>", on_key3)

    tki.mainloop()
```
---
## 【詐欺アカ判定ワードリスト】
※実行ファイルと同じフォルダに設置してください。

### データの説明
* 1つ目：グループ番号（1～99）
* 2つ目：いくつのワードが含まれていたら詐欺アカと判定するか（グループ単位）
* 3つ目：詐欺アカと判定するためのワード


下のサンプルで言うと、
グループ1は「現金,資産,高配当株,プレゼント,経営,🚺,♀,おふぱこ,はめ撮り,マン凸」の内、2つ以上のワードがプロフィール欄に含まれていれば詐欺アカ判定する
グループ2は「壺,統一教会,ネトウヨ」の内2つ以上のワードが含まれていれば詐欺アカ判定


```csvs:block_words.csv
1,2,現金
1,2,資産
1,2,高配当株
1,2,プレゼント
1,2,経営
1,2,🚺
1,2,♀
1,2,おふぱこ
1,2,はめ撮り
1,2,マン凸
2,2,壺
2,2,統一教会
2,2,ネトウヨ
```

## 【今後の予定】

1\. 定期自動実行機能
2\. 自動ツイート＆自動引用RT&自動リプライプログラムと統合

:::note warn
# 【お願い】
pyinstaller で正常に実行出来るように調整出来る方、方法をご教授頂ければ幸いです。
（変換は出来るのですが、logにtop-levelエラーが連発していて実行しても動きません）
どうぞ宜しくお願いいたします。

:::