feat: auto reply with llm by bot
This commit is contained in:
commit
e336593d02
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
token.json
|
||||
1
comments.json
Normal file
1
comments.json
Normal file
File diff suppressed because one or more lines are too long
338
comments.txt
Normal file
338
comments.txt
Normal file
@ -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 告知我她們活動區域
|
||||
|
||||
感謝您不嫌棄🥲 歡迎多多進場!
|
||||
|
||||
世彬值得大家喜愛🥹
|
||||
|
||||
大家都快來桃園球場看世彬~
|
||||
32
main.py
Normal file
32
main.py
Normal file
@ -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))
|
||||
'''
|
||||
126
src/bot.py
Normal file
126
src/bot.py
Normal file
@ -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
|
||||
61
src/llm.py
Normal file
61
src/llm.py
Normal file
@ -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
|
||||
68
src/threads.py
Normal file
68
src/threads.py
Normal file
@ -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())
|
||||
|
||||
17
tools/split_comments.py
Normal file
17
tools/split_comments.py
Normal file
@ -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)
|
||||
|
||||
|
||||
27
tools/token_refresh.py
Normal file
27
tools/token_refresh.py
Normal file
@ -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)
|
||||
Loading…
Reference in New Issue
Block a user