
ãç¶ç·šããåããŠãbotããçªç¶æ»ãã ââ rss.appçµäºãããããè¯ãæ§æã«äœãçŽããŸã§ã®å šèšé²
ååãXïŒæ§TwitterïŒã® AIãã¥ãŒã¹ãŸãšãã Chatwork ã«èªåæçš¿ãã bot ããå®è³ª1æéã§äœã£ããè©±ãæžããããµãŒããŒä»£0åãGitHub Actions ãš rss.app ã§åãæ§æã ã
ããããæ°ãæããã® bot ã¯ãããæãéãã«æ¢ãŸã£ããæ·±å€ã«çºåãããæçš¿ã¯ãããªãã£ãã»ã»ã»ãªãïŒã»ã»ææ··ä¹±ãå§ãŸã£ãã»ã»
ä»åã¯ãã®ãçªç¶æ»ããããåå ãäžã€ãã€æœ°ããŠãçµæçã«åããè¯ãæ§æã«äœãçŽããŸã§ã®å šèšé²ã ãåååæ§ãããŸããã£ãæé ã ãã§ãªããè©°ãŸã£ããšãããå šéšæžããåãæ§æãçµãã§ãã人ã«ã¯ããã¶ããããäžçªåœ¹ã«ç«ã€ã
第äžå ±ïŒãYour trial has expiredã
ç°å€ã«æ°ã¥ããã®ã¯ãChatwork ã«æ¯æå±ãã¯ãã®ãã¥ãŒã¹ãæ¥ãŠããªãã£ãããã ã
rss.app ã®ãã£ãŒãç»é¢ãéããšãèšäºã®ä»£ããã«ãã衚瀺ãããŠããã
ã[Action required] Your RSS.app Trial has Expired.ã
ç¡æãã©ã€ã¢ã«ãçµãã£ãŠããããã£ãŒãã¯çããŠããããã«èŠããŠãäžèº«ã¯ãã®èŠåæã ããè¿ããbot ã¯åŸåã«ããããæ°çèšäºããšããŠæ±ãããšããŠããã
éžæè¢ã¯äºã€ãæé¡9ãã«çšåºŠã課éã㊠rss.app ã䜿ãç¶ããããå¥ã®ææ®µã«ç§»ããããã£ãããªã®ã§ãç¡æã§å®å®ããæ§æãæ¢ãããšã«ããããããé·ãæ ã®å§ãŸãã ã£ããé¢åãªã®ã§èª²éãããããšãæ©ãã ã»ã»ã»
è©°ãŸãâ ïŒX ã® RSS åããããç¡çã«ãªã£ãŠãã
ãŸãèããã®ã¯ãrss.app ãšåãããšãããå¥ã®ç¡æãµãŒãã¹ãã ãX ã®æçš¿ã RSS ã«ã§ããã°ããã£ãŒãURLãå·®ãæ¿ããã ãã§æžãã
ãšãããããããå šæ» ã ã£ããåããããªãµãŒãã¹ã¯ãªãã£ãã»ã»
ãã€ãŠå®çªã ã£ã Nitter ã¯ãæ¬å®¶ã RSS æäŸãçµäºãéå ¬åŒã€ã³ã¹ã¿ã³ã¹ãããã€ãå©ããŠã¿ããã200 ãè¿ã£ãŠããŠãäžèº«ã¯åºåã ããã® HTML ã ã£ãããããããå¿çããªãã£ããã§ãå®çšã«èããªãã
æè¡è åãã® RSSHub ã詊ãããå ¬éã€ã³ã¹ã¿ã³ã¹ïŒrsshub.app ãªã©ïŒã® X ã«ãŒãã¯ã404 ã 503 ã°ããã調ã¹ããšãX ã®ã«ãŒãã¯ä»æ§å€æŽã§ãèªåãã¹ãïŒãã°ã€ã³CookieèªèšŒã§ããåããªãã圢ã«å€ãã£ãŠãããå ¬éã€ã³ã¹ã¿ã³ã¹ã§åããªãã®ã¯ããã¯ã仿§ãªã®ã ã
ããã§äžã€ãéãçŸå®ãåãå ¥ããããšã«ãªã£ãã2026幎ããç¡æã§æè»œã« X ã RSS åããããšããåæãã®ãã®ããã»ãŒåŽ©å£ããŠããã X ã2023幎以éã¹ã¯ã¬ã€ãã³ã°ã培åºçã«æœ°ããŠããçµæã§ãrss.app ãææåããã®ãããã¶ããã®äœæ³¢ã ãç§ã®èª¿æ»ã¯çãã£ãã»ã»ã»
èªåã§ RSSHub ã Docker ã§ç«ãŠãæšãŠã¢ã«ãŠã³ãã® Cookie ãé£ãããã°åããæé ãçšæãããã ããCookie ã®å€±å¹ã¡ã³ãã宿çã«çºçããããæšãŠã¢ã«ãŠã³ãã BAN ããããªã¹ã¯ããããããã£ãšç°¡åãªæ¹æ³ã¯ãªããããšç«ã¡æ¢ãŸã£ãã
転æ©ïŒæ å ±æºãã®ãã®ãèŠçŽã
ããã§çºæ³ãå€ãããããããèªåã远ãããããã®ã¯ãç¹å®ã®äººã® AIãã¥ãŒã¹ãŸãšããã§ãã£ãŠããX ãšãããã©ãããã©ãŒã ãã§ã¯ãªãã
ãã®é ä¿¡è ãããèŠããšãX ã ãã§ãªã ã¡ãŒã«ãã¬ãžã³ã§ãåããŸãšããé ä¿¡ããŠããã 䜿ã£ãŠããã®ã¯ Ghost ãšããé ä¿¡åºç€ããã㊠Ghost ã«ã¯ãæšæºã§å ¬åŒ RSS ãä»ããŠããã
ããã ããšæã£ããé ä¿¡è æ¬äººãåºããŠããå ¬åŒ RSS ãªããCookie ããµãŒããŒãèŠããªããã¹ã¯ã¬ã€ãã³ã°ã§ãããªãããã£ãŒãURLãå·®ãæ¿ããã ãã§çµãããå®éããã£ãŒãURLïŒãµã€ãã®ãã¡ã€ã³ã« /rss/ ãä»ããã ãïŒãå©ããšãããã㪠XML ãè¿ã£ãŠããã
X ã®ã¹ã¯ã¬ã€ãã³ã°ã«å»¶ã ãšèŠåŽããŠããã®ããåã®ããã«äžç¬ã§è§£æ±ºãããæ å ±æºãäžæ®µäžæµã«ããã®ãŒããšããããªã«æ¥œã«ãªãã®ãããšããåŠã³ã ã£ãã
è©°ãŸãâ¡ïŒç§»è¡ããç¬éãéå»èšäºã飿ããã
ãã£ãŒãURLã Ghost ã®å ¬åŒ RSS ã«å·®ãæ¿ããŠãããå®è¡ããããšãChatwork ã« éå»ã®ãŸãšããäœä»¶ãé£ç¶ã§æçš¿ãããã
åå ã¯ãéè€æçš¿ãé²ãããã®ä»çµã¿ã«ãã£ãããã® bot 㯠last_guid.txt ãšãããã¡ã€ã«ã«ãæåŸã«éã£ãèšäºã® IDïŒGUIDïŒããèŠããŠãããããããæ°ããèšäºã ããéãã
ãšãããããã£ãŒãã rss.app ãã Ghost ã«å€ããããšã§ãèšäºãèå¥ãã GUID ã®äœç³»ãäžžããšå€ãã£ãŠãããlast_guid.txt ã«æ®ã£ãŠããã®ã¯ rss.app æä»£ã®å€ã GUIDãGhost ã®æ°ãã GUID ãšã¯äžã€ãäžèŽããªãããããšããŸã éã£ãŠããªãèšäºãã倧éã«ãããšå€å®ãããçã£ç«¯ããæçš¿ããŠããŸã£ãããã ã
察åŠã¯ã·ã³ãã«ã§ãlast_guid.txt ã®äžèº«ããä»ã®ãã£ãŒãã®ææ°èšäºã® GUIDãã«æã§æžãæããã ããããã§ãææ°ãŸã§éä¿¡æžã¿ãã®ç¶æ ã«ãªããé£æã¯æ¢ãŸã£ãã
æèšããã£ãŒããå¥ãµãŒãã¹ã«ç§»ããšãã¯ãç¶æ ãã¡ã€ã«ã®ãªã»ãããå¿ãããªã GUID ã¯åŒãç¶ãããªãã
è©°ãŸãâ¢ïŒæ¬æãããã€ãéäžã§åãã
é£æãæ¢ãŸã£ãŠããããã1ä»¶ãã€æçš¿ãããããã«ãªã£ããããä»åºŠã¯äžèº«ãããããã
ãªãŒãæããGoogleãšAmazonã®ããã®ããã«ãæ¯ååãå Žæã§ããåãã«ãªããç®æ¬¡ãåãããã¯ãå ¥ã£ãŠããªããæŽåœ¢ã¹ã¯ãªãããäœåºŠçŽããŠãå€ãããªãã
åå ç©¶æã®ãããRSS ã®äžèº«ãçŽæ¥èŠããŠã¿ãŠãããããè ã«èœã¡ããGhost ã®æšæº RSS ã¯ãèšäºã®å šæãé ä¿¡ããŠããªãã£ãã é ä¿¡ãããã®ã¯åé ãã¬ãã¥ãŒïŒãªãŒãã®1段èœã®ããããéäžãŸã§ïŒã ããã€ãŸãæŽåœ¢ä»¥åã«ãRSS ã«å ããŒã¿ãååšããªãã£ãã®ã ãã¹ã¯ãªããã®åé¡ã§ã¯ãªãã£ãã
ããã§æ¹åŒãå€ãããRSS ã¯ãæ°çã®æ€ç¥ãã ãã«äœ¿ããæ¬æã¯èšäºããŒãžæ¬äœïŒRSS ã«å«ãŸããèšäºURLïŒãå¥éååŸããŠãããããæŽåœ¢ãããäºæ®µæ§ãã ã
èšäºããŒãžã® HTML æ§é ã調ã¹ããšããªãŒãã¯æ®µèœã¿ã°ãç®æ¬¡ã¯çªå·ä»ããªã¹ãã§ãããã«çµãŸããŠããããããçã£ãŠããªãŒã3æãšç®æ¬¡10é ç®ãæãåºããæšæºã©ã€ãã©ãªã ãã§æžããã®ã§ã远å ã€ã³ã¹ããŒã«ãäžèŠã ã£ãã
ããã§ãçãã©ããã®æçš¿ãå±ãããã«ãªã£ããæ¥ä»å ¥ãã®ã¿ã€ãã«ããªãŒã3æãç®æ¬¡10é ç®ããããŠå šæãžã®ãªã³ã¯ã瀟å¡ãèŠåºãã§å šäœãææ¡ããŠãæ°ã«ãªãã°æ¬æãžé£ã¹ããåœåããããã£ãããšãããããã宿ããã
ããŸãã®çœ ïŒãã¹ãã®ã€ãããæ¬çªã«é£ãã
æåŸã«å°ãããçãçœ ãäžã€ã
æŽåœ¢ã詊ãéäžããã¹ãæçš¿ã®ã€ããã§å®è¡ããããæ¬çªã®å šç€Ÿã«ãŒã ã«é£ãã§ããŸã£ãã æçš¿å ã®ã«ãŒã ID ãç°å¢å€æ°ã§æž¡ããŠããã€ãããããã®è¡ãæããŠããŠãã¹ã¯ãªããå ã®ããã©ã«ãå€ïŒæ¬çªã«ãŒã ïŒã䜿ãããŠããã
æ€èšŒã¯å¿ ã詊éšçšã«ãŒã ã§ãæçš¿å ã®åãæ¿ãã¯ãå®è¡åã«äºåºŠç¢ºèªãããåœããåã®ããšã ããæ¥ãã§ãããšãã»ã©ããããã
ãã©ãçããæ°æ§æ
æçµçãªæ§æã¯ããã ã

