commit e336593d023f6ab00860dcd21734672618ebc776 Author: Ting-Jun Wang Date: Mon Oct 20 00:22:22 2025 +0800 feat: auto reply with llm by bot diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fe6b37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +token.json diff --git a/comments.json b/comments.json new file mode 100644 index 0000000..dcb6bfb --- /dev/null +++ b/comments.json @@ -0,0 +1 @@ +{"comments": ["姐姐是愛之深責之切! 妳真的很愛這支球隊😆", "娜比的威力我可以作證😆", "我貢獻了頭 你貢獻了膝蓋🤣", "還需要多學一點😅 設備之後再說", "感謝你不嫌棄😅", "該來台中了!!", "活力滿滿!!\n看她笑成那樣票錢都值了", "近一個月她超級超級忙 還能在球場活力滿滿!!真的很佩服她的體力", "廉世彬回休息室了吉娜她們才開始跳 是不是沒先揪🤣", "中秋節快樂!! 假期要記得練發球 下一次球場見面我會接好球的😆", "物理性暈🤣", "這就不是說說笑笑那麼簡單了🤣\n肯定保險要拿好拿滿", "太專心拍廉世彬的後果就是沒注意到球過來了🤣\n不過頭被啦啦隊扣球應該也能算是第一人了🤣🤣", "還好球不硬 輕輕彈一下而已🤣", "娜比有送我鳳梨酥當賠禮 是賺的沒錯🤣", "笑死 原來其實球還反彈很遠🤣", "沒事沒事!!🤣 被砸了一下我開始認識娜比了!!不然我本來一直在看廉世彬!😆 娜比也很漂亮👍\n下次球場再見~🥰", "CAN'T WAIT!!!💕", "字放上去而已沒什麼難度🫠 老師處理那些影片才是辛苦了", "預定挑戰賽時間剛好澳門行程🫠 只能相信樂天了!!\nNC 今天晚上比賽很有機會進季後賽", "樂天應該這天請哥當官攝的🤣 拍的真好", "沒好好學拍照基礎 一直是亂拍亂調\n該覺得可惜的是我🤣 應該多認識大家跟前輩們多學一點", "辛苦了!你一直很有心!!", "哇大哥好有心!!", "Sorry 今天我忘記帶來🥲 只能下次再找機會發放了\n您丟一下私訊給我 IG 我留一份下來", "排球真的感覺當了大盤子 好貴🫠", "他們有賽前活動的話很好拍,沒什麼人跟你搶位置。\n球場的話感覺如果不是前三排基本上都是坐牢🤣 不過她們衣服真的都很好看", "會放最後一張是因為看久了會發現她也是挺怪的🤣\n所以每次最後一張都是放她有點怪怪的樣子", "感謝紀錄提供參考!!\n連莊可能要下週場勘才知道😆 我不是季票所以比較彈性,位置不好的話後面場次退掉重買🤣", "對 冬天的票買好了😆", "當然沒問題😍", "大哥明天會來嗎?拿給你!!\n今天等等看完廉世彬要先走🤣", "羨慕了 好多卡😍", "應該時間會多一點… 嗎?🤣 ", "本來說九月 結果九月太忙了🥲", "都在想她不知道有沒有時間回首爾的家🫠 ", "上次直播講到 YouTube 影片我就想到妳可以幫她剪🤣 妳肯定非常樂意", "我最新兩篇都書潤 到時候真的有人以為我要改帳號名字了…🫠", "只能先跟書潤說 Sorry 了", "近四個月只有一個週末有休息,一路走來真的不容易… 她值得獲得更多", "真是謝囉 甘書潤跟我要最後一張照片\n我真的沒錢多追一位了🫠", "沒錯 紀錄一下是當作練習😌🫠", "以防萬一 先道歉🙇", "抱歉了我的錯🫠", "不過大家留了什麼 我先道歉🙇", "不帥才是重點🫠", "我正在懺悔🙇", "我錯了🥲", "我先道歉🙇", "可愛組合🥰🥰", "不容質疑😡", "請繼續喜歡世彬~🥰", "哥 你就只看到這一次😆", "是真的😌", "抽到簽名球不算😆", "不會…暫時…🤣", "不會的😌🤣", "被韓國朋友慫恿🫠", "希望她帶下去了🫠 不然光州跟下禮拜昌原她會很熱😆", "主辦的大哥有心了", "歡迎多多來樂天跟連莊排球看球跟看世彬!🥰", "我以為我跟妳說過了🤣 Sorry", "我超懂 8/22 看廉世彬右手臂在痛,我也是拍到一半就拍不下去了\n希望大家都健健康康、都要好好的🥲", "本來想說之後休賽季可以慢慢來,不過冬季有排球,該說是幸福嗎😆", "妳看到廉世彬了 賺了", "最愛小彬了🥰🥰", "thread 橫的 5:4 好像會吃我的畫質🥲", "對 簽名球", "我後來也有收到世彬鑰匙圈 好可愛🥰", "大概上禮拜沾到哥的好運😂🥰", "羨慕了哥🥲", "分主管跟同事看一下,順便推個廉世彬坑🤣", "這告白… 入廉世彬的坑後不用出來了", "搞笑也很可愛", "搞笑魂上身🤣 難怪之前說想再去上脫口秀和綜藝節目", "她應該真的有搞笑的天賦🤣", "新髮色大家都說很仙😍", "謝謝🥰 希望可以趕快找到自己想做的事", "歡迎多多關注廉世彬 之後會有更多好歌的😆", "唱歌又好聽又有才華🥰", "好謝謝 昨天有看到有好心人有錄完全部\n今天的可能也有機會\n不過這段應該沒差哈哈 還是很可愛😂", "原本就很可愛 這特效還有加成😍", "還換了髮色好好看🥰", "存在本身就拯救世界了🤣", "這個月多了很多 NC 恐龍補賽的平日場,真的得一直來回飛…", "不用拿別人比,認真的人都值得受到喜愛!", "對當天的我來說是萬惡白欄杆🫠", "趁現在人生過渡階段比較能跑,接下來就難了🥲", "對 不奢求什麼,只希望她健健康康🥲", "好喜歡這張!!笑得好好看", "隨時都很可愛🤣", "恭喜恭喜~ 很好看😍", "這個表情好可愛🥰", "好幾次看到她超級痛的表情,我覺得可能沒那麼快🫠🥲", "好好笑 其他能抓就這個不能抓耶🤣", "昌原NC Park是我愛的人在的球場、也是我自己很喜歡的球場… 真的別來搞亂它耶🫠", "追世彬的路上姐姐你一直是很幸運的人!!超級羨慕😍\n之後也一直一起陪世彬走花路吧!🥰🥰", "之前有幾次留比較久的不知道粒姐有沒有每天都在🥲", "少了幾天就變得好不想飛哦🫠 在首爾不知道要幹嘛", "我現在還算是學生,這就是當學生的好處🤣", "比賽取消了 已經退款!感謝您提醒,沒有想過下雨天這個狀況🤣", "跟我印象中好像不一樣🤣 我記得是傾斜45度角的蜻蜓", "值了", "我也超愛今天的髮型 好可愛", "她的眼睛真的很好看!永遠都很有活力的感覺", "真的嗎?其實我對 KBO 賽制不太熟,也不太清楚之前補賽都怎麼安排🥲", "眼神超可愛👀😍", "我前天有穿,昨天沒有😆", "比梗圖那隻貓還可愛😍", "廉世彬哼歌的聲音超可愛😍", "看起來感情真的很好 情同姐妹", "開場前她順著音樂先給的福利☺️", "好好笑 看這影片真的暈了 剛剛發現捷運坐錯邊了😆", "那可不行 還是想多看看客場的世彬🤣", "怎麼所有好事情都被妳遇到了😆 恭喜!!", "看到宋家翔 我馬上轉頭😆 就等這一刻啊", "這新妹妹是真的挺皮的🤣", "謝謝鼓勵!不過還需要多學多練🫠", "Joy 想說下次不帶她來野生出沒了🤣", "我現在也快死了… 廉世彬有活動的時候都要好早起床… 逼迫我規律作息", "可是外野真的好暗🥲", "問就是去🤣 也可以順便看一下釜山的海", "我應該不太敢🤣😅", "안돼!!!🫠 我不行了…😵", "昌原有點遠…🫠 我們先壯大桃園彬店😍", "下次站一排喊😆", "我只看廉世彬,其他人的要找別人😅🤣", "廉世彬跳這首是可愛又帥氣🥰", "其他人我不知道,不過在我心中廉世彬最棒☺️", "除了跳舞唱歌小提琴,有時候還要彈吉他、當DJ😂", "天生就是音樂家,裝備什麼的不會影響演奏🤣", "雖然你是色猴我想反駁個什麼,可是你這句話無法反駁,世彬真的好可愛😍", "如果你說的是我們都認識那位的話,我們沒約好不過剛好是同一班班機過來🤣", "剛剛看海突然想到笑出來,我微退kpop卡坑了怎麼還有另外一個卡坑🤣", "kpop常用的硬卡套哦!", "肯定要買的🤣", "直接暈了!不過還是要澄清我不是渣男🥲", "下次再來看最好看的店員😍", "暈很久了🥹🤣", "一定不是這個原因🤣", "第一次看到那麼好看的店員😍", "這兩天因為妳而過得很幸福!🥰 Hasubeen 版本的世彬下次有機會再見吧~🥹", "謝謝😭 麻煩確認 IG 私訊我跟您詢問一下位置", "我晚點跟您聯絡!!超級感謝🥰", "好好看哦!!好羨慕🥹", "順帶一提,世彬小卡超級好看", "沒事 沒有妳我連看都看不到🤣🫠", "新手還在努力學習中!也還在努力存錢買好設備🥹", "好可惜 差一個小時🫠", "明天在高雄有普格爾的活動,世彬已經連續四週在台灣有工作了,下週大巨蛋明星賽也還有活動,可以多來看看她!", "真的很好看🥰", "在任何地方發現有世彬的時候,我也都很開心!!", "球場上球場下都超暖😭\n不愛不行🥹", "我真的是靠她活下來的🫠 有她我才能今年順利畢業,現在能一直追著她跑😆", "對 自己開心就好🥰", "努力拍!!不過留多久不好說,她快樂就好☺️", "還在摸索怎麼調,其實每次看起來都不太一樣🥲", "感謝您不嫌棄🥲 我也要感謝妳在 ATT 告知我她們活動區域", "感謝您不嫌棄🥲 歡迎多多進場!", "世彬值得大家喜愛🥹", "大家都快來桃園球場看世彬~\n"]} \ No newline at end of file diff --git a/comments.txt b/comments.txt new file mode 100644 index 0000000..ced2aa8 --- /dev/null +++ b/comments.txt @@ -0,0 +1,338 @@ +姐姐是愛之深責之切! 妳真的很愛這支球隊😆 + +娜比的威力我可以作證😆 + +我貢獻了頭 你貢獻了膝蓋🤣 + +還需要多學一點😅 設備之後再說 + +感謝你不嫌棄😅 + +該來台中了!! + +活力滿滿!! +看她笑成那樣票錢都值了 + +近一個月她超級超級忙 還能在球場活力滿滿!!真的很佩服她的體力 + +廉世彬回休息室了吉娜她們才開始跳 是不是沒先揪🤣 + +中秋節快樂!! 假期要記得練發球 下一次球場見面我會接好球的😆 + +物理性暈🤣 + +這就不是說說笑笑那麼簡單了🤣 +肯定保險要拿好拿滿 + +太專心拍廉世彬的後果就是沒注意到球過來了🤣 +不過頭被啦啦隊扣球應該也能算是第一人了🤣🤣 + +還好球不硬 輕輕彈一下而已🤣 + +娜比有送我鳳梨酥當賠禮 是賺的沒錯🤣 + +笑死 原來其實球還反彈很遠🤣 + +沒事沒事!!🤣 被砸了一下我開始認識娜比了!!不然我本來一直在看廉世彬!😆 娜比也很漂亮👍 +下次球場再見~🥰 + +CAN'T WAIT!!!💕 + +字放上去而已沒什麼難度🫠 老師處理那些影片才是辛苦了 + +預定挑戰賽時間剛好澳門行程🫠 只能相信樂天了!! +NC 今天晚上比賽很有機會進季後賽 + +樂天應該這天請哥當官攝的🤣 拍的真好 + +沒好好學拍照基礎 一直是亂拍亂調 +該覺得可惜的是我🤣 應該多認識大家跟前輩們多學一點 + +辛苦了!你一直很有心!! + +哇大哥好有心!! + +Sorry 今天我忘記帶來🥲 只能下次再找機會發放了 +您丟一下私訊給我 IG 我留一份下來 + +排球真的感覺當了大盤子 好貴🫠 + +他們有賽前活動的話很好拍,沒什麼人跟你搶位置。 +球場的話感覺如果不是前三排基本上都是坐牢🤣 不過她們衣服真的都很好看 + +會放最後一張是因為看久了會發現她也是挺怪的🤣 +所以每次最後一張都是放她有點怪怪的樣子 + +感謝紀錄提供參考!! +連莊可能要下週場勘才知道😆 我不是季票所以比較彈性,位置不好的話後面場次退掉重買🤣 + +對 冬天的票買好了😆 + +當然沒問題😍 + +大哥明天會來嗎?拿給你!! +今天等等看完廉世彬要先走🤣 + +羨慕了 好多卡😍 + +應該時間會多一點… 嗎?🤣 + +本來說九月 結果九月太忙了🥲 + +都在想她不知道有沒有時間回首爾的家🫠 + +上次直播講到 YouTube 影片我就想到妳可以幫她剪🤣 妳肯定非常樂意 + +我最新兩篇都書潤 到時候真的有人以為我要改帳號名字了…🫠 + +只能先跟書潤說 Sorry 了 + +近四個月只有一個週末有休息,一路走來真的不容易… 她值得獲得更多 + +真是謝囉 甘書潤跟我要最後一張照片 +我真的沒錢多追一位了🫠 + +沒錯 紀錄一下是當作練習😌🫠 + +以防萬一 先道歉🙇 + +抱歉了我的錯🫠 + +不過大家留了什麼 我先道歉🙇 + +不帥才是重點🫠 + +我正在懺悔🙇 + +我錯了🥲 + +我先道歉🙇 + +可愛組合🥰🥰 + +不容質疑😡 + +請繼續喜歡世彬~🥰 + +哥 你就只看到這一次😆 + +是真的😌 + +抽到簽名球不算😆 + +不會…暫時…🤣 + +不會的😌🤣 + +被韓國朋友慫恿🫠 + +希望她帶下去了🫠 不然光州跟下禮拜昌原她會很熱😆 + +主辦的大哥有心了 + +歡迎多多來樂天跟連莊排球看球跟看世彬!🥰 + +我以為我跟妳說過了🤣 Sorry + +我超懂 8/22 看廉世彬右手臂在痛,我也是拍到一半就拍不下去了 +希望大家都健健康康、都要好好的🥲 + +本來想說之後休賽季可以慢慢來,不過冬季有排球,該說是幸福嗎😆 + +妳看到廉世彬了 賺了 + +最愛小彬了🥰🥰 + +thread 橫的 5:4 好像會吃我的畫質🥲 + +對 簽名球 + +我後來也有收到世彬鑰匙圈 好可愛🥰 + +大概上禮拜沾到哥的好運😂🥰 + +羨慕了哥🥲 + +分主管跟同事看一下,順便推個廉世彬坑🤣 + +這告白… 入廉世彬的坑後不用出來了 + +搞笑也很可愛 + +搞笑魂上身🤣 難怪之前說想再去上脫口秀和綜藝節目 + +她應該真的有搞笑的天賦🤣 + +新髮色大家都說很仙😍 + +謝謝🥰 希望可以趕快找到自己想做的事 + +歡迎多多關注廉世彬 之後會有更多好歌的😆 + +唱歌又好聽又有才華🥰 + +好謝謝 昨天有看到有好心人有錄完全部 +今天的可能也有機會 +不過這段應該沒差哈哈 還是很可愛😂 + +原本就很可愛 這特效還有加成😍 + +還換了髮色好好看🥰 + +存在本身就拯救世界了🤣 + +這個月多了很多 NC 恐龍補賽的平日場,真的得一直來回飛… + +不用拿別人比,認真的人都值得受到喜愛! + +對當天的我來說是萬惡白欄杆🫠 + +趁現在人生過渡階段比較能跑,接下來就難了🥲 + +對 不奢求什麼,只希望她健健康康🥲 + +好喜歡這張!!笑得好好看 + +隨時都很可愛🤣 + +恭喜恭喜~ 很好看😍 + +這個表情好可愛🥰 + +好幾次看到她超級痛的表情,我覺得可能沒那麼快🫠🥲 + +好好笑 其他能抓就這個不能抓耶🤣 + +昌原NC Park是我愛的人在的球場、也是我自己很喜歡的球場… 真的別來搞亂它耶🫠 + +追世彬的路上姐姐你一直是很幸運的人!!超級羨慕😍 +之後也一直一起陪世彬走花路吧!🥰🥰 + +之前有幾次留比較久的不知道粒姐有沒有每天都在🥲 + +少了幾天就變得好不想飛哦🫠 在首爾不知道要幹嘛 + +我現在還算是學生,這就是當學生的好處🤣 + +比賽取消了 已經退款!感謝您提醒,沒有想過下雨天這個狀況🤣 + +跟我印象中好像不一樣🤣 我記得是傾斜45度角的蜻蜓 + +值了 + +我也超愛今天的髮型 好可愛 + +她的眼睛真的很好看!永遠都很有活力的感覺 + +真的嗎?其實我對 KBO 賽制不太熟,也不太清楚之前補賽都怎麼安排🥲 + +眼神超可愛👀😍 + +我前天有穿,昨天沒有😆 + +比梗圖那隻貓還可愛😍 + +廉世彬哼歌的聲音超可愛😍 + +看起來感情真的很好 情同姐妹 + +開場前她順著音樂先給的福利☺️ + +好好笑 看這影片真的暈了 剛剛發現捷運坐錯邊了😆 + +那可不行 還是想多看看客場的世彬🤣 + +怎麼所有好事情都被妳遇到了😆 恭喜!! + +看到宋家翔 我馬上轉頭😆 就等這一刻啊 + +這新妹妹是真的挺皮的🤣 + +謝謝鼓勵!不過還需要多學多練🫠 + +Joy 想說下次不帶她來野生出沒了🤣 + +我現在也快死了… 廉世彬有活動的時候都要好早起床… 逼迫我規律作息 + +可是外野真的好暗🥲 + +問就是去🤣 也可以順便看一下釜山的海 + +我應該不太敢🤣😅 + +안돼!!!🫠 我不行了…😵 + +昌原有點遠…🫠 我們先壯大桃園彬店😍 + +下次站一排喊😆 + +我只看廉世彬,其他人的要找別人😅🤣 + +廉世彬跳這首是可愛又帥氣🥰 + +其他人我不知道,不過在我心中廉世彬最棒☺️ + +除了跳舞唱歌小提琴,有時候還要彈吉他、當DJ😂 + +天生就是音樂家,裝備什麼的不會影響演奏🤣 + +雖然你是色猴我想反駁個什麼,可是你這句話無法反駁,世彬真的好可愛😍 + +如果你說的是我們都認識那位的話,我們沒約好不過剛好是同一班班機過來🤣 + +剛剛看海突然想到笑出來,我微退kpop卡坑了怎麼還有另外一個卡坑🤣 + +kpop常用的硬卡套哦! + +肯定要買的🤣 + +直接暈了!不過還是要澄清我不是渣男🥲 + +下次再來看最好看的店員😍 + +暈很久了🥹🤣 + +一定不是這個原因🤣 + +第一次看到那麼好看的店員😍 + +這兩天因為妳而過得很幸福!🥰 Hasubeen 版本的世彬下次有機會再見吧~🥹 + +謝謝😭 麻煩確認 IG 私訊我跟您詢問一下位置 + +我晚點跟您聯絡!!超級感謝🥰 + +好好看哦!!好羨慕🥹 + +順帶一提,世彬小卡超級好看 + +沒事 沒有妳我連看都看不到🤣🫠 + +新手還在努力學習中!也還在努力存錢買好設備🥹 + +好可惜 差一個小時🫠 + +明天在高雄有普格爾的活動,世彬已經連續四週在台灣有工作了,下週大巨蛋明星賽也還有活動,可以多來看看她! + +真的很好看🥰 + +在任何地方發現有世彬的時候,我也都很開心!! + +球場上球場下都超暖😭 +不愛不行🥹 + +我真的是靠她活下來的🫠 有她我才能今年順利畢業,現在能一直追著她跑😆 + +對 自己開心就好🥰 + +努力拍!!不過留多久不好說,她快樂就好☺️ + +還在摸索怎麼調,其實每次看起來都不太一樣🥲 + +感謝您不嫌棄🥲 我也要感謝妳在 ATT 告知我她們活動區域 + +感謝您不嫌棄🥲 歡迎多多進場! + +世彬值得大家喜愛🥹 + +大家都快來桃園球場看世彬~ diff --git a/main.py b/main.py new file mode 100644 index 0000000..d09054e --- /dev/null +++ b/main.py @@ -0,0 +1,32 @@ +import os +from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters, ContextTypes, CallbackQueryHandler +from src.bot import ThreadsBot + +BOT_TOKEN = os.environ.get('THREAD_TELEGRAM_TOKEN') + +if __name__ == '__main__': + bot = ThreadsBot() + + app = ApplicationBuilder().token(BOT_TOKEN).build() + + # app.add_handler(CommandHandler("start", start)) + app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, bot.echo)) + app.add_handler(CallbackQueryHandler(bot.button_callback)) + + print("🤖 Bot is running...") + app.run_polling() + + ''' + client = OpenAI(api_key=os.environ['OPENAI_API_KEY']) + + + + print(response.choices[0].message.content) + + print("使用的 token:") + print(" prompt_tokens =", response.usage.prompt_tokens) + print(" completion_tokens =", response.usage.completion_tokens) + print(" total_tokens =", response.usage.total_tokens) + cost = 1.25/1000000 * response.usage.prompt_tokens + 10.00/1000000 * response.usage.completion_tokens + print(" cost = {} USD, {} NTD".format(cost, cost*30)) + ''' diff --git a/src/bot.py b/src/bot.py new file mode 100644 index 0000000..9372e6f --- /dev/null +++ b/src/bot.py @@ -0,0 +1,126 @@ +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, Update +from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters, ContextTypes, CallbackQueryHandler +from telegram.constants import ParseMode +import json +from openai import OpenAI +import os +from src.llm import get_reply +from src.threads import ThreadsAPI + +RESPONSE = ''' +====================================================== + +Your Post: +{} + +--- + +Link: +{} + +====================================================== +''' + +class ThreadsBot(): + def __init__(self): + self.threads = ThreadsAPI() + self.llm_client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY')) + + self.post_memory = {} + self.comments_memory = [] + self.replies_memory = [] + + async def echo(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + text = update.message.text + if update.message.from_user.id == 940081323: + if 'www.threads.com' in text: + self.post_memory = self.threads.get_post_by_link(text) + self.comments_memory = self.filter_comments(self.threads.get_comments_by_post(self.post_memory)) + + + text = RESPONSE.format(self.post_memory['text'], text) + + buttons = [] + for index, comment in enumerate(self.comments_memory): + buttons.append([InlineKeyboardButton(f"@{comment['username']}: {comment['text']}", callback_data=f"choose_comment:{index}")]) + button_markup = InlineKeyboardMarkup(buttons) + + await update.message.reply_text(text) + await update.message.reply_text("要回覆哪一則留言?", reply_markup=button_markup) + else: + await update.message.reply_text(f"這不是 thread 網址") + else: + await update.message.reply_text(f"Auth Failed") + + async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + + action, data = query.data.split(':') + if action == 'choose_comment': + comment = self.comments_memory[int(data)] + self.comments_memory = [comment] + await query.message.reply_text(f"你選擇回覆 @{comment['username']}: {comment['text']}\n\n請等待 LLM 協助...") + + llm_response = get_reply(self.llm_client, self.post_memory['text'], comment['text']) + llm_data = json.loads(llm_response.choices[0].message.content) + self.replies_memory = llm_data['reply'] + + buttons = [] + for index, reply in enumerate(self.replies_memory): + buttons.append([InlineKeyboardButton(reply, callback_data=f"reply:{index}")]) + buttons.append([InlineKeyboardButton("❌ 重新生成", callback_data=f"reply:again_{data}")]) + button_markup = InlineKeyboardMarkup(buttons) + print(button_markup) + + await query.message.reply_text(json.dumps(self.replies_memory, ensure_ascii=False, indent=4)) + await query.message.reply_text("你喜歡哪一則回覆呢?", reply_markup=button_markup) + elif action == 'reply': + if 'again' in data: + print(data.replace('again_', '')) + data = data.replace('again_', '') + comment = self.comments_memory[0] + await query.message.reply_text(f"你選擇回覆 @{comment['username']}: {comment['text']}\n\n請等待 LLM 協助...") + + llm_response = get_reply(self.llm_client, self.post_memory['text'], comment['text']) + llm_data = json.loads(llm_response.choices[0].message.content) + self.replies_memory = llm_data['reply'] + + buttons = [] + for index, reply in enumerate(self.replies_memory): + buttons.append([InlineKeyboardButton(reply, callback_data=f"reply:{index}")]) + buttons.append([InlineKeyboardButton("❌ 重新生成", callback_data=f"reply:again_{data}")]) + button_markup = InlineKeyboardMarkup(buttons) + + await query.message.reply_text(json.dumps(self.replies_memory, ensure_ascii=False, indent=4)) + await query.message.reply_text("你喜歡哪一則回覆呢?", reply_markup=button_markup) + else: + self.replies_memory = [self.replies_memory[int(data)]] + buttons = [ + [InlineKeyboardButton("❌ 取消", callback_data="send_reply:False")], + [InlineKeyboardButton("✅ 發送", callback_data="send_reply:True")], + ] + button_markup = InlineKeyboardMarkup(buttons) + + await query.message.reply_text("你想回覆\n\n\t{}\n\n對嗎?".format(self.replies_memory[0]), reply_markup=button_markup) + elif action == 'send_reply': + if data == 'True': + assert len(self.comments_memory) == 1, "comments_memory error" + assert len(self.replies_memory) == 1, "replies_memory error" + comment_id = self.comments_memory[0]['id'] + reply = self.replies_memory[0] + # reply = "自動回覆 bot 測試:\n\n"+reply + + await query.message.reply_text(f"正在對 comment {comment_id} 送出回覆:「{reply}」... 請稍候...") + self.threads.send_reply(comment_id, reply) + await query.message.reply_text("送出!") + else: + await query.message.reply_text("已經取消") + + + def filter_comments(self, comments): + ans = [] + for comment in comments: + if not comment['is_reply_owned_by_me']: + ans.append(comment) + return ans diff --git a/src/llm.py b/src/llm.py new file mode 100644 index 0000000..b558a21 --- /dev/null +++ b/src/llm.py @@ -0,0 +1,61 @@ +import random +import json +from openai import OpenAI + +RANDOM_POST_NUM = 15 +PROMPT = ''' +I run a fan page for 廉世彬, a Korean baseball(樂天桃猿 team) and volleyball(台中連莊 team) cheerleader currently active in Taiwan. + +I will provide my past comment history in JSON format (under the comments_history field), and you must imitate my commenting style. +I will also provide the main post content (post field) and a fan’s comment (comment field). +You should generate 3 reference replies that matches my style and tone, and return it in JSON format with your answer in the reply field. + +For example: + +Input: + { + "comments_history": ["我應該不太敢🤣😅", "下次站一排喊😆", ...], + "post": "我還想今年在大巨蛋、桃園主場看到廉世彬", + "comment": "一定可以的 我們打到大巨蛋" + } +Output: + { + "reply": ["樂天一定可以贏的😆", "加油!!", ...] + } + +Now it's your turn: + +Input: + ___INPUT___ + +Output: +''' + +def random_comments(): + with open('comments.json', 'r', encoding="utf-8") as fp: + comments = json.load(fp)['comments'] + + random.shuffle(comments) + return comments[:15] + +def get_reply(client: OpenAI, post: str, comment: str): + + input_data = { + "comments_history": random_comments(), + "post": post, + "comment": comment, + } + + input_prompt = PROMPT.replace('___INPUT___', json.dumps(input_data, ensure_ascii=False, indent=4)) + print(input_prompt) + + response = client.chat.completions.create( + model="gpt-4o", # 或 "gpt-4o" 等可用模型 + response_format={"type": "json_object"}, + messages=[ + {"role": "system", "content": "You are a helpful assistant to help me to reply the comments."}, + {"role": "user", "content": input_prompt} + ], + ) + + return response diff --git a/src/threads.py b/src/threads.py new file mode 100644 index 0000000..73df0e7 --- /dev/null +++ b/src/threads.py @@ -0,0 +1,68 @@ +import requests +import time +import json + +class ThreadsAPI: + + def __init__(self): + self.ACCESS_TOKEN = self.get_thread_token() + self.user_id = "32143803045264731" + + def get_thread_token(self): + with open('token.json') as fp: + token = json.load(fp)['token'] + return token + + def get_posts(self): + URL = "https://graph.threads.net/v1.0/me/threads?fields=id,permalink,owner,username,text,shortcode&access_token={}" + + response = requests.get(URL.format(self.ACCESS_TOKEN)).json() + posts = response['data'] + + return posts + + def get_comments_by_post(self, post): + URL = "https://graph.threads.net/v1.0/{}/conversation?fields=id,text,username,is_reply_owned_by_me&reverse=false&access_token={}" + + response = requests.get(URL.format(post['id'], self.ACCESS_TOKEN)).json() + comments = response['data'] + + return comments + + def get_post_by_link(self, link): + posts = self.get_posts() + target_post = None + for post in posts: + if post['permalink'] in link: + target_post = post + break + return target_post + + def send_reply(self, comment_id: str, reply: str): + + url = "https://graph.threads.net/v1.0/me/threads" + + payload = { + "media_type": "TEXT_POST", # 如果只是文字回覆就用 TEXT + "text": reply, + "reply_to_id": comment_id, + "access_token": self.ACCESS_TOKEN, + } + + response = requests.post(url, data=payload) + print(response) + print(response.json()) + + CONTAINER_ID = response.json()['id'] + url = f"https://graph.threads.net/v1.0/{self.user_id}/threads_publish" + + payload = { + "creation_id": CONTAINER_ID, + "access_token": self.ACCESS_TOKEN, + } + + time.sleep(5) + response = requests.post(url, data=payload) + print(response) + print(response.json()) + diff --git a/tools/split_comments.py b/tools/split_comments.py new file mode 100644 index 0000000..d42f870 --- /dev/null +++ b/tools/split_comments.py @@ -0,0 +1,17 @@ +import json + +with open('comments.txt') as fp: + data = fp.read() + +comments = data.split('\n\n') + +print(len(comments)) + +data = { + 'comments': comments +} + +with open('comments.json', 'w', encoding="utf-8") as fp: + json.dump(data, fp, ensure_ascii=False) + + diff --git a/tools/token_refresh.py b/tools/token_refresh.py new file mode 100644 index 0000000..cfa3e75 --- /dev/null +++ b/tools/token_refresh.py @@ -0,0 +1,27 @@ +import os +import requests +import json + +with open('token.json') as fp: + token = json.load(fp)['token'] + +print(token) +token = "THAALHhpO4mmtBUVR6MjRBNUs5NUNDRnhILXhsTEZAhSVBVRmtRc1l5WEZAGZAUl6OENvOEZA0R3NzdUo1akNyMlc4OU93TEpiVnpMRGRTYzVhOG5hcDhQNG9CcnBVd19mb3VfZAkFVQVM5bGRMSGNPVzZAiSHVCODVxUVdNSkJ1MkJYTUFsVW9ucnpwSFdEYlhoN2FId09JVHE1RWttNmZAvV1BYUy1yZA3QtUQZDZD" +print(token) + +CLIENT_ID = os.environ['THREAD_APP_CLIENT_ID'] +CLIENT_SECRET = os.environ['THREAD_APP_CLIENT_SECRET'] + +URL = "https://graph.threads.net/v1.0/access_token" +params = { + "grant_type": "th_exchange_token", + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "access_token": token + } +response = requests.get(URL, params=params) +new_token = response.json()['access_token'] +print("New Token: ", new_token) + +with open('token.json', 'w') as fp: + json.dump({'token': new_token}, fp)