2022-03-03 19:10:20 +00:00
|
|
|
import { fetchJson, sleep } from '../../utils'
|
2021-11-03 20:48:18 +00:00
|
|
|
|
2022-02-25 15:27:33 +00:00
|
|
|
export const chunkSize = 100;
|
2022-03-15 20:04:28 +00:00
|
|
|
const postURL = 'https://api.pushshift.io/reddit/submission/search/?fields=author,created_utc,domain,edited,id,link_flair_text,num_comments,permalink,position,removed_by_category,retrieved_on,retrieved_utc,score,selftext,subreddit,thumbnail,thumbnail_height,thumbnail_width,title,url&ids='
|
2022-05-09 19:55:18 +00:00
|
|
|
const commentURL = `https://api.pushshift.io/reddit/comment/search/?metadata=true&size=${chunkSize}&sort=asc&fields=author,body,created_utc,id,link_id,parent_id,retrieved_on,retrieved_utc,score,subreddit&link_id=`
|
2018-01-14 13:41:00 +00:00
|
|
|
|
2022-03-03 14:10:35 +00:00
|
|
|
const errorHandler = (msg, origError, from) => {
|
|
|
|
|
console.error(from + ': ' + origError)
|
|
|
|
|
const error = new Error(msg)
|
|
|
|
|
if (origError.name == 'TypeError') // Usually indicates that Pushshift is down
|
|
|
|
|
error.helpUrl = '/about#psdown'
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-19 03:23:34 +00:00
|
|
|
class TokenBucket {
|
|
|
|
|
|
|
|
|
|
// Refills tokens at a rate of one per msRefillIntvl millis, storing up to size tokens.
|
|
|
|
|
constructor(msRefillIntvl, size) {
|
|
|
|
|
if (!(msRefillIntvl > 0))
|
|
|
|
|
throw RangeError('msRefillIntvl must be > 0')
|
|
|
|
|
if (!(size > 0))
|
|
|
|
|
throw RangeError('size must be > 0')
|
|
|
|
|
this._msRefillIntvl = msRefillIntvl
|
|
|
|
|
this._maxSize = size
|
|
|
|
|
this._tokens = size
|
|
|
|
|
// Invariant: this._msNextRefill is valid iff this._tokens < this._maxSize
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Removes one token, waiting for it to refill if none are available.
|
|
|
|
|
async waitForToken() {
|
|
|
|
|
let msNow
|
|
|
|
|
// Calculate if/how many tokens to refill
|
|
|
|
|
if (this._tokens < this._maxSize) { // this._msNextRefill is valid
|
|
|
|
|
msNow = Date.now()
|
|
|
|
|
if (msNow >= this._msNextRefill) {
|
|
|
|
|
const newTokens = Math.floor((msNow - this._msNextRefill) / this._msRefillIntvl) + 1
|
|
|
|
|
this._tokens += newTokens
|
|
|
|
|
if (this._tokens < this._maxSize)
|
|
|
|
|
this._msNextRefill += newTokens * this._msRefillIntvl
|
|
|
|
|
else
|
|
|
|
|
this._tokens = this._maxSize // this._msNextRefill is now invalid
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Remove a token or wait for _msNextRefill, and recalculate it
|
|
|
|
|
if (this._tokens > 0) {
|
|
|
|
|
if (this._tokens == this._maxSize) // this._msNextRefill is invalid,
|
|
|
|
|
this._msNextRefill = (msNow || Date.now()) + this._msRefillIntvl // make it valid
|
|
|
|
|
this._tokens--
|
|
|
|
|
} else { // this._msNextRefill is valid and msNow has already been set above
|
|
|
|
|
await sleep(this._msNextRefill - msNow)
|
|
|
|
|
this._msNextRefill += this._msRefillIntvl
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Removes all tokens, and will refill the next token msNextAvail
|
|
|
|
|
// millis from now. After it's refilled, resumes normal refill rate.
|
|
|
|
|
setNextAvail(msNextAvail) {
|
2022-03-02 21:39:33 +00:00
|
|
|
this._tokens = 0
|
2022-02-19 03:23:34 +00:00
|
|
|
this._msNextRefill = Date.now() + msNextAvail
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pushshiftTokenBucket = new TokenBucket(1015, 7)
|
|
|
|
|
|
2022-02-17 01:32:24 +00:00
|
|
|
export const getPost = async threadID => {
|
2022-02-19 03:23:34 +00:00
|
|
|
await pushshiftTokenBucket.waitForToken()
|
2022-02-17 01:32:24 +00:00
|
|
|
try {
|
|
|
|
|
return (await fetchJson(`${postURL}${threadID}`)).data[0]
|
|
|
|
|
} catch (error) {
|
2022-03-21 15:58:32 +00:00
|
|
|
errorHandler('Could not get removed/edited post', error, 'pushshift.getPost')
|
2022-02-17 01:32:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
2018-01-14 19:21:53 +00:00
|
|
|
|
2022-05-09 19:55:18 +00:00
|
|
|
// Comments w/a created_utc in the ranges below must be queried *without* the faster `q=*` parameter:
|
|
|
|
|
// 1504224000 - 1506815999 (Sep/1/2017 0:00 - Sep/30/2017 23:59:59 UTC)
|
|
|
|
|
// 1517443200 - 1522540799 (Feb/1/2018 0:00 - Mar/31/2018 23:59:59 UTC)
|
|
|
|
|
// The ranges below subtract two weeks from the range start and optionally add two to the end.
|
|
|
|
|
const inBrokenRange = (utc, looseEnd = false) => looseEnd ?
|
|
|
|
|
utc > 1503014400 && utc < 1508025599 || utc > 1516233600 && utc < 1523750399 :
|
|
|
|
|
utc > 1503014400 && utc < 1506815999 || utc > 1516233600 && utc < 1522540799
|
|
|
|
|
|
2022-03-03 19:10:20 +00:00
|
|
|
// The callback() function is called with an Array of comments after each chunk is
|
|
|
|
|
// retrieved. It should return as quickly as possible (scheduling time-taking work
|
|
|
|
|
// later), and may return false to cause getComments to exit early, or true otherwise.
|
Improve permalinks and add parent link to comments
Beforehand, visiting a permalink would cause Unddit to begin loading
from the Post's first comment. If the max-to-download setting was too
small, and the permalinked comment wasn't yet encountered, no comment
tree would be displayed.
Now, only comments that occur at the same time as the permalinked
comment or later (chronologically) are downloaded.
This also adds a Parent link to each comment. When viewing a permalinked
comment, clicking the top-level Parent link will download only new
comments between the parent and child comments. Likewise, clicking the
'view rest of comments' link will only download new comments from the
first comment up until the previously viewed comment.
In general, Unddit will not re-download comments that it knows it has
previously downloaded, nor download more than max-to-download comments
per link clicked. This can leave 'gaps' in the list of downloaded
comments. If the 'load more comments' link is clicked, Unddit will only
download comments that can fill such gaps between the currently-linked
permalink (or if not viewing one, the first comment) and the last, up to
the max-to-download setting.
2022-03-18 18:10:20 +00:00
|
|
|
export const getComments = async (callback, threadID, maxComments, after = 0, before = undefined) => {
|
2022-05-09 19:55:18 +00:00
|
|
|
let chunks = Math.floor(maxComments / chunkSize), firstChunk = true, response, lastCreatedUtc = 1
|
2022-02-17 01:32:24 +00:00
|
|
|
while (true) {
|
2018-02-06 03:07:30 +00:00
|
|
|
|
2022-02-19 03:23:34 +00:00
|
|
|
let delay = 0
|
2022-02-17 01:32:24 +00:00
|
|
|
while (true) {
|
2022-05-09 19:55:18 +00:00
|
|
|
let query = commentURL + threadID
|
|
|
|
|
if (!inBrokenRange(after))
|
|
|
|
|
query += '&q=*'
|
|
|
|
|
if (after)
|
|
|
|
|
query += `&after=${after}`
|
|
|
|
|
if (before)
|
|
|
|
|
query += `&before=${before}`
|
2022-02-19 03:23:34 +00:00
|
|
|
await pushshiftTokenBucket.waitForToken()
|
2022-02-17 01:32:24 +00:00
|
|
|
try {
|
2022-05-09 19:55:18 +00:00
|
|
|
response = await fetchJson(query)
|
2022-02-17 01:32:24 +00:00
|
|
|
break
|
|
|
|
|
} catch (error) {
|
2022-03-03 14:10:35 +00:00
|
|
|
if (delay >= 8000) // after ~16s of consecutive failures
|
|
|
|
|
errorHandler('Could not get removed comments', error, 'pushshift.getComments') // rethrows
|
2022-02-19 03:23:34 +00:00
|
|
|
delay = delay * 2 || 125
|
|
|
|
|
pushshiftTokenBucket.setNextAvail(delay)
|
2022-03-18 15:54:15 +00:00
|
|
|
if (!callback([]))
|
|
|
|
|
return [ lastCreatedUtc, false ]
|
|
|
|
|
console.log('pushshift.getComments delay: ' + delay)
|
2022-02-17 01:32:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
2022-03-10 19:26:10 +00:00
|
|
|
const comments = response.data
|
2022-03-18 15:54:15 +00:00
|
|
|
const exitEarly = !callback(comments.map(c => ({
|
2022-02-17 01:32:24 +00:00
|
|
|
...c,
|
|
|
|
|
parent_id: c.parent_id?.substring(3) || threadID,
|
|
|
|
|
link_id: c.link_id?.substring(3) || threadID
|
2022-03-03 19:10:20 +00:00
|
|
|
})))
|
|
|
|
|
|
2022-05-09 19:55:18 +00:00
|
|
|
// If there's a chance the comments are in a broken range, restart the retrieval
|
|
|
|
|
if (firstChunk && !after && (comments.length === 0 || inBrokenRange(comments[0].created_utc, true)))
|
|
|
|
|
return getComments(callback, threadID, maxComments, 1503014401, before)
|
|
|
|
|
firstChunk = false
|
|
|
|
|
|
|
|
|
|
const loadedAllComments = response.metadata.hasOwnProperty('total_results') ?
|
|
|
|
|
response.metadata.results_returned >= response.metadata.total_results :
|
|
|
|
|
comments.length < chunkSize/2
|
2022-02-25 18:25:02 +00:00
|
|
|
if (comments.length)
|
|
|
|
|
lastCreatedUtc = comments[comments.length - 1].created_utc
|
2022-03-03 19:10:20 +00:00
|
|
|
if (loadedAllComments || chunks <= 1 || exitEarly)
|
|
|
|
|
return [ lastCreatedUtc, loadedAllComments ]
|
2022-02-19 18:27:40 +00:00
|
|
|
chunks--
|
2022-02-25 18:25:02 +00:00
|
|
|
after = Math.max(lastCreatedUtc - 1, after + 1)
|
2022-02-17 01:32:24 +00:00
|
|
|
}
|
|
|
|
|
}
|