mirror of https://github.com/ekimekim/wubloader
Add youtubebot
This adds a zulip bot that polls the youtube API for new comment threads, and posts them to Zulip. Some limitations: - It doesn't keep any state, so it won't post anything it "missed" while not running. - It can only find top-level comments, not replies - For quota reasons, we shouldn't poll more often than every 1 minute (at this rate we consume approx 1 upload worth of quota per day) - If somehow there are more than 100 comments within 1 minute, it will miss all but the last 100.mike/playlist-debug
parent
3d9490d335
commit
54fd356b39
@ -0,0 +1,107 @@
|
|||||||
|
|
||||||
|
import gevent.monkey
|
||||||
|
gevent.monkey.patch_all()
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from common.googleapis import GoogleAPIClient
|
||||||
|
|
||||||
|
from .config import get_config
|
||||||
|
from .zulip import Client
|
||||||
|
|
||||||
|
def get_comments(google, channel_id):
|
||||||
|
resp = google.request("GET",
|
||||||
|
"https://www.googleapis.com/youtube/v3/commentThreads",
|
||||||
|
params={
|
||||||
|
"part": "snippet",
|
||||||
|
"allThreadsRelatedToChannelId": channel_id,
|
||||||
|
"maxResults": "100",
|
||||||
|
"textFormat": "plainText",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
items = resp.json()["items"][::-1] # flip direction so we get earliest first
|
||||||
|
if items:
|
||||||
|
earliest = items[0]["snippet"]["topLevelComment"]
|
||||||
|
logging.info(f"Got {len(items)} comment threads, oldest is {earliest['id']} at {earliest['snippet']['publishedAt']}")
|
||||||
|
else:
|
||||||
|
logging.info("Got no comment threads")
|
||||||
|
# We could look at replies, but since we can only check for new replies in the first 100 threads,
|
||||||
|
# we'd rather just never show them than confuse people when they don't show up sometimes.
|
||||||
|
comments = []
|
||||||
|
for thread in items:
|
||||||
|
logging.debug(f"Got thread: {json.dumps(thread)}")
|
||||||
|
comment = thread["snippet"]["topLevelComment"]
|
||||||
|
comment["videoId"] = thread["snippet"]["videoId"]
|
||||||
|
comments.append(comment)
|
||||||
|
return comments
|
||||||
|
|
||||||
|
|
||||||
|
def show_comment(zulip, stream, topic, comment):
|
||||||
|
c = comment["snippet"]
|
||||||
|
author = f"[{c['authorDisplayName']}]({c['authorChannelUrl']})"
|
||||||
|
video = f"https://youtu.be/{comment['videoId']}"
|
||||||
|
message = f"{author} commented on {video}:\n```quote\n{c['textDisplay']}\n```"
|
||||||
|
logging.info(f"Sending message to {stream}/{topic}: {message!r}")
|
||||||
|
# Empty stream acts as a dry-run mode
|
||||||
|
if stream:
|
||||||
|
zulip.send_to_stream(stream, topic, message)
|
||||||
|
|
||||||
|
|
||||||
|
def main(conf_file, interval=60, one_off=0, stream="bot-spam", topic="Youtube Comments", keep=1000, log="INFO"):
|
||||||
|
"""Config:
|
||||||
|
zulip_url
|
||||||
|
zulip_email
|
||||||
|
zulip_api_key
|
||||||
|
channel_id
|
||||||
|
google_credentials_file:
|
||||||
|
Path to json file containing at least:
|
||||||
|
client_id
|
||||||
|
client_secret
|
||||||
|
refresh_token
|
||||||
|
These creds should be authed as the target account with Youtube Data API read perms
|
||||||
|
|
||||||
|
In one-off=N mode, get the last N comments and show them, then exit.
|
||||||
|
"""
|
||||||
|
logging.basicConfig(level=log)
|
||||||
|
|
||||||
|
config = get_config(conf_file)
|
||||||
|
zulip = Client(config["zulip_url"], config["zulip_email"], config["zulip_api_key"])
|
||||||
|
with open(config["google_credentials_file"]) as f:
|
||||||
|
credentials = json.load(f)
|
||||||
|
google = GoogleAPIClient(credentials["client_id"], credentials["client_secret"], credentials["refresh_token"])
|
||||||
|
channel_id = config["channel_id"]
|
||||||
|
|
||||||
|
if one_off:
|
||||||
|
comments = get_comments(google, channel_id)
|
||||||
|
for comment in comments[-one_off:]:
|
||||||
|
show_comment(zulip, stream, topic, comment)
|
||||||
|
return
|
||||||
|
|
||||||
|
seen = None
|
||||||
|
while True:
|
||||||
|
start = time.monotonic()
|
||||||
|
|
||||||
|
if seen is None:
|
||||||
|
# Get latest messages as of startup, so we know what's new next time
|
||||||
|
seen = [comment["id"] for comment in get_comments(google, channel_id)]
|
||||||
|
else:
|
||||||
|
for comment in get_comments(google, channel_id):
|
||||||
|
if comment["id"] in seen:
|
||||||
|
logging.debug(f"Comment {comment['id']} already seen, skipping")
|
||||||
|
continue
|
||||||
|
show_comment(zulip, stream, topic, comment)
|
||||||
|
seen.append(comment["id"])
|
||||||
|
seen = seen[-keep:]
|
||||||
|
|
||||||
|
remaining = start + interval - time.monotonic()
|
||||||
|
logging.debug(f"Keeping {len(seen)} seen, waiting {remaining:.2f}s")
|
||||||
|
if remaining > 0:
|
||||||
|
time.sleep(remaining)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import argh
|
||||||
|
argh.dispatch_command(main)
|
Loading…
Reference in New Issue