é ä¿¡è æ¬äººã® Ghost å ¬åŒ RSS ã§æ°çãæ€ç¥ããGitHub Actions ã1æ¥2åèµ·åãæ°çãããã°èšäºããŒãžæ¬äœãååŸããŠããªãŒãïŒç®æ¬¡ã«æŽåœ¢ããChatwork ãžæçš¿ãããç¶æ ãã¡ã€ã«ã¯ãªããžããªã«ã³ãããããŠæ°žç¶åãã飿ãé²ãã
ååã®æ§æãšæ¯ã¹ãŠãäœãè¯ããªã£ããã
X ã®ã¹ã¯ã¬ã€ãã³ã°ã«äžåäŸåããªããªã£ããCookie ãèªåãµãŒããŒãèŠããªããå®å šç¡æã§ãé ä¿¡è ã Ghost ããããªãéãå£ããªããç®èãªããšã«ãrss.app ãæ¢ãŸã£ãŠããããããã§ãåããé äžãªæ§æã«è¡ãçããã
以äžã¯å šã³ãŒããæ²èŒããã
name: MLBear2 AIãã¥ãŒã¹ â Chatwork
on:
schedule:
- cron: '17 23 * * *' # JST 8:00
- cron: '17 11 * * *' # JST 20:00
workflow_dispatch:
permissions:
contents: write # last_guid.txt ãã³ãããããã®ã«å¿
èŠ
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Python ã»ããã¢ãã
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: äŸåã€ã³ã¹ããŒã«
run: pip install feedparser requests
- name: ã¹ã¯ãªããå®è¡
env:
CHATWORK_TOKEN: ${{ secrets.CHATWORK_TOKEN }}
RSS_URL: ${{ secrets.RSS_URL }}
run: python ml_bear_to_chatwork.py
- name: éä¿¡æžã¿GUIDãä¿å
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add last_guid.txt
git diff --staged --quiet || git commit -m "update last_guid [skip ci]"
git pushimport os
import re
import html
import requests
import feedparser
from html.parser import HTMLParser
# ================== èšå® ==================
CHATWORK_TOKEN = os.getenv("CHATWORK_TOKEN")
ROOM_ID = os.getenv("ROOM_ID", "********") # âãã£ããã¯ãŒã¯ROOMIDãå
¥ããŠãã ãã
RSS_URL = os.getenv("RSS_URL")
LAST_GUID_FILE = "last_guid.txt"
# ã¿ã€ãã«ã«ãã®ãããããå«ãŸããŠããã°ããŸãšãæçš¿ããšããŠæ±ã
KEYWORDS = ["ãã¥ãŒã¹ãŸãšã", "aiãã¥ãŒã¹"]
# ãªãŒããšããŠæ¡çšããªãå®åæïŒããã»ç»é²ãã©ãŒã çïŒãéšåäžèŽã§é€å€ã
LEAD_NG = ["Home", "Archive", "About", "æ¯æ¥AIãã¥ãŒã¹", "ãç»é²", "Subscribe", "Sign up"]
def get_last_guid():
try:
with open(LAST_GUID_FILE, encoding="utf-8") as f:
return f.read().strip()
except FileNotFoundError:
return None
def save_last_guid(guid):
with open(LAST_GUID_FILE, "w", encoding="utf-8") as f:
f.write(guid)
def is_matome(entry):
title = entry.title.lower()
return any(k in title for k in KEYWORDS)
# ================== èšäºããŒãžããæ¬ææœåº ==================
class _ArticleParser(HTMLParser):
"""èšäºããŒãž HTML ãã ãªãŒã段èœ(<p>) ãš ç®æ¬¡(<ol><li>) ãæœåºããã"""
def __init__(self):
super().__init__()
self.in_p = False
self.in_li = False
self.in_ol = False
self.cur = ""
self.paras = []
self.toc_items = []
self.seen_toc_heading = False
self._in_heading = False
self._heading_text = ""
self._toc_capturing = False
def handle_starttag(self, tag, attrs):
if tag == "p":
self.in_p = True
self.cur = ""
elif tag == "ol":
self.in_ol = True
if self.seen_toc_heading and not self.toc_items:
self._toc_capturing = True
elif tag == "li":
self.in_li = True
self.cur = ""
elif tag in ("h1", "h2", "h3", "h4"):
self._in_heading = True
self._heading_text = ""
def handle_endtag(self, tag):
if tag == "p" and self.in_p:
self.in_p = False
text = self.cur.strip()
if text:
self.paras.append(text)
elif tag == "ol":
self.in_ol = False
self._toc_capturing = False
elif tag == "li" and self.in_li:
self.in_li = False
text = self.cur.strip()
if text and self._toc_capturing:
self.toc_items.append(text)
elif tag in ("h1", "h2", "h3", "h4"):
self._in_heading = False
if "ç®æ¬¡" in self._heading_text:
self.seen_toc_heading = True
def handle_data(self, data):
if self.in_p or self.in_li:
self.cur += data
if self._in_heading:
self._heading_text += data
def _is_ng_lead(text):
return any(ng in text for ng in LEAD_NG)
def extract_from_article(article_url):
"""èšäºããŒãžãååŸã (lead[:3], toc[:10]) ãè¿ãã倱ææã¯ ([], [])ã"""
try:
r = requests.get(article_url, timeout=20,
headers={"User-Agent": "Mozilla/5.0 (ML_Bear bot)"})
r.raise_for_status()
except Exception as e:
print(f"â ïž èšäºããŒãžååŸå€±æ: {e}")
return [], []
parser = _ArticleParser()
try:
parser.feed(r.text)
except Exception:
pass
lead = []
for p in parser.paras:
t = html.unescape(p).strip()
if not t or _is_ng_lead(t):
continue
lead.append(t)
if len(lead) >= 3:
break
toc = [html.unescape(t).strip() for t in parser.toc_items[:10]]
return lead, toc
def _fallback_clean_text(raw_html):
text = re.sub(r"<script.*?</script>", "", raw_html, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r"<br\s*/?>", "\n", text, flags=re.IGNORECASE)
text = re.sub(r"</p\s*>", "\n", text, flags=re.IGNORECASE)
text = re.sub(r"<[^>]+>", "", text)
text = html.unescape(text)
text = re.sub(r"https?://\S+", "", text)
text = re.sub(r"[ \t\u3000]+", " ", text)
text = re.sub(r"\n{3,}", "\n\n", text)
return "\n".join(ln.rstrip() for ln in text.splitlines()).strip()
def format_for_chatwork(entry):
title = entry.get("title", "AIãã¥ãŒã¹ãŸãšã").strip()
link = entry.get("link", "")
lead, toc = ([], [])
if link:
lead, toc = extract_from_article(link)
parts = []
if lead:
parts.append("\n".join(lead))
if toc:
numbered = []
for i, item in enumerate(toc, 1):
item = item.strip()
head = item.split(".", 1)[0].strip()
if head.isdigit():
numbered.append(item)
else:
numbered.append(f"{i}. {item}")
parts.append("ãç®æ¬¡ã\n" + "\n".join(numbered))
if parts:
body = "\n\n".join(parts)
else:
raw = entry.get("description") or entry.get("summary") or ""
body = _fallback_clean_text(raw) or "(æ¬æãååŸã§ããŸããã§ãã)"
MAX_BODY = 4000
if len(body) > MAX_BODY:
body = body[:MAX_BODY].rstrip() + "\nâŠ(以äžç¥)"
link_line = f"\n\n⌠å
šæã¯ãã¡ã\n{link}" if link else ""
return f"[info][title]{title}[/title]\n{body}{link_line}[/info]"
# ================== éä¿¡ ==================
def send_to_chatwork(message):
url = f"https://api.chatwork.com/v2/rooms/{ROOM_ID}/messages"
headers = {"X-ChatWorkToken": CHATWORK_TOKEN}
r = requests.post(url, headers=headers, data={"body": message})
if r.status_code == 200:
print("â
éä¿¡æå")
return True
print(f"â é信倱æ {r.status_code} - {r.text[:300]}")
return False
def main():
if not CHATWORK_TOKEN or not RSS_URL:
print("â ïž CHATWORK_TOKEN ãŸã㯠RSS_URL ãæªèšå®ã§ã")
return
print("ð RSSååŸéå§")
feed = feedparser.parse(RSS_URL)
if not feed.entries:
print("â ïž RSSãšã³ããªããããŸããïŒãã£ãŒãURLã確èªããŠãã ããïŒ")
return
matched = [e for e in feed.entries if is_matome(e)]
if not matched:
print("該åœãããã¥ãŒã¹ãŸãšãæçš¿ãèŠã€ãããŸããã§ãã")
return
last_guid = get_last_guid()
if last_guid is None:
new_entries = matched[:1]
else:
new_entries = []
for e in matched:
if e.guid == last_guid:
break
new_entries.append(e)
if not new_entries:
print("æ°ãããŸãšãã¯ãããŸããïŒéä¿¡æžã¿ïŒ")
return
for e in reversed(new_entries):
print(f"ð° éä¿¡: {e.title[:50]} ...")
if send_to_chatwork(format_for_chatwork(e)):
save_last_guid(e.guid)
if __name__ == "__main__":
main()ãŸãšãïŒãåããããšãåãç¶ãããã¯éã
ååã®èšäºã§ãæžããããæ¹ããŠæãããäžåºŠåããããšãåãç¶ãããã®éã«ã¯ãæ·±ãè°·ããããå®å®ããŠåãããšããããšã¯å€§å€ãªããšã ãšå匷ã«ãªã£ãã
å€éšãµãŒãã¹ã«äŸåããèªååã¯ããã€ãå¿ ããåããã®éœåã§æ¢ãŸããrss.app ã®ææåããX ã® API ç· ãä»ããããã¡ãã«ã¯ã©ãã«ãã§ããªãã倧äºãªã®ã¯ãæ¢ãŸã£ããšãã«åå ãåãåããããããšããããŠæ å ±æºãäžæ®µäžæµã«ããã®ãŒããªããèããŠã¿ãããšã ã
ä»åã§ããã°ãX ãšãããã©ãããã©ãŒã ã«åºå·ãããé ä¿¡è æ¬äººã®å ¬åŒ RSS ãšããäžæµã«ç§»ã£ãããšãããã¹ãŠã解決ããã
åãããã«å€éšæ å ±ã瀟å ã«æµãèªååãçµãã§ãã人ã¯ããã²ããã®æ å ±æºããã£ãšäžæµã«å ¬åŒã®èå£ã¯ãªããïŒããäžåºŠçã£ãŠã¿ãŠã»ããããããŠããããã«äžçªããããªæ°ŽãæµããŠããã